目标文件中的符号引用到底是什么?

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

这里有几点需要注意:

  1. 注意 bar 如何在地址 0 调用 fooobjdump 如何知道正在调用的是 foo? 它真的可以在地址 0 吗? (大多数现代系统将虚拟内存的零页映射到 PROT_NONE,因此那里不会发生读取或写入访问。)
  2. 请注意从 main 调用 baz 与调用 foobar 有何不同?编译器知道 foobar 相对于调用指令本身的位置,但不知道 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)(两个引用:foobar)。