假设我们有一个简单的代码:
int* q = new int(13);
int main() {
return 0;
}
显然,变量q
是全局的并且已初始化。从this answer开始,我们希望q
变量存储在程序文件中的初始化数据段(.data)中,但它是一个指针,所以它的值(这是一个地址)在堆段中)在运行时确定。那么程序文件中存储在数据段中的值是什么?
我的尝试:
在我看来,编译器在数据段中为变量q
分配一些空间(通常为64位地址为8个字节),没有任何有意义的值。然后,在main
函数代码之前的 text 段中放置一些初始化代码,以便在运行时初始化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:
....
有什么想法吗?
答案 0 :(得分:3)
是的,这基本上就是它的运作方式。
请注意,在ELF .data
中,.bss
和.text
实际上是部分,而不是细分。您可以通过运行编译器来自行查看程序集:
c++ -S -O2 test.cpp
您通常会看到main
函数,以及该函数之外的某种初始化代码。程序入口点(C ++运行时的一部分)将调用初始化代码,然后调用main
。初始化代码还负责运行构造函数。
答案 1 :(得分:2)
int *q
将进入.bss
,而不是.data
部分,因为它仅在运行时由非常量初始化程序初始化(因此这仅在C ++中是合法的,不在C)。在可执行文件的数据段中不需要8个字节。
编译器安排初始化函数运行,方法是将其地址放入CRT(C运行时)启动代码调用的初始化器数组中,然后再调用main
。
在Godbolt编译器资源管理器中,您可以看到init函数的asm,而没有指令的所有噪声。请注意,寻址模式只是对q
的简单RIP相对访问。链接器此时填充右侧偏移量,因为即使.text
和.bss
部分最终位于不同的段中,这也是链接时间常量。
Godbolt的compiler-noise filtering对我们来说并不理想。有些指令是相关的,但其中很多都没有。以下是gcc6.2 -O3
asm output with Godbolt's "filter directives" option unchecked的手动选择组合,仅适用于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解释器在进行动态链接时完成的。这就是为什么你可以在从手写asm创建的动态链接二进制文件中从printf()
调用_start
或其他glibc stdio函数的原因,以及为什么在静态链接二进制文件中如果你不调用则失败的原因正确的init函数。 (有关构建定义自己的_start
或仅main()
的静态或动态二进制文件的更多信息,请参阅this Q&A,无论是否有libc。)