ELF重定位

链接重定位

在通过编译和汇编后,就生成了目标文件,链接就是把这些目标文件加工后合并成一个输出文件的过程。

链接过程可以分为两步:

  • 第一步 空间与地址分配。扫描所有的输入目标文件,获得它们每个各个节的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步中,链接器将能够获得所有输入目标文件的节长度,并且将它们合并(相同的节互相合并,如.text和.text合并、.data和.data合并),计算出输出文件中各个段合并后的长度和位置,并建立映射关系。通过这一步,输入文件中的各个节在链接后的地址就确定了。分配并计算出可执行文件中各个节的地址后,链接器开始计算各个符号的地址。因为各个符号在节内的相对位置是固定的,所以这时候各个符号的地址已经是确定的了。举个例子,比如“main.o”中的“add”函数相对于“main.o”的“.text”段得偏移是0x33,在经过合并之后,往后移动到0x115c, 这时候需要更新符号表的add项(详细见下面的例子)
  • 第二步符号解析和重定位。使用上面第一步中收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析和重定位、调整代码中的地址等。事实上第二步是链接过程的核心,特别是重定位过程。通过objdump -d命令对目标文件进行反汇编后(能看到汇编代码),可以看到对应指令中外部引用的地址部分都是暂时用临时的假地址来代替,真正的地址计算工作由符号解析和重定位这一步来进行,也是符号解析和重定位的主要工作。通过前面的空间和地址分配可以得知,链接器在完成地址和空间分配之后就已经可以确定所有符号的虚拟地址了,那么链接器就可以根据符号的地址对每个需要重定位的指令进行地址修正。

指令修正

typedef struct {
    Elf64_Addr       r_offset;
    Elf64_Word       r_info;
} Elf64_Rel;

typedef struct {
    Elf64_Addr    r_offset;
    Elf64_Word    r_info;
    Elf64_Sword   r_addend;
} Elf64_Rela;

上面是重定义节区的数据结构。

  • r_offset
    • 需要重定位的位置。对于重定位文件,此值是从需要重定位的符号所在节区头部开始到将被重定位的位置之间的字节偏移。对于可执行文件或者共享目标文件而言,其取值是需要重定位的虚拟地址,一般而言,也就是说我们所说的 GOT 表的地址。
  • r_info
    • 此成员给出需要重定位的符号的符号表索引,以及相应的重定位类型。 例如一个调用指令的重定位项将包含被调用函数的符号表索引。如果索引是 STN_UNDEF,那么重定位使用 0 作为“符号值”。此外,重定位类型是和处理器相关的。
  • r_addend
    • 此成员给出一个常量补齐,用来计算将被填充到可重定位字段的数值。也就是我们俗称的加数,只有在rela节里会有

重定位类型表

名称 大小 计算公式 说明
R_X86_64_NONE 0 none none  
R_X86_64_64 1 word64 S + A  
R_X86_64_PC32 2 word32 S + A - P  
R_X86_64_GOT32 3 word32 G + A  
R_X86_64_PLT32 4 word32 L + A - P  
R_X86_64_COPY 5 none none  
R_X86_64_GLOB_DAT 6 word64 S  
R_X86_64_JUMP_SLOT 7 word64 S  
R_X86_64_RELATIVE 8 word64 B + A  
R_X86_64_GOTPCREL 9 word32 G + GOT + A - P  
R_X86_64_32S 11 word32 S + A  
R_X86_64_16 12 word16 S + A  
R_X86_64_PC16 13 word16 S + A - P  
R_X86_64_8 14 word8 S + A  
R_X86_64_PC8 15 word8 S + A - P  
R_X86_64_DPTMOD64 16 word64    
R_X86_64_DTPOFF64 17 word64    
R_X86_64_TPOFF64 18 word64    
R_X86_64_TLSGD 19 word32    
R_X86_64_TLSLD 20 word32    
R_X86_64_DTPOFF32 21 word32    
R_X86_64_GOTTPOFF 22 word32    
R_X86_64_TPOFF32 23 word32    
  • A(addend)
    • The addend used to compute the value of the relocatable field.
    • 加数,在ELF中会显示指定
  • B(base)
    • 共享文件运行期的基地址。通常基地址在编译的时候是0, 但是运行的时候运行的时候会加载到一个offset之后
  • G(Global)
    • 在运行时重定位项的符号在全局偏移表中的偏移。
  • GOT (global offset table)
    • GOT中的地址
  • L (linkage)
    • 过程链接表项中一个符号的节区偏移或者地址。过程链接表项会把函数调用重定位到正确的目标位置。链接编辑器会构造初始的过程链接表,然后动态链接器在执行过程中会修改这些项目
  • P (place)
    • 表示被修正(用 r_offset 计算)的存储单元的位置(节区偏移或者地址)。
  • S (symbol)
    • 符号的实际地址。

我们这里讲编译中的链接过程的重定位,一般是R_X86_64_PC32R_X86_64_PLT32类型

例子

下面我们以具体的例子举例并进一步的说明

