全局指针变量如何存储在内存中?
假设我们有一个简单的代码:
int* q = new int(13);
int main() {
return 0;
}
显然,变量q
是全局的并且已初始化。 从这个答案中,我们希望q
变量存储在程序文件中的初始化数据段(.data)中 ,但它是一个指针,所以它的值(它是堆段中的地址)在运行时确定。 那么程序文件中存储在数据段中的值是什么?
我的尝试:
在我看来,编译器为数据段中的变量q
(通常为8个字节,用于64位地址)分配了一些空间,没有任何有意义的值。 然后,在main
函数代码之前在文本段中放置一些初始化代码以在运行时初始化q
变量。 汇编中的这样的东西:
....
mov edi, 4
call operator new(unsigned long)
mov DWORD PTR [rax], 13 // rax: 64 bit address (pointer value)
// offset : q variable offset in data segment, calculated by compiler
mov QWORD PTR [ds+offset], rax // store address in data segment
....
main:
....
任何想法?
是的,这基本上是如何工作的。
请注意,在ELF中.data
, .bss
和.text
实际上是部分,而不是段。 你可以通过运行你的编译器来查看程序集:
c++ -S -O2 test.cpp
您通常会看到一个main
函数,以及该函数之外的某种初始化代码。 程序入口点(C ++运行时的一部分)将调用初始化代码,然后调用main
。 初始化代码也负责运行构造函数之类的东西。
int *q
将进入.bss
而不是.data
部分,因为它只在运行时由非常量初始化器初始化(所以这只在C ++中合法,而不是在C中)。 对于它,可执行文件的数据段中不需要有8个字节。
编译器通过将其地址放入CRT(C运行时间)启动代码在调用main
之前调用的初始化程序数组中来安排初始化函数的运行。
在Godbolt编译器资源管理器中,你可以看到init函数的asm没有所有的指令噪声。 注意寻址模式只是一个简单的RIP相对访问q
。 链接器在这一点上填充了RIP的右侧偏移量,因为即使.text
和.bss
段最终在单独的段中,链接时间常量也是如此。
Godbolt的编译器噪声过滤对我们来说并不理想。 一些指令是相关的,但其中许多不是。 下面是一个手动选择的gcc6.2 -O3
asm输出和Godbolt的“过滤器指令”选项混合使用,只需要int* q = new int(13);
声明。 (不需要同时编译一个main
文件,我们没有链接一个可执行文件)。
# gcc6.2 -O3 output
_GLOBAL__sub_I_q: # presumably stands for subroutine
sub rsp, 8 # align the stack for calling another function
mov edi, 4 # 4 bytes
call operator new(unsigned long) # this is the demangled name, like from objdump -dC
mov DWORD PTR [rax], 13
mov QWORD PTR q[rip], rax # clang uses the equivalent `[rip + q]`
add rsp, 8
ret
.globl q
.bss
q:
.zero 8 # reserve 8 bytes in the BSS
没有提及ELF数据(或任何其他)的基础。
也肯定没有段寄存器覆盖。 ELF段与x86段无关。 (无论如何,默认段寄存器是DS
,所以编译器不需要发出[ds:rip+q]
或任何其他内容。有些反汇编可能是显式的,并且显示DS,即使在DS上没有段覆盖前缀指令,但是。)
这是编译器安排它在main()
之前被调用的方式:
# the "aw" sets options / flags for this section to tell the linker about it.
.section .init_array,"aw"
.align 8
.quad _GLOBAL__sub_I_q # this assembles to the absolute address of the function.
CRT开始代码有一个循环,它知道.init_array
部分的大小,并.init_array
在每个函数指针上使用内存间接call
指令。
.init_array
节标记为可写,因此它会进入数据段。 我不确定写什么。 也许CRT代码将它标记为已经完成了,并在调用它们之后调零指针?
在Linux中有一个用于在动态链接库中运行初始化程序的类似机制,这是由ELF解释程序在进行动态链接时完成的。 这就是为什么你可以调用printf()
或从其他glibc的标准输入输出功能_start
从手写ASM创建一个动态链接的二进制文件,为什么在一个静态链接二进制失败,如果你不调用正确的初始化函数。 (有关构建静态或动态二进制文件的更多信息,请参阅此Q&A,这些二进制文件定义了自己的_start
或main()
,带有或不带有libc)。