X

曜彤.手记

随记,关于互联网技术、产品与创业

吉 ICP 备10004938号

ELF 静态 / 动态链接流程解析


本文将以两个具体的例子来剖析 ELF 目标文件(.o)和动态库文件(.so)在分别进行静态链接和动态链接时的具体流程。

操作系统版本:

Distributor ID: Ubuntu
Description: Ubuntu 20.04 LTS
Release: 20.04
Codename: focal

Clang 版本:

clang version 10.0.0-4ubuntu1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/9
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/9
Selected GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/9
Candidate multilib: .;@m64
Selected multilib: .;@m64

静态链接

我们将用以下两个目标文件的 C++ 代码来演示静态链接的过程:

// main.cc
extern int shared;  // 全局符号;
extern void swap(int* x, int* y);  // 全局符号;
int main(int argc, char** argv) {
  int v = 100;
  swap(&v, &shared);
}

// module.cc
int shared = 1;
void swap(int* x, int* y) {
  *x ^= *y ^= *x ^= *y;
}

编译成对应的目标文件:

clang++ -c main.cc -o main.o
clang++ -c module.cc -o module.o
clang++ main.o module.o -o main -v  # 链接合并为可执行文件,并输出编译过程;

输出:

clang version 10.0.0-4ubuntu1 
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/9
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/9
Selected GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/9
Candidate multilib: .;@m64
Selected multilib: .;@m64
 "/usr/bin/ld" -z relro --hash-style=gnu --build-id --eh-frame-hdr -m elf_x86_64 -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o a /usr/bin/../lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crt1.o /usr/bin/../lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crti.o /usr/bin/../lib/gcc/x86_64-linux-gnu/9/crtbegin.o -L/usr/bin/../lib/gcc/x86_64-linux-gnu/9 -L/usr/bin/../lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu -L/usr/bin/../lib/gcc/x86_64-linux-gnu/9/../../../../lib64 -L/lib/x86_64-linux-gnu -L/lib/../lib64 -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib64 -L/usr/lib/x86_64-linux-gnu/../../lib64 -L/usr/bin/../lib/gcc/x86_64-linux-gnu/9/../../.. -L/usr/lib/llvm-10/bin/../lib -L/lib -L/usr/lib a.o b.o -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/bin/../lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/bin/../lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crtn.o

可以看到,Clang 在调用链接器 ld 时实际上需要做的工作远比我们想象的要多。其中主要的包括:使用 -dynamic-linker 参数指定动态连接器的位置;使用 -L 参数指定 ld 需要搜索静态库的位置;以及通过 -l 指定需要链接的 namespec 库名称,如:libc、libgcc 以及 libgcc_s 等等。其中一部分链接的内容会为可执行文件提供 CRT 环境,而这里为了仅为了掩饰 ld 的静态链接过程,因此仅适用 ld 将上述两个目标文件进行合并(这里生成的“可执行文件”无法运行,因为其内部还有没有被解析的符号),命令如下:

ld main.o module.o -e main -o main  # ld 默认入口函数为 _start;
  1. 相似段合并

第一步,链接器会将两个目标文件内的各个同名 Section 进行合并。首先来看下合并后生成的“可执行文件”其段结构:

main:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000006c  0000000000401000  0000000000401000  00001000  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .eh_frame     00000058  0000000000402000  0000000000402000  00002000  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .data         00000004  0000000000404000  0000000000404000  00003000  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  3 .comment      0000001f  0000000000000000  0000000000000000  00003004  2**0
                  CONTENTS, READONLY

可以看到,这里的 VMA 已经有了具体值。在进行链接之前,目标文件中的所有段的 VMA 都是 0,因为此时虚拟空间还没有被分配。在 Linux 下,32 位 ELF 可执行文件默认从地址 0x08048000 开始分配;64 位 ELF 可执行文件默认从地址 0x400000 开始分配。

  1. 符号地址的确定

此时经过第一步的扫描和空间分配阶段后,“可执行文件”中的各个段在连接后的虚拟地址就已经确定了。接下来,链接器开始计算各个符号的虚拟地址。链接器需要给各个段中的符号加上一个偏移量,使它们调整到正确的虚拟地址。

