目标文件中的符号引用到底是什么?
Exactly what is a symbol reference in an object file?
我正在从程序员的角度阅读计算机系统,关于链接的章节。它解释了如何使用程序 ld 在 linux x86-64 中进行链接。作者声称,为了从可重定位目标文件构建可执行文件,链接器做了两件事:符号解析和重定位。这是他们对符号解析的简要概述:
Object files define and reference symbols, where each symbol corresponds to a function, a global variable, or a static variable (i.e., any C variable declared with the static attribute). The purpose of symbol resolution is to associate each symbol reference with exactly one symbol definition.
但他们并没有阐明符号引用的含义,即使他们开始深入描述符号解析。那么在可重定位目标文件中究竟是如何引用符号的呢?
考虑以下来源:
static int foo() { return 42; }
static int bar() { return foo() + 1; }
extern int baz();
int main()
{
return foo() + bar() + baz();
}
在 gcc -c foo.c
之后,objdump -d foo.o
在 x86_64 Linux 上的输出是:
foo.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <foo>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: b8 2a 00 00 00 mov [=11=]x2a,%eax
9: 5d pop %rbp
a: c3 retq
000000000000000b <bar>:
b: 55 push %rbp
c: 48 89 e5 mov %rsp,%rbp
f: b8 00 00 00 00 mov [=11=]x0,%eax
14: e8 e7 ff ff ff callq 0 <foo>
19: 83 c0 01 add [=11=]x1,%eax
1c: 5d pop %rbp
1d: c3 retq
000000000000001e <main>:
1e: 55 push %rbp
1f: 48 89 e5 mov %rsp,%rbp
22: 53 push %rbx
23: 48 83 ec 08 sub [=11=]x8,%rsp
27: b8 00 00 00 00 mov [=11=]x0,%eax
2c: e8 cf ff ff ff callq 0 <foo>
31: 89 c3 mov %eax,%ebx
33: b8 00 00 00 00 mov [=11=]x0,%eax
38: e8 ce ff ff ff callq b <bar>
3d: 01 c3 add %eax,%ebx
3f: b8 00 00 00 00 mov [=11=]x0,%eax
44: e8 00 00 00 00 callq 49 <main+0x2b>
49: 01 d8 add %ebx,%eax
4b: 48 83 c4 08 add [=11=]x8,%rsp
4f: 5b pop %rbx
50: 5d pop %rbp
51: c3 retq
这里有几点需要注意:
- 注意
bar
如何在地址 0
调用 foo
?
objdump
如何知道正在调用的是 foo
?
它真的可以在地址 0 吗? (大多数现代系统将虚拟内存的零页映射到 PROT_NONE
,因此那里不会发生读取或写入访问。)
- 请注意从
main
调用 baz
与调用 foo
和 bar
有何不同?编译器知道 foo
和 bar
相对于调用指令本身的位置,但不知道 baz
的位置。
那么,鉴于以上信息,linker 如何才能将其变成合理的东西?不能:这里没有足够的信息。
为了让 linker 能够 link 引用 baz
(我们还没有看到)调用 baz
,它需要额外的信息。在 ELF 系统上,附加信息被写入此处的特殊部分 .rela.text
,其中包含:
$ readelf -Wr foo.o
Relocation section '.rela.text' at offset 0x5d0 contains 1 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000000045 0000000b00000002 R_X86_64_PC32 0000000000000000 baz - 4
那个是书中所说的“参考”,但没有定义。它告诉 linker:如果你能找到 baz
的定义(在其他一些对象中),获取它的地址,然后把它(实际上,&baz - 4
因为 CALL
指令是相对于 next 指令之后的 CALL
) into bytes [45-48] of .text
section of foo.o
.
如果没有这样的定义呢? linker 会产生一个错误:
$ gcc foo.o
foo.o: In function `main':
foo.c:(.text+0x45): undefined reference to `baz'
collect2: error: ld returned 1 exit status
最后,回到上面的第 1 点:foo
真的可以在地址 0 吗?
不,但是地址 0x14
处的 CALL
指令实际上并没有说 CALL 0
。它说“在调用后下一条指令的地址处调用例程,负 25”。如果最终二进制文件中的调用指令在地址 0x400501
结束,那么该调用的目标将是 0x4004ed
,这就是 foo
结束的位置([=17 之间的距离=] 并且当 linker 将 foo.o
的 .text
部分重新定位到不同的地址时 CALL
不会改变(尽管 linker 放宽了;但那是复杂的话题改天)。
Employed Russian 的回答很好,但也有一个简短的回答:符号引用是您使用变量(或函数名称)的任何时候。符号定义创建一个变量(或函数名)。
因此符号定义为 int bar;
(只要它是全局的)或 int foo() { ... }
。符号引用将是 foo(bar)
(两个引用:foo
和 bar
)。
我正在从程序员的角度阅读计算机系统,关于链接的章节。它解释了如何使用程序 ld 在 linux x86-64 中进行链接。作者声称,为了从可重定位目标文件构建可执行文件,链接器做了两件事:符号解析和重定位。这是他们对符号解析的简要概述:
Object files define and reference symbols, where each symbol corresponds to a function, a global variable, or a static variable (i.e., any C variable declared with the static attribute). The purpose of symbol resolution is to associate each symbol reference with exactly one symbol definition.
但他们并没有阐明符号引用的含义,即使他们开始深入描述符号解析。那么在可重定位目标文件中究竟是如何引用符号的呢?
考虑以下来源:
static int foo() { return 42; }
static int bar() { return foo() + 1; }
extern int baz();
int main()
{
return foo() + bar() + baz();
}
在 gcc -c foo.c
之后,objdump -d foo.o
在 x86_64 Linux 上的输出是:
foo.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <foo>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: b8 2a 00 00 00 mov [=11=]x2a,%eax
9: 5d pop %rbp
a: c3 retq
000000000000000b <bar>:
b: 55 push %rbp
c: 48 89 e5 mov %rsp,%rbp
f: b8 00 00 00 00 mov [=11=]x0,%eax
14: e8 e7 ff ff ff callq 0 <foo>
19: 83 c0 01 add [=11=]x1,%eax
1c: 5d pop %rbp
1d: c3 retq
000000000000001e <main>:
1e: 55 push %rbp
1f: 48 89 e5 mov %rsp,%rbp
22: 53 push %rbx
23: 48 83 ec 08 sub [=11=]x8,%rsp
27: b8 00 00 00 00 mov [=11=]x0,%eax
2c: e8 cf ff ff ff callq 0 <foo>
31: 89 c3 mov %eax,%ebx
33: b8 00 00 00 00 mov [=11=]x0,%eax
38: e8 ce ff ff ff callq b <bar>
3d: 01 c3 add %eax,%ebx
3f: b8 00 00 00 00 mov [=11=]x0,%eax
44: e8 00 00 00 00 callq 49 <main+0x2b>
49: 01 d8 add %ebx,%eax
4b: 48 83 c4 08 add [=11=]x8,%rsp
4f: 5b pop %rbx
50: 5d pop %rbp
51: c3 retq
这里有几点需要注意:
- 注意
bar
如何在地址0
调用foo
?objdump
如何知道正在调用的是foo
? 它真的可以在地址 0 吗? (大多数现代系统将虚拟内存的零页映射到PROT_NONE
,因此那里不会发生读取或写入访问。) - 请注意从
main
调用baz
与调用foo
和bar
有何不同?编译器知道foo
和bar
相对于调用指令本身的位置,但不知道baz
的位置。
那么,鉴于以上信息,linker 如何才能将其变成合理的东西?不能:这里没有足够的信息。
为了让 linker 能够 link 引用 baz
(我们还没有看到)调用 baz
,它需要额外的信息。在 ELF 系统上,附加信息被写入此处的特殊部分 .rela.text
,其中包含:
$ readelf -Wr foo.o
Relocation section '.rela.text' at offset 0x5d0 contains 1 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000000045 0000000b00000002 R_X86_64_PC32 0000000000000000 baz - 4
那个是书中所说的“参考”,但没有定义。它告诉 linker:如果你能找到 baz
的定义(在其他一些对象中),获取它的地址,然后把它(实际上,&baz - 4
因为 CALL
指令是相对于 next 指令之后的 CALL
) into bytes [45-48] of .text
section of foo.o
.
如果没有这样的定义呢? linker 会产生一个错误:
$ gcc foo.o
foo.o: In function `main':
foo.c:(.text+0x45): undefined reference to `baz'
collect2: error: ld returned 1 exit status
最后,回到上面的第 1 点:foo
真的可以在地址 0 吗?
不,但是地址 0x14
处的 CALL
指令实际上并没有说 CALL 0
。它说“在调用后下一条指令的地址处调用例程,负 25”。如果最终二进制文件中的调用指令在地址 0x400501
结束,那么该调用的目标将是 0x4004ed
,这就是 foo
结束的位置([=17 之间的距离=] 并且当 linker 将 foo.o
的 .text
部分重新定位到不同的地址时 CALL
不会改变(尽管 linker 放宽了;但那是复杂的话题改天)。
Employed Russian 的回答很好,但也有一个简短的回答:符号引用是您使用变量(或函数名称)的任何时候。符号定义创建一个变量(或函数名)。
因此符号定义为 int bar;
(只要它是全局的)或 int foo() { ... }
。符号引用将是 foo(bar)
(两个引用:foo
和 bar
)。