/***************main.c***************/
int add(int first, int second);
int main() {
    int a,b;
    a = 3;
    b = 4;
    int ret = add(a,b);
    return 0;
}

int add(int first, int second) {
    return first+second;
}

我们首先编译一下main.c

✗ gcc -c main.c -o main.o

然后用objdump看一下代码段内容

➜  relocate git:(master) ✗ objdobjdump -d main.o

main.o:     file format elf64-x86-64


Disassembly of section .text:

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:	c7 45 f4 03 00 00 00 	movl   $0x3,-0xc(%rbp)
  13:	c7 45 f8 04 00 00 00 	movl   $0x4,-0x8(%rbp)
  1a:	8b 55 f8             	mov    -0x8(%rbp),%edx
  1d:	8b 45 f4             	mov    -0xc(%rbp),%eax
  20:	89 d6                	mov    %edx,%esi
  22:	89 c7                	mov    %eax,%edi
  24:	e8 00 00 00 00       	callq  29 <main+0x29>
  29:	89 45 fc             	mov    %eax,-0x4(%rbp)
  2c:	b8 00 00 00 00       	mov    $0x0,%eax
  31:	c9                   	leaveq
  32:	c3                   	retq

0000000000000033 <add>:
  33:	f3 0f 1e fa          	endbr64
  37:	55                   	push   %rbp
  38:	48 89 e5             	mov    %rsp,%rbp
  3b:	89 7d fc             	mov    %edi,-0x4(%rbp)
  3e:	89 75 f8             	mov    %esi,-0x8(%rbp)
  41:	8b 55 fc             	mov    -0x4(%rbp),%edx
  44:	8b 45 f8             	mov    -0x8(%rbp),%eax
  47:	01 d0                	add    %edx,%eax
  49:	5d                   	pop    %rbp
  4a:	c3                   	retq

我们可以看到,在 24: e8 00 00 00 00 callq 29 <main+0x29>代码段,这里是调用add函数的指令,函数地址是00 00 00 00。

我们看一下main.o的重定位表

➜  relocate git:(master) ✗ readelf -r main.o

Relocation section '.rela.text' at offset 0x250 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000025  000a00000004 R_X86_64_PLT32    0000000000000033 add - 4

...

根据上文提到的,add符号的重定位类型是R_X86_64_PLT32,计算公式是L + A - P

由于编译器在处理该外部函数符号的时候是不知道这个符号是定义在普通对象还是共享对象里的,所以在rela表中统一将其定义为R_X86_64_PLT32类型,在随后的链接过程会根据函数符号是定义在普通对象还是共享对象进行不同的处理。

  • 该函数符号定义在普通对象中
- LD将其当做普通的R_X86_64_PC32类型进行处理,这时L+A-P = S+A-P
  • 该函数符号定义在共享对象中
- LD将其作为R_X86_64_PLT32进行处理,LD会为其create一个“函数名@plt”过程和在.got.plt表中创建一个表项(用于存储函数被加载后的实际虚拟地址),并将代码中对该函数的访问改为对该过程的访问,这些操作都要在静态链接的时候完成的,这个过程(函数名@plt)的地址就是L,所以relocate计算公式变为:L+A-P。

最后动态链接的时候会将函数的实际虚拟地址更新到.got.plt表项中,这样该过程通过.got.plt表项就可以间接跳转到实际要访问的函数了。

从符号表中知道函数符号定义在当前文件中,所以,L+A-P = S+A-P。我们再回忆一下, S表示符号的实际地址,这个在第一步空间的重新分配中就可以得到,.text段合并重新分配后,并将分配的值写入符号表,我们假设这个值是0x115c,P表示被修正(用 r_offset 计算)的存储单元的位置, (空间从新分配后,新指令位置写入新的重定位表中, 所以我们能够获取到修正后的指令位置), 我们假设是0x114e, 这里的加数是-4, 所以,我们计算

S+A-P = 0x115C - 0x114E + (-4) = 0xA

这里计算的其实就是调用者地址和函数(变量)地址的差值,在加上加数

我们看一下最后链接的结果(上述的假设数值都是我们从实际链接后的代码段中拿到的,所以很一致)

...

0000000000001129 <main>:
    1129:	f3 0f 1e fa          	endbr64
    112d:	55                   	push   %rbp
    112e:	48 89 e5             	mov    %rsp,%rbp
    1131:	48 83 ec 10          	sub    $0x10,%rsp
    1135:	c7 45 f4 03 00 00 00 	movl   $0x3,-0xc(%rbp)
    113c:	c7 45 f8 04 00 00 00 	movl   $0x4,-0x8(%rbp)
    1143:	8b 55 f8             	mov    -0x8(%rbp),%edx
    1146:	8b 45 f4             	mov    -0xc(%rbp),%eax
    1149:	89 d6                	mov    %edx,%esi
    114b:	89 c7                	mov    %eax,%edi
    114d:	e8 0a 00 00 00       	callq  115c <add>
    1152:	89 45 fc             	mov    %eax,-0x4(%rbp)
    1155:	b8 00 00 00 00       	mov    $0x0,%eax
    115a:	c9                   	leaveq
    115b:	c3                   	retq

