int *q
将放在.bss
而不是.data
部分,因为它只是由非常量初始化器在运行时初始化(所以这只有在C++中合法,而不是在C中)。对于它来说,在可执行文件的数据段中没有必要有8个字节。
编译器通过将初始化函数的地址放入一个初始化程序数组中来安排初始化函数在调用main
之前运行的CRT(C运行时)启动代码来运行。
在Godbolt编译器资源管理器上,您可以看到初始化函数的汇编码,而不会出现所有指令的噪音。请注意,寻址模式只是对q
进行简单的RIP相对访问。由于这是一个链接时间常量,即使.text
和.bss
部分最终位于不同的段中,链接器也会在此时填充正确的RIP偏移量。
Godbolt的编译器噪声过滤对我们来说并不理想。一些指示符是相关的,但许多指示符则不是。下面是一个手动选择的混合物,其中包含了使用Godbolt的“过滤指令”选项未选中的gcc6.2-O3
汇编输出,只包括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,即使在指令上没有段覆盖前缀。)
这就是编译器在调用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
部分的大小,并依次使用内存间接
call
指令调用每个函数指针。
.init_array
部分标记为可写入,因此它进入数据段。我不确定是什么将其写入。也许CRT代码在调用它们后通过清零指针来标记它们已完成?
Linux中有一个类似的机制用于在动态库中运行初始化程序,这是由ELF解释器在执行动态链接时完成的。这就是为什么您可以从手写汇编创建的动态链接二进制文件的
_start
中调用
printf()
或其他glibc stdio函数,而如果您不调用正确的init函数,则静态链接二进制文件会失败。 (有关构建定义自己的
_start
或只有
main()
的静态或动态二进制文件的更多信息,请参见
此Q&A)。
gcc
进行链接时,默认的起始点是_start
。这就是初始化代码所在的位置,然后它将调用main
函数。因此,那些没有使用clib但要与gcc
进行链接的汇编编程人员,必须在其代码开头放置_start:
标签,而那些链接默认clib的人则从他们的源文件中的main:
开始(在二进制文件中,起始点是来自库的_start:
)。 :) - Ped7g_start
不是一个函数,因此您不能在 C 或 C++ 中编写_start
。在汇编中,您不必编写函数,可以编写任意代码,因此您可以自己编写_start
。 - Dietrich Epp