静态链接 (static linking) 是指将多个目标文件链接成一个可执行文件的过程:

1
2
3
4
gcc -c a.c b.c
gcc -o ab a.o b.o
# or
ld -e main -o ab a.o b.o

由于 ld 没有添加 __start、链接标准库等处理,这里需要指定入口。这里也不能使用 printf 等库函数。collect2 包装了 ld 链接器,进行了一些额外处理,是 gcc/g++ 实际使用的工具。

可重定位文件/目标文件

讲解链接过程之前,先介绍一下目标文件 (object file) 或者说可重定位文件 (relocatable file)。考虑如下代码的反汇编:

1
2
3
4
5
6
7
8
extern int var;

int myabs(int x);

int main() {
    int a = myabs(var);
    return 0;
}

objdump -drwC 反汇编结果:

1
2
3
4
5
6
7
8
9
10
11
12
0000000000000000 <main>:
   0:   f3 0f 1e fa             endbr64 
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   48 83 ec 10             sub    $0x10,%rsp
   c:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 12 <main+0x12>   e: R_X86_64_PC32        var-0x4
  12:   89 c7                   mov    %eax,%edi
  14:   e8 00 00 00 00          call   19 <main+0x19>   15: R_X86_64_PLT32      myabs-0x4
  19:   89 45 fc                mov    %eax,-0x4(%rbp)
  1c:   b8 00 00 00 00          mov    $0x0,%eax
  21:   c9                      leave  
  22:   c3                      ret

有几个特点:

  • 目标文件的代码段地址是从 0 开始的。使用 objdump -h 可以发现,不仅是代码段,所有段的虚拟内存地址都是从 0 开始的
  • 使用变量 var 和函数 myabs 的地方,全部用 0 替代。

链接时重定位

静态链接采用相似段合并,即将多个目标文件中的 .text 段合并为一个大的 .text。使用这种方法的链接器一般都采用两步链接(Two-pass Linking)方法:

  1. Pass 1: 读取 section 大小,计算最终的内存布局。同时,读取所有符号 symbol,在内存中创建完整的符号表(全局符号表,global symbol table)。
  2. Pass 2: 读取 section 和重定位信息,进行符号解析,调整代码中的地址,将新文件写出。

其中,第二步就是链接时重定位 (link time relocation) 每个需要重定位的段都有一个对应的重定位表 (relocation table),例如,这里的 .text 段对应的重定位表是 .rela.text。我们可以通过 objdump -r 查看重定位表:

1
2
3
4
5
6
7
$ objdump -r a.o
test2.o:     文件格式 elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE 
000000000000000e R_X86_64_PC32     var-0x0000000000000004
0000000000000015 R_X86_64_PLT32    myabs-0x0000000000000004

其中,OFFSET 代表重定位入口 (relocation entry) 在所属段内的偏移,这里可以和前面的反汇编相印证。TYPE 代表重定位类型,将决定重定位的具体方式。

链接时,链接器每遇到一个重定位入口,首先要在全局符号表中查找符号,找到后,一般会利用如下信息,修改重定位入口的指令:

  1. 入口原指令中的值。比如 mov 0x0(%rip),%eax 中的 00 00 00
  2. 链接后,入口的虚拟地址。它等于段的虚拟地址 + 入口在段内的偏移。(入口地址和入口指令地址有略微的差值)
  3. 链接后,符号定义的虚拟地址。

还要注意一点的的是,在 gcc 10.1 之后,除非显式定义了弱符号,否则任何定义在链接时都不能有两份。

链接顺序

在链接内命令中,源文件、目标文件、静态库文件出现的顺序也有影响。以下是我自己的一些总结,可能有误:

  • 源文件先被编译为目标文件,所以这里和其他目标文件一起考虑。
  • 链接顺序是从左到右,无论是目标文件还是库文件。具体规则是:
    1. 普通目标文件直接链接:如果它提供了需要重定位的符号的定义,则进行重定位。
    2. 静态库内目标文件按需链接:在静态库搜索当前未重定位的符号,搜索顺序为打包顺序。搜索到定义之后,将库内定义所在的目标文件链接,并进行重定位。
  • 每链接一个目标文件时,如果遇到了需要重定位的符号,分两种情况考虑:
    1. 如果是普通目标文件,则先在已链接内容中查找符号,找到则进行重定位。
    2. 如果是静态库内的目标文件,则什么也不做。

可以发现,按照如上规则,假如静态库 A 依赖于静态库 B,那么链接时,应该将 A 放在 B 的前面。否则,A 中需要重定位的符号将无法被重定位。有一种特殊情况是,静态库 A 和 B 相互有依赖,此时可以用 -Wl,--start-group -lA -lB -Wl,--end-group 选项来解决。

设计规范

我们注意到在静态库中,一个目标文件通常只包含一个函数,比如 printf.o 只包含 printf 函数。这样做的好处是,链接 printf.o 时不会将其他不需要的函数也链接进来。它还能避免潜在的多重定义问题。

装载时重定位

如果我们希望可执行文件可以被装载到虚拟地址空间中的任意地址,就需要装载时重定位技术,相关内容将在动态链接一文中讲解。

Comments