没有符号解析怎么能编译呢?
How can compilation occur without symbol resolution?
这是我的问题。假设你要编译c代码:
void some_function() {
write_string("Hello, World!\n");
}
对于这个例子,我想特别关注字符串:“Hello, World!\n”。我的理解是编译器会将字符串放入 elf 文件的 .rodata 部分。将引用其在 .rodata 部分中的位置的符号添加到符号 table 中,并且该符号保留在 .text 部分中作为字符串位置的占位符。
问题来了。你怎么能在机器代码中留下这样一个未解析的值呢?在 x86 中,当位置已知时,链接器应该很容易在符号上进行查找和替换。但是,有许多 CPU 架构无法将地址完整编码为单个机器指令。因此,该值必须分两个阶段加载,使用单独的机器指令,链接器必须解决这个问题。它必须足够聪明,才能用一个地方的一半地址和另一个地方的一半地址来操纵机器码。此外,elf 文件必须以某种方式为稍后的链接器表示这种复杂的编码方案。这一切是如何运作的?
我的大多数程序,这将在用户 space 应用程序中。所以内核可以在内存中任何它想要的地方加载 .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
其中两个 00000000
插槽被填充为加载时修正。加载程序执行符号解析并用正确的值填充 00000000
值。
这是一个简化。实际上,有一个额外的间接层称为全局偏移量 table,它用于(除其他外)将所有修正彼此相邻。
其工作原理的内部结构是 CPU 和 OS 特定的,但通常您不必真正关心它是如何工作的,它可能会在下一个版本中改变编译器(并且已经至少改变了两次)。加载程序使用修正 table 在非常通用的级别上理解修正,并且可以处理新想法,只要他们决定将符号的(绝对或相对)地址放在偏移量 + 大小处。
Alpha 处理器过去有点糟糕。 Fixups 必须在函数之间,相对寻址只能以有符号的 16 位大小完成,因此函数的 fixups 位于每个函数之前或之后,如果指针没有,则可能在 ASM pass 中出现错误'不适合,因为功能太大了。我确实想出了一个聪明的序列,可以解决 Alpha 上的问题,但那是在平台退役很久之后,没人再关心了,所以它从未被实施。
我记得加载程序可以进行良好修补之前的糟糕日子。曾经有一个共享库加载地址的全局(我的意思是全局)table,并且编译器发出绝对地址,如果您更改了库,则必须重建您的应用程序,即使您使用了共享库。那不是最聪明的想法,难怪人们到处都是静态链接的紧急二进制文件。破坏 libc 并不好玩。
这是我的问题。假设你要编译c代码:
void some_function() {
write_string("Hello, World!\n");
}
对于这个例子,我想特别关注字符串:“Hello, World!\n”。我的理解是编译器会将字符串放入 elf 文件的 .rodata 部分。将引用其在 .rodata 部分中的位置的符号添加到符号 table 中,并且该符号保留在 .text 部分中作为字符串位置的占位符。
问题来了。你怎么能在机器代码中留下这样一个未解析的值呢?在 x86 中,当位置已知时,链接器应该很容易在符号上进行查找和替换。但是,有许多 CPU 架构无法将地址完整编码为单个机器指令。因此,该值必须分两个阶段加载,使用单独的机器指令,链接器必须解决这个问题。它必须足够聪明,才能用一个地方的一半地址和另一个地方的一半地址来操纵机器码。此外,elf 文件必须以某种方式为稍后的链接器表示这种复杂的编码方案。这一切是如何运作的?
我的大多数程序,这将在用户 space 应用程序中。所以内核可以在内存中任何它想要的地方加载 .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
其中两个 00000000
插槽被填充为加载时修正。加载程序执行符号解析并用正确的值填充 00000000
值。
这是一个简化。实际上,有一个额外的间接层称为全局偏移量 table,它用于(除其他外)将所有修正彼此相邻。
其工作原理的内部结构是 CPU 和 OS 特定的,但通常您不必真正关心它是如何工作的,它可能会在下一个版本中改变编译器(并且已经至少改变了两次)。加载程序使用修正 table 在非常通用的级别上理解修正,并且可以处理新想法,只要他们决定将符号的(绝对或相对)地址放在偏移量 + 大小处。
Alpha 处理器过去有点糟糕。 Fixups 必须在函数之间,相对寻址只能以有符号的 16 位大小完成,因此函数的 fixups 位于每个函数之前或之后,如果指针没有,则可能在 ASM pass 中出现错误'不适合,因为功能太大了。我确实想出了一个聪明的序列,可以解决 Alpha 上的问题,但那是在平台退役很久之后,没人再关心了,所以它从未被实施。
我记得加载程序可以进行良好修补之前的糟糕日子。曾经有一个共享库加载地址的全局(我的意思是全局)table,并且编译器发出绝对地址,如果您更改了库,则必须重建您的应用程序,即使您使用了共享库。那不是最聪明的想法,难怪人们到处都是静态链接的紧急二进制文件。破坏 libc 并不好玩。