000000000000115c <add>:
    115c:	f3 0f 1e fa          	endbr64
    1160:	55                   	push   %rbp
    1161:	48 89 e5             	mov    %rsp,%rbp
    1164:	89 7d fc             	mov    %edi,-0x4(%rbp)
    1167:	89 75 f8             	mov    %esi,-0x8(%rbp)
    116a:	8b 55 fc             	mov    -0x4(%rbp),%edx
    116d:	8b 45 f8             	mov    -0x8(%rbp),%eax
    1170:	01 d0                	add    %edx,%eax
    1172:	5d                   	pop    %rbp
    1173:	c3                   	retq
    1174:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
    117b:	00 00 00
    117e:	66 90                	xchg   %ax,%ax

...

这时候相对应的函数调用地址已经填上的正确的数值114d: e8 0a 00 00 00 callq 115c <add>,也就是我们上述计算出来的值(使用小端序),右边callq 115c <add>是工具给的解释,方便我们查看调试。

callq指令后面加上了跳转的偏移值,这是callq的相对寻址方式。

什么是加数?(我猜测的)相对寻址指向目标指令的上一条指令,所以需要加上一个偏移值,现在主流的cpu架构的指令长度都是4,所以,这里的加数就是-4 —— 指向上一条指令

这里的值采用补码表示,因为原码的补码等于自身,所以这里看起来像是原码;但是一旦add函数合并的时候,跑到main函数前面了,这里的偏移值就是一个负数,必须采用补码表示

我们这里的例子是一个文件的情况,对于多个重定位文件的链接,链接过程也是一样的。

ELF文件结构解析

可执行与可链接格式 (英语:Executable and Linkable Format,缩写为ELF),常被称为ELF格式,在计算机科学中,是一种用于可执行文件、目标文件、共享库和核心转储(core dump)的标准文件格式。

1999年,被86open项目选为x86架构上的类Unix操作系统的二进制文件格式标准,用来取代COFF。因其可扩展性与灵活性,也可应用在其它处理器、计算机系统架构的操作系统上

—— 来自维基百科

  • 可执行文件(Executable File),就是我们通常在 Linux 中执行的程序。
  • 目标文件(Object code)一般值的是可重定位文件(Relocatable File),包含由编译器生成的代码以及数据。链接器会将它与其它目标文件链接起来从而创建可执行文件或者共享目标文件。在 Linux 系统中,这种文件的后缀一般为 .o
  • 共享库(Shared Libraries),包含代码和数据,这种文件是我们所称的库文件,一般以 .so 结尾。一般情况下,它有以下两种使用情景:
    • 链接器(Link eDitor, ld)可能会处理它和其它可重定位文件以及共享目标文件,生成另外一个目标文件(编译里链接概念)
    • 动态链接器(Dynamic Linker)将它与可执行文件以及其它共享目标组合在一起生成进程镜像(运行中的动态链接概念, 我们最后一节讲的内容)
  • 核心转储(core dump)是操作系统在进程收到某些信号而终止运行时,将此时进程地址空间的内容以及有关进程状态的其他信息写出的一个磁盘文件。这种信息往往用于调试,在这里我们不涉及

在下文的讲解中,我们会以下面的代码的编译和运行在的docker系统是ubuntu:21.04, docker file在这里 代码在这里

结构

上面是ELF文件的结构图,我们可以分为四个部分

  • ELF header, ELF头部表,关于ELF的概述部分
  • Program header table, 程序头表,一般简写成PHT
  • Section header table, 节区头表,一般简写成SHT
  • 中间的内容区域,下文会重点介绍

从结构图我们可以看到中间的内容区域被PHT和SHT同时引用到,为什么会有这样的情况呢?因为ELF文件有两种用途,用于代码编译过程中的链接和用于程序运行中的运行。站在不同的用途,ELF文件被使用到的结构各有不同,一般我们称为链接视图执行视图

链接视图中的程序头表是可以忽略的,相应的在执行视图中节头表是可以忽略的。

ELF头部表

我们先编译一下main.c

/************main.c****************/
#include <stdio.h>

int main() {
    printf("hello world!\n");
    return 0;
}
➜  ELFLearning git:(master) gcc -c main.c -o main.o

然后用readelf工具读取头部信息

readelf工具可以读取ELF文件中的各个区块的信息,具体可以用readelf -h查看各个参数的用法 同时,在这里我们也会使用objdump工具来查看ELF文件的二进制内容,具体查看objdump -H

➜  ELFLearning git:(master) ✗ rea readelf -h main.o
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          784 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         14
  Section header string table index: 13

