深入理解计算机系统篇之链接(2):目标文件
1.摘要
本文主要讲解一下链接的基础:目标文件格式以及相关基础知识,嵌入式的目标文件通常是ELF-32,本文就ELF-32描述一下其文件的组织,还有一些查看elf文件的工具,如Mac上的greadelf等。
2.目标文件类型
目标文件是汇编或链接的产物,主要类型分为==可重定位目标文件、可执行目标文件和可共享目标文件==(还有一个核心转储文件在嵌入式领域不常用)。在 ELF(Executable and Linkable Format)体系下,文件类型由 ELF Header → e_type 字段决定。 可以理解为:同样的 ELF 结构,不同类型承担不同的职责。
| 目标文件类型 | e_type | 编译阶段 | 是否可执行 | 作用 |
|---|---|---|---|---|
| 可重定位目标文件(.o) | ET_REL | 汇编 | 否 | 用于链接器合并生成其他文件 |
| 可执行目标文件(.exe .out .elf等) | ET_EXEC | 链接 | 是 | 用于执行,程序开始 |
| 可共享目标文件(.so) | ET_DYN | 链接 | 是 | 用于动态加载调用 |
1)生成可重定位目标文件
arm-none-eabi-gcc -c test01.c
-c表示只执行编译和汇编过程,不会执行链接过程,所以汇编结束后会生成.o文件
2)生成可执行目标文件
arm-none-eabi-gcc test01.c -o test01
-o表示输出特定文件名称的可执行文件,如果不加会输出a.out文件,文件内容一样,只不过名称不同而已
3)生成可共享目标文件
arm-none-eabi-gcc -shared -fPIC test01.c -o test01.so
同样不加-o会输出a.out;前面的-shared代表生成可共享目标文件,是链接器的选项,也就是链接阶段才会用到的选项;-fPIC表示生成地址无关的代码,它是编译器的选项,在编译阶段就要确定代码是否要生成地址无关的了。
3.目标文件格式(ELF-32)
首先给出目标文件的通用格式:

