这是我的问题。假设您想要编译c代码:
void some_function() {
write_string("Hello, World!n");
}
对于这个例子,我想特别关注字符串:;你好,世界!\n〃;。我的理解是,编译器会将字符串放入elf文件中的.rodata部分。一个符号(指其在.rodata部分中的位置(被添加到符号表中,该符号作为字符串位置的占位符保存在.text部分中。
问题来了。你怎么能在机器代码中留下这样一个未解析的值呢?在x86中,当位置已知时,链接器应该很容易对符号进行查找和替换。然而,在许多CPU体系结构中,地址不能完全编码为单个机器指令。因此,该值必须分两个阶段加载,使用单独的机器指令,链接器必须弄清楚这一点。它必须足够聪明,才能将一半的地址放在一个地方,一半的地址在另一个地方操纵机器代码。此外,不知何故,elf文件必须稍后为链接器表示这种复杂的编码方案。这一切是如何工作的?
我大多数程序,这都是在用户空间应用程序。因此内核可以在内存中任意位置加载.rodata部分。因此,当程序在运行时加载时,内核加载程序似乎必须在开始执行之前解析程序中的所有这些符号。它必须将代码注入到机器中,将每个部分放在其中,以便可以适当地引用它们。这是怎么回事?
我有一种感觉,我的理解和上面的描述是错误的,或者我错过了一些非常重要的东西,因为这对我来说似乎不正确。以太,或者说,事实上,在现代内核和链接器中,有一个逻辑可以预制这些复杂的函数。我正在寻求进一步的解释和理解。
编译发生,发出这样的东西:
lea rdi, [rip+some_function.hello_world]
mov rax, [rip+some_function.write_string]
call rax
asm通过后,我们最终得到了一些反汇编到的东西
lea rdi, [rip+00000000]
mov rax, [rip+00000000]
call rax
其中两个CCD_ 1时隙被填充为加载时间固定。加载器执行符号解析,并用正确的值填充00000000
值。
这是一种简化。事实上,还有一个额外的间接层,称为全局偏移表,用于(除其他外(将所有修正放在一起。
它的工作原理是特定于CPU和操作系统的,但一般来说,你不必真正关心它是如何工作的,它可能会在下一版本的编译器中发生变化(并且已经至少改变了两次(。加载器使用fixup表在一个非常通用的级别上理解fixup,并且可以处理新的想法,只要他们决心将符号的(绝对或相对(地址放在偏移量+大小。
阿尔法处理器在当时有点糟糕。修正必须在函数之间,并且相对寻址只能以有符号的16位大小进行,因此函数的修正位于每个函数之前或之后,如果由于函数太大导致指针不合适,则可能会在ASM过程中出错。我确实想出了一个聪明的序列来解决Alpha上的问题,但那是在平台退役很久之后,没有人再在乎了,所以它从未实现过。
我还记得以前装载机能拍出好拍之前的糟糕日子。曾经有一个共享库加载地址的全局(我指的是全局(表,编译器会发出绝对地址,如果您更改了库,即使您使用了共享库,您也必须重新构建应用程序。这并不是最聪明的想法,难怪人们会发现周围有静态链接的紧急二进制文件。打破libc并不好玩。