在文件起始处,有4个标识字节。 在ASCII代码0x7f字符之后, 接下来是字符E(0x45) 、 L(0x4c) 、 F(0× 46) 的ASCII码值。这使得所有处理ELF的工具都可以识别文件是否是所要的格式。还有一些与 具体体系结构相关的信息,在本例中,ubuntu的64位系统。类别标识( ELF64)正确 地表明这是一台64位机器(现在很少能找到32位的机器了) 文件类型是REL,意味着文件是个可重定位文件。 Version字段用于区分ELF标准的各个修订版本。但 因为版本1仍然是最新的,目前还不需要这个特性。另外还包括ELF文件的各个部分的长度和索引位置 信息(稍后会更详细地讨论)。详细的格式定义可以参考这篇“ELF Header”

节头

上文提到了ELF的结构有两种视图,这里我们先站在编译的角度,从链接视图分析。 链接视图最重要的概念是节(section), 节头表(section header table)详细的给出了各个节的信息。 下面我们用readelf再分析一下main.o的节头表

➜  ELFLearning git:(master) ✗ readelf -S main.o
There are 14 section headers, starting at offset 0x310:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       000000000000001b  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000250
       0000000000000030  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  0000005b
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  0000005b
       0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .rodata           PROGBITS         0000000000000000  0000005b
       000000000000000d  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  00000068
       000000000000002b  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  00000093
       0000000000000000  0000000000000000           0     0     1
  [ 8] .note.gnu.propert NOTE             0000000000000000  00000098
       0000000000000020  0000000000000000   A       0     0     8
  [ 9] .eh_frame         PROGBITS         0000000000000000  000000b8
       0000000000000038  0000000000000000   A       0     0     8
  [10] .rela.eh_frame    RELA             0000000000000000  00000280
       0000000000000018  0000000000000018   I      11     9     8
  [11] .symtab           SYMTAB           0000000000000000  000000f0
       0000000000000138  0000000000000018          12    10     8
  [12] .strtab           STRTAB           0000000000000000  00000228
       0000000000000028  0000000000000000           0     0     1
  [13] .shstrtab         STRTAB           0000000000000000  00000298
       0000000000000074  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

数据结构

下面是section header table在linux源码中数据结构的定义

 typedef struct {
   Elf32_Word    sh_name;
   Elf32_Word    sh_type;
   Elf32_Word    sh_flags;
   Elf32_Addr    sh_addr;
   Elf32_Off     sh_offset;
   Elf32_Word    sh_size;
   Elf32_Word    sh_link;
   Elf32_Word    sh_info;
   Elf32_Word    sh_addralign;
   Elf32_Word    sh_entsize;
 } Elf32_Shdr;

 typedef struct elf64_shdr {
   Elf64_Word sh_name;           /* Section name, index in string tbl */
   Elf64_Word sh_type;           /* Type of section */
   Elf64_Xword sh_flags;         /* Miscellaneous section attributes */
   Elf64_Addr sh_addr;           /* Section virtual addr at execution */
   Elf64_Off sh_offset;          /* Section file offset */
   Elf64_Xword sh_size;          /* Size of section in bytes */
   Elf64_Word sh_link;           /* Index of another section */
   Elf64_Word sh_info;           /* Additional section information */
   Elf64_Xword sh_addralign;     /* Section alignment */
   Elf64_Xword sh_entsize;       /* Entry size if section holds table */
 } Elf64_Shdr;

name(4字节)

4个字节,这里的name字段记录了当前节点的名字的字符串在字符串表(有一个字符串表节点,专门存储ELF文件中用到的字符串)中的索引值

type(4字节)

4个字节,节点类型,字段值和解释如下

| 类型值 | 名字 | 解释 | |—— | — | - | |0x0| SHT_NULL| 节表头入口,没有类型| |0x1| SHT_PROGBITS|程序数据, 该类型节区包含程序定义的信息,它的格式和含义都由程序来决定。| |0x2| SHT_SYMTAB|该类型节区包含一个符号表(Symbol Table)。目前目标文件对每种类型的节区都只能包含一个,不过这个限制将来可能发生变化。 一般,SHT_SYMTAB 节区提供用于链接编辑(指 ld 而言) 的符号,尽管也可用来实现动态链接。 | |0x3| SHT_STRTAB|该类型节区包含字符串表( STRing TABle )。| |0x4| SHT_RELA| 该类型节区包含显式加数(后续重定位会讲到)的重定位项( RELocation entry with Addends ),例如,64位目标文件中的 Elf64_Rela 类型。此外,目标文件可能拥有多个重定位节区。 | |0x5| SHT_HASH| 符号哈希表,用于快速查找符号| |0x6| SHT_DYNAMIC| 动态链接信息| |0x7| SHT_NOTE| 该类型节区包含以某种方式标记文件的信息| |0x8| SHT_NOBITS| 该类型节区不占用文件的空间,其它方面和 SHT_PROGBITS 相似。尽管该类型节区不包含任何字节,其对应的节头成员 sh_offset 中还是会包含概念性的文件偏移。一般是.bss节使用| |0x9| SHT_REL| 该类型节区包含重定位表项(RELocation entry without Addends),不过并没有加数(后续重定位会讲到)。例如,64 位目标文件中的 Elf64_rel 类型。目标文件中可以拥有多个重定位节区| |0xA| SHT_SHLIB| 保留| |0xB| SHT_DYNSYM| 作为一个完整的符号表,它可能包含很多对动态链接而言不必 要的符号。因此,目标文件也可以包含一个 SHT_DYNSYM 节区,其中保存动态链接符号的一个最小集合,以节省空间。| |0xE| SHT_INIT_ARRAY| 构造器列表, ELF可以定义自己的构造器,在加载/动态链接的时候,加载器会优先执行| |0xF| SHT_FINI_ARRAY| 解构器列表| |0x10| SHT_PREINIT_ARRAY| Array of pre-constructors| |0x11| SHT_GROUP|Section group | |0x12| SHT_SYMTAB_SHNDX| Extended section indices| |0x13| SHT_NUM| Number of defined types.| |0x60000000| SHT_LOOS| Start OS-specific.| |…|…|…| ####flag(4字节/8字节) 用于标记节点的权限,比如修改,可执行,如下