上述的目标文件格式是一个预览图,目的是为了介绍目标文件的格式分几大板块,第一个是所以目标文件都有的一个文件信息的描述ELF header;第二个是只有可执行目标文件和可共享目标文件才有的程序头表,用于运行加载使用;第三个所以目标文件都会有的节区,不过大多不太一样,某些目标文件会有特殊的节区;最后一个是描述节区详细信息的表。
3.1可重定位目标文件格式
下面是可重定位目标文件的详细格式:
| 组成部分 | 描述 | 如何查看 |
|---|---|---|
| ELF header | 描述运行该目标文件的**系统**的字大小和字节顺序、目标文件类型、机器类型等信息。 | greadelf -h <object file> |
| .text | 已编译程序的机器字节码。 | 方式1:objdump -d <object file>(只输出汇编,输出内容包括:.init、.plt、.plt.got、.text、.fini 等 sections)方式2:objdump -S <object file>(输出汇编和对应的源码。需要编译时加 -g 选项,才会打印源码。)方式3:objdump -d --section .text sum.so(只输出汇编,且只打印 .text section) |
| .rodata | 只读数据,比如:常量字符串、带 const 修饰的全局变量和静态变量等。 | 无 |
| .data | 已初始化的且初始值非0的全局变量和静态变量。 | 无 |
| .bss | 未初始化的或初始值为0的全局变量和静态变量。 | 无 |
| .symtab | 一个符号表,它存放着在目标文件中定义和引用的函数和全局变量、静态变量的信息。 | readelf -s <object file>(注:删除目标文件中 .symtab section 的命令为:strip <object file>) |
.rel<name> |
<name> section 的重定位信息。如.rel.text表示text段的重定位信息 |
readelf -r <object file> |
| .debug | 一个调试符号表,其条目是程序中定义的局部变量和类型定义(typedefs),程序中定义和引用的全局变量,以及原始的 C 源文件。(只有带 -g 编译时才会产生) | 方式1:readelf --debug-dump <object file>方式2:objdump -g <object file>(注:删除目标文件中 .debug 等与调试相关 sections 的命令为:strip --strip-debug <object file>(只删除调试信息相关的 sections)或 strip <object file>(除调试信息相关的 sections 以外,还删除 .symtab 和 .strtab) ) |
| .comment | 版本控制信息。 | readelf -p .comment <object file> |
| .shstrtab | 一个字符串表,用来保存“节区名字符串”的表 | readelf -p .shstrtab <object file>(注:不能通过 strip 命令删除 .shstrtab section ) |
| .strtab | 一个字符串表,其内容包括所有符号的名称。 | readelf -p .strtab <object file> (注:删除目标文件中 .strtab section 的命令为:strip <object file>) |
| Section header table | 描述目标文件的所有节(sections)的位置和大小等信息。 | readelf -S <object file> |
3.2可执行目标文件格式
可执行目标文件的格式比可重定位目标文件多了一些信息具体如下:
| 组成部分 | 描述 | 如何查看 |
|---|---|---|
| ELF header | 描述生成该目标文件的系统的字大小和字节顺序、目标文件类型、机器类型等信息。 | readelf -h <object file> |
| Program header table | 描述可执行目标文件中所有段(Segments)的位置和大小等信息。 | 方式1:readelf -l <object file>方式2:objdump -p <object file> |
| .interp | 该 section 中保存了可执行目标文件所需要的动态链接器的路径。 | 方式1:readelf -p .interp <object file>方式2:readelf -l <object file> | grep interpreter方式3:objdump -s --section .interp <object file> |
| .dynsym | 包含需要动态链接的符号 | 方式1:readelf --dyn-syms <object file>方式2:readelf -s <object file> |
| .dynstr | 动态链接符号表,仅包括需要动态链接的符号名称 | readelf -p .dynstr <object file> |
| .rela.dyn | 动态链接中的数据重定位表。类似于静态链接中的 .rela.data。 | readelf -r <object file> |
| .rela.plt | 动态链接中的代码重定位表。类似于静态链接中的 .rela.text。 | readelf -r <object file> |
| .init | 该 section 中定义了一个名称为 _init 的函数,会被程序初始化代码调用。 | objdump -d <object file> |
| .text | 已编译程序的机器代码。 | 方式1:objdump -d <object file>(只输出汇编)方式2:objdump -S <object file>(输出汇编和对应的源码。需要编译时加-g选项,才会打印源码。)方式3:objdump -d --section .text sum.so(只输出汇编,且只打印 .text section) |
| .rodata | 只读数据,比如:常量字符串、带 const 修饰的全局变量和静态变量等。 | 无 |
| .dynamic | 该 section 中保存了动态链接的基本信息。 | 方式1:readelf -d <object file>方式2:objdump -p <object file> |
| .data | 已初始化的且初始值非0的全局变量和静态变量。 | 无 |
| .bss | 未初始化的或初始值为0的全局变量和静态变量。 | 无 |
| .symtab | 一个符号表,它存放着在目标文件中定义和引用的函数和全局变量、静态变量的信息。 | readelf -s <object file>(注:删除目标文件中 .symtab section 的命令为:strip <object file>) |
| .debug | 一个调试符号表,其条目是程序中定义的局部变量和类型定义(typedefs),程序中定义和引用的全局变量,以及原始的 C 源文件。(只有带 -g 编译时才会产生) | 方式1:readelf --debug-dump <object file>方式2:objdump -g <object file> |
| .comment | 版本控制信息。 | readelf -p .comment <object file> |
| .shstrtab | 一个字符串表,其内容包括所有 section 的名称。 | readelf -p .shstrtab <object file> |
| .strtab | 一个字符串表,其内容包括所有符号的名称。 | readelf -p .strtab <object file> (注:删除目标文件中 .strtab section 的命令为:strip <object file>) |
| Section header table | 描述目标文件的所有节(sections)的位置和大小等信息。 | readelf -S <object file> |
4.节区介绍
下面对目标文件中的各个节区进行介绍,首先介绍一下ELF header的结构,然后对可重定位目标文件中的某些节区进行介绍,最后介绍一下可执行目标文件多出的部分节区。
4.1 头部ELF header
查看可重定位目标文件(ELF-32 格式)的ELF Header的内容:
% greadelf -h test01.o
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: ARM
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 560 (bytes into file)
Flags: 0x5000000, Version5 EABI
Size of this header: 52 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 40 (bytes)
Number of section headers: 11
Section header string table index: 10
该头部有Elf32_Ehdr结构体与其对应:
| 成员 | greadelf字段 | greadelf结果说明 |
|---|---|---|
| e_ident | Magic,类别,数据,版本,OS/ABI,ABI | Magic[0]-[3]魔数:前面固定四字节为Del、E、L、F Magic[4]类别:Elf32 Magic[5]数据:小端 Magic[6]版本:1 Magic[7]操作系统及ABI:UNIX - System V |
| e_type | 类型 | 文件类型为可重定位目标文件——ET_REL(一共三种类型ET_REL\ET_EXEC\ET_DYN) |
| e_machine | 系统架构 | ARM |
| e_version | 版本 | 1 |
| e_entry | 入口点地址 | 程序入口点的虚拟地址,0表示没有程序入口点 |
| e_phoff | start of program headers | program header table 的起始位置在目标文件中的偏移量,0表示没有program header table |
| e_shoff | start of section headers | section header table 的起始位置在目标文件中的偏移量为560 |
| e_flags | 标志 | 特定处理器中的标识 |
| e_ehsize | 文件头的大小 | ELF Header 的大小为52字节 |
| e_phentsize | 程序头大小 | program header table 中每个条目的大小为0。因为可重定位目标文件没有program header table。 |
| e_phnum | number of program headers | program header table 中条目的数量为0。原因同上 |
| e_shentsize | 节头大小 | section header table 中每个条目的大小为40字节 |
| e_shnum | number of program headers | section header table 中条目的数量为11个 |
| e_shstrndx | 字符串表段索引 | .strtab section 在 section header table 中的索引。0 表示没有 .strtab section。 |
4.2节区表Section Header Table
关注节区之前先关注编译器、链接器、调试器是如何知道某个节区在哪里的,这里目标文件提供一个节区表供它们查询定位。
查看可重定位目标文件(ELF-32格式)的Section Header Table的内容:
% greadelf -S test01.o
There are 11 section headers, starting at offset 0x230:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 00002c 00 AX 0 0 4
[ 2] .rel.text REL 00000000 0001c4 000018 08 I 8 1 4
[ 3] .data PROGBITS 00000000 000060 000000 00 WA 0 0 1
[ 4] .bss NOBITS 00000000 000060 000000 00 WA 0 0 1
[ 5] .rodata PROGBITS 00000000 000060 000005 00 A 0 0 4
[ 6] .comment PROGBITS 00000000 000065 000047 01 MS 0 0 1
[ 7] .ARM.attributes ARM_ATTRIBUTES 00000000 0000ac 00002a 00 0 0 1
[ 8] .symtab SYMTAB 00000000 0000d8 0000d0 10 9 11 4
[ 9] .strtab STRTAB 00000000 0001a8 00001c 00 0 0 1
[10] .shstrtab STRTAB 00000000 0001dc 000051 00 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),
D (mbind), y (purecode), p (processor specific)
starting at offset 0x230正好对应ELF Header中的Start of section headers的值(16进制0x230=10进制560)
There are 11 section headers一共有11个条目,可以与ELF header的Number of section headers对应上,每个条目40字节大小,条目中每个字段的含义如下表:
| 字段名称 | 字段大小 | 字段含义 | 常用字段值 |
|---|---|---|---|
| Name | 4 | 该 section 名称的起始位置在.shstrtabsection 中的偏移量 |
偏移量,只不过greadelf通过在.shstrtabsection的索引找到的字符串展示在控制台上 |
| Type | 4 | section的类型 | 0(SHT_NULL,表示无效 section) 1(SHT_PROGBITS,表示该 section 包含由程序定义的信息,比如:.text、.data sections) 2(SHT_SYMTAB,表示链接器符号表) 3(SHT_STRTAB,表示字符串表) 4(SHT_RELA,表示 “Rela” 类型的重定位表) 5(SHT_HASH,表示符号表的哈希表) 6(SHT_DYNAMIC,表示动态链接信息) 7(SHT_NOTE,表示提示性信息) 8(SHT_NOBITS,表示未初始化的空间,不占用目标文件的任何空间,比如:.bss section) 9(SHT_REL,表示 “Rel” 类型的重定位表) 10(SHT_SHLIB,保留) 11(SHT_DYNSYM,表示动态链接的符号表)等 |
| Flags | 4 | section 在进程虚拟地址空间中的属性 | 1(SHF_WRITE,简写为 W,表示该 section 在进程空间中可写) 2(SHF_ALLOC,简写为 A,表示该 section 在进程空间中需要被分配空间,即执行程序时会将该 section 加载到内存中) 3(SHF_EXECINSTR,简写为 X,表示该 section 在进程空间中可以被执行) |
| Address | 4 | 表示该 section 的起始位置在被加载后所对应的在进程地址空间中的虚拟地址(该字段仅对于那些可加载到内存的目标文件有意义,对可重定位目标文件来说,应该全为0) | 地址值,0 表示该 section 不会被加载到内存中。 |
| Offset | 4 | 表示该 section 的起始位置在目标文件中偏移量(与Size字段一起用于链接器进行拼接) | 偏移量 |
| Size | 4 | 表示该 section 在目标文件中占用的空间大小(除 SHT_NOBITS section 以外) | 字节大小 |
| Link | 4 | 表示该 section 相关 section 的索引。比如:如果该条目表示.textsection,并且其重定位信息在.rela.textsection 中,那么该字段的值为.rela.textsection 的索引。 |
索引值 |
| Info | 4 | 表示该 section 的额外信息 | |
| Align | 4 | 表示该 section 的内存对齐要求 | 这个字段的值必须是 2 的幂。如果该字段的值为 0 或 1,那么表示该 section 没有内存对齐要求。 |
| EntSize | 4 | 表示该 section 中每个条目的大小 (它只对由固定大小条目组成的“表格类型的节”有意义) | 0 表示该 section 不包含任何条目 |
4.2字符串表.shstrtab与.strtab
分别查看.shstrtab与.strtab
% greadelf -p .shstrtab test01.o
String dump of section '.shstrtab':
[ 1] .symtab
[ 9] .strtab
[ 11] .shstrtab
[ 1b] .rel.text
[ 25] .data
[ 2b] .bss
[ 30] .rodata
[ 38] .comment
[ 41] .ARM.attributes
% greadelf -p .strtab test01.o
String dump of section '.strtab':
[ 1] test01.c
[ a] $d
[ d] gal_2
[ 13] $a
[ 16] loc_2.0
[ 1e] gal_1
[ 24] main
[ 29] printf
代码比较简陋,看看大概即可
这两张字符串表的目的就是为了其余两个节区的结构可以完整对齐,如果不采用字符串表,那么Name字段很难对齐,所以这种实现方式很大程度上是为了对齐想出来的。一张是节区头表的Name字符串,一张是.symtab符号表的Name字符串,它们的Name字段都是索引值,比如.symtab的Name字段是10,就表示这个条目的名称为main。
4.3符号表.symtab
符号表是链接部分最核心的一张表,通过符号表可以在其他文件中找到自己引用的函数、变量等
符号表中的符号可以分为三类:
| 符号类型 | 定义者 | 可见性 | 哪些属于这类符号 |
|---|---|---|---|
| 由目标文件自身定义并能被其他模块引用的全局符号 | 本目标文件 | 本目标文件和其他目标文件都可见 | 由本目标文件定义的非静态函数和非静态全局变量 |
| 由其他目标文件定义并能被模块自身引用的全局符号 | 其他目标文件 | 本目标文件和其他目标文件都可见 | 由本目标文件引用的但定义在其他目标文件中的非静态函数和非静态全局变量 |
| 只被目标文件自身定义和引用的局部符号 | 本目标文件 | 仅本目标文件可见 | 由本目标文件定义的静态函数、静态全局变量和静态局部变量 |
符号表中不包含本地非静态程序局部变量的任何符号,这些符号在运行时的栈中进行管理;不过静态局部变量不是在栈中管理,而是编译器会像对全局变量一样为局部静态变量分配空间,并且在符号表中创建一个有唯一名字的本地符号链接。所以,如果两个不同的函数中存在相同局部静态变量也不会造成符号表中有冲突,因为编译器输出了两个不同的名字。
下面举例介绍符号表都包含哪些字段,含义是什么:
% greadelf -s test01.o
Symbol table '.symtab' contains 17 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS test01.c
2: 00000000 0 SECTION LOCAL DEFAULT 1 .text
3: 00000000 0 SECTION LOCAL DEFAULT 3 .data
4: 00000000 0 SECTION LOCAL DEFAULT 4 .bss
5: 00000000 0 NOTYPE LOCAL DEFAULT 4 $d
6: 00000004 4 NOTYPE LOCAL DEFAULT 4 gal_2
7: 00000000 0 SECTION LOCAL DEFAULT 5 .rodata
8: 00000000 0 NOTYPE LOCAL DEFAULT 5 $d
9: 00000000 0 NOTYPE LOCAL DEFAULT 1 $a
10: 00000034 0 NOTYPE LOCAL DEFAULT 1 $d
11: 00000008 4 NOTYPE LOCAL DEFAULT 4 loc_2.0
12: 00000000 0 SECTION LOCAL DEFAULT 6 .comment
13: 00000000 0 SECTION LOCAL DEFAULT 7 .ARM.attributes
14: 00000000 4 OBJECT GLOBAL DEFAULT 4 gal_1
15: 00000000 56 FUNC GLOBAL DEFAULT 1 main
16: 00000000 0 NOTYPE GLOBAL DEFAULT UND printf
符号表中各个属性的含义:
| 属性 | 结构体字段 | 说明 |
|---|---|---|
| Name | st_name | 符号在字符串表中对应的索引。如果该值非 0,则它表示了给出符号名的字符串表索引,否则符号表项没有名称。 |
| Value | st_value | 给出与符号相关联的数值,具体取值依赖于上下文,可能是一个正常的数值、一个地址等等。 |
| Size | st_size | 给出对应符号所占用的大小。如果符号没有大小或者大小未知,则此成员为 0。 |
| Type+Bind | st_info | 给出符号的类型和绑定属性。之后会给出若干取值和含义的绑定关系。 |
| Vis | st_other | 目前为 0,其含义没有被定义。 |
| Ndx | st_shndx | 如果符号定义在该文件中,那么该成员为符号所在节在节区头部表中的下标;如果符号不在本目标文件中,或者对于某些特殊的符号,该成员具有一些特殊含义。 |
4.3.1Name字段
符号表的Name字段都是一个索引值,指向.strtab里的各个函数或者变量。如果是节区即类型为SECTION,那么Name字段值为0,正常节区的Name显示与符号表的第0项一样为空,但是这里输出了节区名称是因为greadelf通过Ndx查找的Section Header 的 sh_name,而sh_name不是0,是一个指向.shstrtab字符串表的索引,所以最终控制台打印的符号表里有节区的名称。
4.3.2Value字段
符号表的Value字段根据不同的类型有不同的含义,具体如下表:
| 文件类型 | 符号类型 | st_value 的含义 |
|---|---|---|
| 可重定位文件 (.o) | 函数/变量 | 该符号相对于所属 section 起始地址的偏移 |
| 可重定位文件 (.o) | SECTION | 该节区的起始地址(通常是 0) |
| 可执行文件 (ELF) | 函数/变量 | 载入内存后的虚拟地址(VA) |
| 共享库 (.so) | 函数/变量 | 地址为相对虚拟地址(通常是基址 + offset) |
| dynsym 中的动态符号 | 函数/变量 | PLT/GOT 或运行时可解析的虚拟地址 |
| 未定义符号 (UND) | ANY | 0(必须在链接时或运行时解析) |
4.3.3Size字段
Size表示符号所占的空间大小,不是所有的符号都有大小的,一般除了函数和变量,其他类型大小基本为0。函数的大小一般根据.text节区计算出来的。
4.3.4Type字段
| 名称 | 取值 | 说明 |
|---|---|---|
| STT_NOTYPE | 0 | 符号的类型没有定义。 |
| STT_OBJECT | 1 | 符号与某个数据对象相关,比如一个变量、数组等等。 |
| STT_FUNC | 2 | 符号与某个函数或者其他可执行代码相关。 |
| STT_SECTION | 3 | 符号与某个节区相关。这种类型的符号表项主要用于重定位,通常具有 STB_LOCAL 绑定。 |
| STT_FILE | 4 | 一般情况下,符号的名称给出了生成该目标文件相关的源文件的名称。如果存在的话,该符号具有 STB_LOCAL 绑定,其节区索引是 SHN_ABS 且优先级比其他STB_LOCAL符号高。 |
STT_LOPROC~STT_HIPROC |
13~15 | 保留用于特定处理器 |
4.3.5Bind字段
这部分信息确定了符号的链接可见性以及其行为
| 名称 | 取值 | 说明 |
|---|---|---|
| STB_LOCAL | 0 | 表明该符号为局部符号,在包含该符号定义的目标文件以外不可见。相同名称的局部符号可以存在于多个文件中,互不影响。 |
| STB_GLOBAL | 1 | 表明该符号为全局符号,对所有将被组合在一起的目标文件都是可见的。一个文件中对某个全局符号的定义将满足另一个文件对相同全局符号的未定义引用。我们称初始化非零变量的全局符号为强符号,只能定义一次。 |
| STB_WEAK | 2 | 弱符号与全局符号类似,不过它们的定义优先级比较低。 |
| STB_LOPROC ~STB_HIPROC | 13 | 这个范围的取值是保留给处理器专用语义的。 |
4.3.6Ndx字段
符号所在的“所属节区(Section Index)”或特殊标记。
特殊的索引及其意义如下
- SHN_ABS: 符号的取值具有绝对性,不会因为重定位而发生变化。
- SHN_COMMON: 符号标记了一个尚未分配位置的未被初始化的数据目标。符号的取值给出了对齐约束,与节区的 sh_addralign 成员类似。就是说,链接编辑器将在地址位于 st_value 的倍数处为符号分配空间。符号的大小给出了所需要的字节数。
- SHN_UNDEF: 此索引值表示符号没有定义。当链接编辑器将此目标文件与其他定义了该符号的目标文件进行组合时,此文件中对该符号的引用将被链接到实际定义的位置。
COMMON和.bss的区别有些细微。现在编译器根据以下规则将可重定位目标文件的符号分配到COMMON与.bss中:
- COMMON:未初始化的全局变量
- .bss:未初始化的静态变量,以及初始化为0的全局变量与静态变量
采用这种方式的原因在于链接器符号解析的方式,具体见下一章节。