Symbol table '.symtab' contains 13 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000401000     0 SECTION LOCAL  DEFAULT    1 
     2: 0000000000402000     0 SECTION LOCAL  DEFAULT    2 
     3: 0000000000404000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS a.cc
     6: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS b.cc
     7: 0000000000401040    44 FUNC    GLOBAL DEFAULT    1 _Z4swapPiS_
     8: 0000000000404000     4 OBJECT  GLOBAL DEFAULT    3 shared
     9: 0000000000404004     0 NOTYPE  GLOBAL DEFAULT    3 __bss_start
    10: 0000000000401000    49 FUNC    GLOBAL DEFAULT    1 main
    11: 0000000000404004     0 NOTYPE  GLOBAL DEFAULT    3 _edata
    12: 0000000000404008     0 NOTYPE  GLOBAL DEFAULT    3 _end
  1. 符号解析与重定位

链接器通过重定位表(Relocation Table)来更改诸如 .code/.text 中所引用符号的虚拟地址。我们可以通过 readelf -r main.o 命令来查看位于该目标文件内的重定位表,如下所示。

main.o:     file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE 
000000000000001c R_X86_64_64       shared
0000000000000025 R_X86_64_PLT32    _Z4swapPiS_-0x0000000000000004


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET           TYPE              VALUE 
0000000000000020 R_X86_64_PC32     .text

可以看到,符号 shared 和 swap 在 .text 段中的引用地址偏移(OFFSET)。 在重定位过程中,每个重定位入口都是对一个符号的引用,那么当链接器需要对某个符号的引用进行重定位时,它就会去查找由所有输入目标文件的符号表组成的全局符号表,找到相应的符号后进行重定位。

在重定位的过程中,链接器需要修改符号的实际引用地址,但不同处理器指令对于地址的格式和使用方式都不相同。比如,常见的几种重定位类型(TYPE,即 r_info 字段的高 8 位)如下所示:

其中,A 表示保存在被修正位置的值;P 表示被修正的位置(相对于段开始的偏移或虚拟地址,可通过 r_offset 计算得到);S 表示符号的实际地址(可通过 r_info 的低 24 位计算得出对应在符号表中的下标,则符号的值为实际虚拟地址);L 表示 PLT 中该符号的入口(相对于段开始的偏移或虚拟地址);综上,在静态链接中,链接器需要配合全局符号表与重定向表来完成符号重定向的过程。

以上述符号 shared 为例,我们来看下符号重定向的结果:首先重定向表中第一行表示对 main.o 目标文件中的 .text 段中的 shared 符号进行 R_X86_64_64 类型的引用地址重定向。通过查看该目标文件的重定位表,我们可以发现符号 shared 在 .text 的重定向偏移位置为 0x1c,对照着反编译后的汇编代码,我们可以看到当前位置的符号值,即 A 为 “00 00 00 00 00 00 00 00”,如下所示:

...
   b:    48 89 75 f0              mov    %rsi,-0x10(%rbp)
   f:    c7 45 ec 64 00 00 00     movl   $0x64,-0x14(%rbp)
  16:    48 8d 7d ec              lea    -0x14(%rbp),%rdi
  1a:    48 be 00 00 00 00 00     movabs $0x0,%rsi
  21:    00 00 00 
...

R_X86_64_64 类型重定向的计算规则为:S + A,即符号的新地址值加上其原来被修正位置的值。而通过查看“可执行文件”内的符号表可以发现,符号 shared 的虚拟地址为 “0x404000”,如下所示为 nm main 命令的结果。

0000000000404004 D __bss_start
0000000000404004 D _edata
0000000000404008 D _end
0000000000401000 T main
0000000000404000 D shared
0000000000401040 T _Z4swapPiS_

此时再反编译该“可执行文件”,并查看 .text 段对 shared 符号引用的同一位置。可以发现,该位置的引用值已被正确的替换(0 + 0x404000 = 0x404000)。

...
  401008:    89 7d fc                 mov    %edi,-0x4(%rbp)
  40100b:    48 89 75 f0              mov    %rsi,-0x10(%rbp)
  40100f:    c7 45 ec 64 00 00 00     movl   $0x64,-0x14(%rbp)
  401016:    48 8d 7d ec              lea    -0x14(%rbp),%rdi
  40101a:    48 be 00 40 40 00 00     movabs $0x404000,%rsi
  401021:    00 00 00 
...

动态链接

(懒得加了,请参考“链接、装载与库相关记录(二)”一文)



这是文章底线,下面是评论
  暂无评论,欢迎勾搭 :)