类型值 名字 解释
0x1 SHF_WRITE 这种节包含了进程运行过程中可以被写的数据。
0x2 SHF_ALLOC 这种节在进程运行时占用内存。对于不占用目标文件的内存镜像空间的某些控制节,该属性处于关闭状态 (off)。
0x3 SHF_EXECINSTR 这种节包含可执行的机器指令(EXECutable INSTRuction)。
0x20 SHF_STRINGS 包含了以NULL(’\0’)结尾的字符串,一般用于定义字符串节
0x40 SHF_INFO_LINK ‘sh_info’ contains SHT index
0x40 SHF_TLS 保存了TLS(Thread-Local storage data, 线程局部存储数据)

address(4字节/8字节)

如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应该在进程镜像中的位置。否则,此字段为 0。

offset(4字节/8字节)

节区在文件中的偏移值

size(4字节/8字节)

节区在文件中的大小(单位byte),可以是0

link&info(4字节+4字节)

这个信息的解释依赖于节点的类型

addralign(4字节/8字节)

对齐大小,必须是2的整数倍

entsize

某些节区中存在具有固定大小的表项的表,如符号表。对于这类节区,该成员给出每个表项的字节大小。反之,此成员取值为 0。

节区

节区包含目标文件中除了 ELF 头部、程序头部表、节区头部表的所有信息。节区满足以下条件

  • 每个节区都有对应的节头来描述它。但是反过来,节区头部并不一定会对应着一个节区。
  • 每个节区在目标文件中是连续的,但是大小可能为 0。
  • 任意两个节区不能重叠,即一个字节不能同时存在于两个节区中。
  • 目标文件中可能会有闲置空间(inactive space),各种头和节不一定会覆盖到目标文件中的所有字节,闲置区域的内容未指定。

许多在 ELF 文件中的节都是预定义的,它们包含程序和控制信息。这些节被操作系统使用,但是对于不同的操作系统,同一节区可能会有不同的类型以及属性。

可执行文件是由链接器将一些单独的目标文件以及库文件链接起来而得到的。其中,链接器会解析引用(不同文件中的子例程的引用以及数据的引用,调整对象文件中的绝对引用)并且重定位指令。加载与链接过程需要目标文件中的信息,并且会将处理后的信息存储在一些特定的节区中,比如 .dynamic 。

每一种操作系统都会支持一组链接模型,但这些模型都大致可以分为两种

类型 描述
静态链接 静态链接的文件中所使用的库文件或者第三方库都被静态绑定了,其引用已经被解析了。
动态链接 动态链接的文件中所使用的库文件或者第三方库只是单纯地被链接到可执行文件中。当可执行文件执行时使用到相应函数时,相应的函数地址才会被解析。

有一些特殊的节可以支持调试,比如说 .debug 以及 .line 节;支持程序控制的节有 .bss,.data, .data1, .rodata, .rodata1。

note相关节区

有时候生产厂商或者系统构建者可能需要使用一些特殊的信息来标记ELF文件,从而其它程序可以来检查该ELF文件的一致性以及兼容性。节区类型为 SHT_NOTE 或者程序头部类型为 PT_NOTE 的元素用于来实现这个目的,它们中对象的表项可能包含一到多个,每一个表项都是目标处理器格式的 4 字节数组。下面给出了一些可能的注释信息。但是这并不在 ELF 文件的规范内。

  • namesz与name
    • name的前namesz字节包含了一个以NULL结尾的字符串,这表示该项的拥有者或者发起人。但是目前并没有避免冲突的格式化的命名机制。一般来说,生产厂商会使用他们自己公司的名字,例如“XYZ Computer Company”来当做对应的标志。如果没有任何名字的话,namesz应该是0。那么在name的地方应该填充到4字节对齐。
  • descsz与desc
    • desc的前descsz字节包含了注释的描述。ELF文件对于描述的内容没有任何约束。如果没有任何描述的话,descsz应该为0。当然,应该在desc处填充到4字节对齐。
  • type
    • 这个字段给出了描述的解释,对于不同那个的发起者来说,他们都必须控制自己的类型,对于同一类型来说,有可能有多个描述与其对应。因此,发起者必须能够识别名字以及类型以便于来理解对应的描述。目前来说,类型不能够为非负值,ELF文件的规范里并不定义描述符的意思。这也是为什么type在前面。

