我想知道当我们使用链接器获取目标代码的可执行版本时会发生什么。
我认为 Linux 和 Windows 的链接器工作不一样,我在 Linux 上。
目标代码缺乏有关全局的信息。它包含函数的可执行代码,但对其他外部函数以及全局数据的所有引用都不能成为实际指令的一部分,因为它们的地址未知。因此,所有这些引用都留空(例如,仅在目标代码中填充零字节)并用符号名称进行注释。
链接器的工作是查看所有丢失的符号名称并将它们与所有导出的名称(即目标文件提供的函数和全局数据)进行匹配,然后为每个数据找到永久位置,最后重写所有代码将零字节替换为最终存储数据(函数和全局变量)的实际地址。
例如,考虑这段 C 代码:
extern int a;
extern int bar(int); // "extern" is redundant here
static int zip(int);
int foo(int x, int y)
{
return 2 * x + 3 * y + zip(x - y) + a * bar(x + y);
}
int zip(int n)
{
return 2 * (n + 1) - (n - 1) / 2;
}
此代码导出一个符号
foo
,将其提供给链接到此翻译单元的任何人。它还缺少两个符号:a
和 bar
。在实现 foo
的代码中,对 a
和 bar
的引用留空,只有当链接器知道这些实际数据所在的位置时才能由链接器填充。
这是 GCC 使用
-O3
为 x86 生成的机器代码:
0000000000000000 <foo>:
0: 89 f9 mov ecx,edi
2: 8d 04 76 lea eax,[rsi+rsi*2]
5: 53 push rbx
6: 29 f1 sub ecx,esi
8: 8d 51 ff lea edx,[rcx-0x1]
b: 8d 1c 78 lea ebx,[rax+rdi*2]
e: 01 f7 add edi,esi
10: 89 d0 mov eax,edx
12: c1 e8 1f shr eax,0x1f
15: 01 c2 add edx,eax
17: d1 fa sar edx,1
19: f7 da neg edx
1b: 8d 44 4a 02 lea eax,[rdx+rcx*2+0x2]
1f: 01 c3 add ebx,eax
21: e8 00 00 00 00 call 26 <foo+0x26>
22: R_X86_64_PC32 bar-0x4
26: 0f af 05 00 00 00 00 imul eax,DWORD PTR [rip+0x0] # 2d <foo+0x2d>
29: R_X86_64_PC32 a-0x4
2d: 01 d8 add eax,ebx
2f: 5b pop rbx
30: c3 ret
注意字节 22 和 29:操作数保留为零,但有一个注释告诉链接器要填充的符号名称。
除了 Kerrek 的回答之外:链接器的工作在某种程度上依赖于操作系统。例如,处理外部引用(来自 .so 或 .dll 文件)的方式取决于操作系统,不同的段(数据、代码等)如何放置在文件中也可能取决于操作系统。
可执行文件的标头(也由链接器生成)是特定于操作系统的,定义文件类型以及在哪里可以找到不同的段。 Linux 中的可执行文件以“ELF”标头开头,在 Windows 中以“MZ”标头开头(这些是可以在文件开头找到的标识字符)。
我认为 Linux 和 Windows 的链接器工作不一样
只是对 Kerrek SB 的答案进行一些补充:
链接器在所有操作系统上的工作方式都相同。只是对象文件和二进制文件的文件格式不同。