在没有符号解析的情况下如何进行编译?

时间:2020-12-21 01:43:01

标签: c linux gcc compilation elf

这是我的问题。假设你要编译c代码:

void some_function() {
  write_string("Hello, World!\n");
}

对于这个例子,我想特别关注字符串:“Hello, World!\n”。我的理解是编译器会将字符串放入 elf 文件中的 .rodata 部分。将引用其在 .rodata 部分中的位置的符号添加到符号表中,并将该符号保留在 .text 部分中作为字符串位置的占位符。

问题来了。你怎么能在机器代码中留下一个未解决的值?在 x86 中,当位置已知时,链接器应该很容易在符号上进行查找和替换。但是,在许多 CPU 架构中,地址无法全部编码为一条机器指令。因此,必须使用单独的机器指令分 2 个阶段加载该值,并且链接器必须弄清楚这一点。它必须足够聪明,才能用一个地方的一半地址和另一个地方的一半地址来操纵机器代码。此外,稍后 elf 文件必须以某种方式为链接器表示这种复杂的编码方案。这一切是如何运作的?

我的大多数程序,这将在用户空间应用程序中。所以内核可以在内存中的任何地方加载 .rodata 部分。因此,当程序加载时,不知何故,在运行时,内核加载器必须在开始执行之前解析程序中的所有这些符号。它必须注入机器代码,将每个部分放在其中,以便可以适当地引用它们。这是如何工作的?

我觉得我的理解和上述描述是错误的,或者我遗漏了一些非常重要的东西,因为这对我来说似乎不正确。等等,或者实际上存在在现代内核和链接器中执行这些复杂功能的逻辑。我正在寻找进一步的解释和理解。

1 个答案:

答案 0 :(得分:2)

编译发生,发出如下内容:

lea rdi, [rip+some_function.hello_world]
mov rax, [rip+some_function.write_string]
call rax

在 asm pass 之后,我们最终得到了一些可以反汇编的东西

lea rdi, [rip+00000000]
mov rax, [rip+00000000]
call rax

其中两个 00000000 槽被填充为加载时修正。加载器执行符号解析并用正确的值填充 00000000 值。

这是一种简化。实际上,还有一个额外的间接层,称为全局偏移表,用于(除其他外)将所有修正放在一起。

它是如何工作的内部是 CPU 和操作系统特定的,但一般来说你真的不必关心它是如何工作的,它可能会在编译器的下一个版本中改变(并且已经改变了至少两次)。加载程序使用修复表在非常通用的级别上理解修复,并且可以处理新想法,只要它们解析将符号的(绝对或相对)地址放在偏移量 + 大小处即可。

Alpha 处理器在当时的情况有点糟糕。修正必须在函数之间,并且相对寻址只能以有符号的 16 位大小完成,因此函数的修正位于每个函数之前或之后,如果指针没有,则可能在 ASM 传递中出现错误不适合,因为函数太大了。我确实想出了一个聪明的序列来解决 Alpha 上的问题,但那是在平台退役后很久,没人关心,所以它从未实施。

我记得在加载程序可以进行良好修补之前的糟糕日子。曾经有一个共享库加载地址的全局(我的意思是全局)表,编译器发出绝对地址,如果你改变了一个库,你必须重建你的应用程序,即使你使用了共享库。那并不是最聪明的想法,难怪人们会保留静态链接的紧急二进制文件。破解 libc 并不好玩。