下面给出一个简单的例子来说明一下

这里包含了两个表项。

在 Linux 中,与 Note 相关的节包含了 ELF 文件中的一些注释信息,主要包含两个节

  • .note.ABI-tag
  • .note.gnu.build-id

.strtab

该节区描述默认的字符串表,包含了一系列的以 NULL 结尾的字符串。ELF 文件使用这些字符串来存储程序中的符号名,包括

  • 变量名
  • 函数名

该节在运行的过程中不需要加载,只需要加载对应的子集 .dynstr 节。

一般通过对字符串的首个字母在字符串表中的下标来索引字符串。

字符串表的首尾字节都是NULL。此外,索引为0的字符串要么没有名字,要么就是名字为空,其解释依赖于上下文。字符串表也可以为空,相应的,其节区头部的 sh_size 成员将为0。在空字符串表中索引大于 0 的下标显然是非法的。

一个节区头部的 sh_name 成员的值为其相应的节区头部字符串表节区的索引,此节区由 ELF 头的 e_shstrndx 成员给出。下图给出了一个包含 25 个字节的字符串表,以及与不同索引相关的字符串。

索引 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9
0 \0 n a m e . \0 V a r
10 i a b l e \0 a b l e
20 \0 \0 x x \0          

其中包含的字符串有

索引 字符串
0 none
1 name.
7 Variable
11 able
16 able
24 空字符串

可以看出

  • 字符串表索引可以引用节区中任意字节。
  • 字符串可以出现多次。
  • 可以存在对子字符串的引用。
  • 同一个字符串可以被引用多次。
  • 字符串表中也可以存在未引用的字符串。

这部分信息在进行 strip 后就会消失。

.dynstr

此节区包含用于动态链接的字符串,格式和.strtab类似

.symtab: 符号表

概述

每个目标文件都会有一个符号表,熟悉编译原理的就会知道,在编译程序时,必须有相应的结构来管理程序中的符号以便于对函数和变量进行重定位。

此外,链接本质就是把多个不同的目标文件相互“粘”在一起,实际上,目标文件相互粘合是目标文件之间对地址的引用,即函数和变量的地址的相互引用。而在粘合的过程中,符号就是其中的粘合剂。

目标文件中的符号表包含了一些通用的符号,这部分信息在进行了 strip 操作后就会消失。包括

  • 变量名
  • 函数名

符号表其实是一个数组,数组中的每一个元素都是一个结构体,具体如下

typedef struct {
    Elf32_Word      st_name;
    Elf32_Addr      st_value;
    Elf32_Word      st_size;
    unsigned char   st_info;
    unsigned char   st_other;
    Elf32_Half      st_shndx;
} Elf32_Sym;

每个字段的含义如下

字段 说明
st_name 符号在字符串表中对应的索引。如果该值非 0,则它表示了给出符号名的字符串表索引,否则符号表项没有名称。 注:外部 C 符号在 C 语言和目标文件的符号表中具有相同的名称。
st_value 给出与符号相关联的数值,具体取值依赖于上下文,可能是一个正常的数值、一个地址等等。
st_size 给出对应符号所占用的大小。如果符号没有大小或者大小未知,则此成员为0。
st_info 给出符号的类型和绑定属性。之后会给出若干取值和含义的绑定关系。
st_other 目前为0,其含义没有被定义。
st_shndx 如果符号定义在该文件中,那么该成员为符号所在节在节区头部表中的下标;如果符号不在本目标文件中,或者对于某些特殊的符号,该成员具有一些特殊含义。

其中,符号表中下标 0 存储了符号表的一个元素,同时这个元素也相对比较特殊,作为所有未定义符号的索引,具体如下

名称 取值 说明
st_name 0 无名称
st_value 0 0 值
st_size 0 无大小
st_info 0 无类型,局部绑定
st_other 0 无附加信息
st_shndx 0 无节区
st_value

在 Linux 的 ELF 文件中,具体说明如下

  1. 该符号对应着一个变量,那么表明该变量在内存中的偏移。我们可由这个值获取其文件偏移
    1. 获取该符号对应的 st_shndx,进而获取到相关的节区。
    2. 根据节区头元素可以获取节区的虚拟基地址和文件基地址。
    3. value-内存基虚拟地址=文件偏移-文件基地址
  2. 该符号对应着一个函数,那么表明该函数在文件中的起始地址。
st_info

st_info 中包含符号类型和绑定信息,这里给出了控制它的值的方式具体信息如下

#define ELF32_ST_TYPE(i)    ((i)&0xf)
#define ELF32_ST_INFO(b, t) (((b)<<4) + ((t)&0xf))
Symbol Type

可以看出 st_info 的低 4 位表示符号的类型,具体定义如下

名称 取值 说明
STT_NOTYPE 0 符号的类型没有定义。
STT_OBJECT 1 符号与某个数据对象相关,比如一个变量、数组等等。
STT_FUNC 2 符号与某个函数或者其他可执行代码相关。
STT_SECTION 3 符号与某个节区相关。这种类型的符号表项主要用于重定位,通常具有 STB_LOCAL 绑定。
STT_FILE 4 一般情况下,符号的名称给出了生成该目标文件相关的源文件的名称。如果存在的话,该符号具有 STB_LOCAL 绑定,其节区索引是 SHN_ABS 且优先级比其他STB_LOCAL符号高。
STT_LOPROCSTT_HIPROC 13~15 保留用于特定处理器

共享目标文件中的函数符号有比较特殊,当另一个目标文件从共享目标文件中引用一个函数时,链接器自动为被引用符号创建过程链接表项。共享目标中除了STT_FUNC , 其它符号将不会通过过程链接表自动被引用。

如果一个符号的值指向节内的特定位置,则它的节索引号 st_shndx,包含了它在节头表中的索引。当一个节在重定位过程中移动时,该符号值也做相应改变,对该符号的引用继续指向程序中的相同位置。有些特定节索引值具有其他语义。

Symbol Binding

根据 #define ELF32_ST_BIND(i) ((i)>>4) 可以看出 st_info 的高 4 位表示符号绑定的信息。而这部分信息确定了符号的链接可见性以及其行为,具体的取值如下

名称 取值 说明
STB_LOCAL 0 表明该符号为局部符号,在包含该符号定义的目标文件以外不可见。相同名称的局部符号可以存在于多个文件中,互不影响。
STB_GLOBAL 1 表明该符号为全局符号,对所有将被组合在一起的目标文件都是可见的。一个文件中对某个全局符号的定义将满足另一个文件对相同全局符号的未定义引用。我们称初始化非零变量的全局符号为强符号,只能定义一次。
STB_WEAK 2 弱符号与全局符号类似,不过它们的定义优先级比较低。
STB_LOPROC ~STB_HIPROC 13 这个范围的取值是保留给处理器专用语义的。

在每个符号表中,所有具有 STB_LOCAL 绑定的符号都优先于弱符号和全局符号。符号表节区中的 sh_info 项所对应的头部的成员包含第一个非局部符号的符号表索引。

此外,全局符号与弱符号的主要区别如下:

  • 当链接器在链接多个可重定位目标文件时,不允许定义多个相同名字的 STB_GLOBAL 符号。另一方面,如果存在一个已定义全局符号,则同名的弱符号的存在不会引起错误。链接器会优先选择全局定义,忽略弱符号定义。类似的,如果一个公共符号存在(st_shndx域为SHN_COMMON的符号),则同名的弱符号的存在不会引起错误。链接器会选择公共定义,忽略弱符号定义。
  • 当链接器寻找文件库时,它会提取包含未定义全局符号的成员,可能是一个全局符号或者弱符号。链接器不会为了解决未定义的弱符号问题而提取文件,未定义的弱符号的值为0。
符号取值

不同的目标文件类型对符号表项中 st_value 成员的解释不同:

  • 在可重定位文件中,st_value 保存了节区索引为 SHN_COMMON 的符号的对齐约束。
  • 在可重定位文件中,st_value 保存了已定义符号的节区偏移。也就是说,st_value保留了st_shndx 所标识的节区的头部到符号位置的偏移。
  • 在可执行和共享目标文件中,st_value 包含一个虚地址。为了使得这些文件的符号对动态链接器更有用,节区偏移(针对文件的解释)给出了与节区号无关的虚拟地址(针对内存的解释)。

符号表取值在不同的目标文件中具有相似的含义,可以有适当的程序可以采取高效的方法来访问数据。

st_shndx

特殊的索引及其意义如下

  • SHN_ABS: 符号的取值具有绝对性,不会因为重定位而发生变化。
  • SHN_COMMON: 符号标记了一个尚未分配的公共块。符号的取值给出了对齐约束,与节区的 sh_addralign 成员类似。就是说,链接编辑器将在地址位于 st_value 的倍数处为符号分配空间。符号的大小给出了所需要的字节数。
  • SHN_UNDEF: 此索引值表示符号没有定义。当链接编辑器将此目标文件与其他定义了该符号的目标文件进行组合时,此文件中对该符号的引用将被链接到实际定义的位置。
如何定位

那么对于一个符号来说如何定位其对应字符串的地址呢?具体步骤如下

  1. 根据 Section Header Table 中符号节头中的 sh_link 获取该符号节中对应符号字符串节在 Section Header Table 中的下标。进而我们就可以获取对应符号节的地址。
  2. 根据该符号的定义中的 st_name 获取该符号的偏移,即在对应符号节中的偏移。
  3. 根据上述两者就可以定位一个符号对应的字符串的地址了。

关于定位我们会在下章节重定位再详细讲解

.gnu.hash

注:本部分主要参考https://blogs.oracle.com/ali/gnu-hash-elf-sections。

在 ELF 良好地可扩展性的帮助下, GNU 为 ELF 对象添加了一个新的哈希节,这个节的性能相比于原有的 SYSV hash 会好很多。该节用于快速根据符号名获取对应符号表中的索引。

.bss

未初始化的全局变量对应的节。此节区不占用 ELF 文件空间,但占用程序的内存映像中的空间。当程序开始执行时,系统将把这些数据初始化为 0。bss其实是block started by symbol的简写,说明该节区中单纯地说明了有哪些变量。

.dataSection

这些节区包含初始化了的数据,会在程序的内存映像中出现。

.rodata

这些节区包含只读数据,这些数据通常参与进程映像的不可写段。

init & .init_array

此节区包含可执行指令,是进程初始化代码的一部分。程序开始执行时,系统会在开始调用主程序入口(通常指 C 语言的 main 函数)前执行这些代码。

.text

此节区包含程序的可执行指令。

.fini & .fini_array

此节区包含可执行的指令,是进程终止代码的一部分。程序正常退出时,系统将执行这里的代码。

.interp section

一般来说,可执行文件具有一个 PT_INTERP 类型的程序头元素,以便于来加载程序中的段。这个节包含了程序对应的解释器。在 exec (BA_OS) 过程中,系统会从该节中提取对应解释器的路径,并根据解释器文件的段创建初始时的程序镜像。也就是说,系统并不使用给定的可执行文件的镜像,而会首先为解释器构造独立的内存镜像。因此,解释器需要从系统处获取控制权,然后为应用程序提供执行环境。

解释器可能有两种方式获取控制权。

  • 它可以接收一个指向文件头的文件描述符,以便于读取可执行文件。它可以使用这个文件描述符来读取并将可执行文件的段映射到内存中。 有时候根据可执行文件格式的不同,系统有可能不会把文件描述符给解释器,而是会直接将可执行文件加载到内存中。虽然文件描述符可能会出现异常,但是解释器的初始状态仍然会与可执行文件的收到的相匹配,解释器本身不需要再有一个解释器。解释器本身可能是一个共享目标文件或者是一个可执行文件。 需要注意以下问题

  • 共享目标文件(正常情况下)被加载为地址独立的,也就是说,对于不同的进程来说,它的地址会有所不同。系统通过 mmap (KE_OS) 以及一些相关的操作来创建动态段中的内容。因此,共享目标文件的地址通常来说不会和原来的可执行文件的原有地址冲突。 可执行文件一般会被加载到固定的地址。系统通过程序头部表的虚拟地址来创建对应的段。因此,一个可执行文件的解释器的虚拟地址可能和第一个可执行文件冲突。解释器有责任来解决相应的冲突。

其余节区

名称 类型 属性 含义
.comment SHT_PROGBITS   包含版本控制信息。
.debug SHT_PROGBITS   此节区包含用于符号调试的信息。gcc使用-g参数生成,有这个节区才能使用gdb调试
.dynamic SHT_DYNAMIC SHF_ALLOC SHF_WRITE 此节区包含动态链接信息。SHF_WRITE 位设置与否是否被设置取决于具体的处理器。
.dynstr SHT_STRTAB SHF_ALLOC 此节区包含用于动态链接的字符串,大多数 情况下这些字符串代表了与符号表项相关的名称。结构和.strtab相同
.dynsym SHT_DYNSYM SHF_ALLOC 此节区包含动态链接符号表。
.got SHT_PROGBITS   此节区包含全局偏移表。动态链接章节会讲到相关知识
.line SHT_PROGBITS   此节区包含符号调试的行号信息,描述了源程序与机器指令之间的对应关系,其内容是未定义的。
.plt SHT_PROGBITS   此节区包含过程链接表(procedure linkage table)。动态链接章节会讲到
.relname SHT_REL   这些节区中包含重定位信息。如果文件中包含可加载的段,段中有重定位内容,节区的属性将包含SHF_ALLOC位,否则该位置 0。传统上 name 根据重定位所适用的节区给定。例如 .text 节区的重定位节区名字将是:.rel.text 或者 .rela.text。
.relaname SHT_RELA    
.shstrtab SHT_STRTAB   此节区包含节区名称。结构和.strtab一样
.text SHT_PROGBITS SHF_ALLOC, SHF_EXECINSTR 此节区包含可执行的代码段
.rela.text SHT_RELA   此节区包含可执行的代码段的重定位表, 每个需要重定位的节区都会有一个重定位表,比如.rela.dyn/.rela.plt。这个一个带加数的重定位表

注意:

  • 以 “.” 开头的节区名称是系统保留的,当然应用程序也可以使用这些节区。为了避免与系统节区冲突,应用程序应该尽量使用没有前缀的节区名称。
  • 目标文件格式允许定义不在上述列表中的节区,可以包含多个名字相同的节区。
  • 保留给处理器体系结构的节区名称一般命名规则为:处理器体系结构名称简写+ 节区名称。其中,处理器名称应该与 e_machine 中使用的名称相同。例如 .FOO.psect 节区是 FOO 体系结构中的 psect 节区。