深入理解计算机系统篇之链接(2):目标文件

Posted by WHX on November 9, 2025

深入理解计算机系统篇之链接(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表示生成地址无关的代码,它是编译器的选项,在编译阶段就要确定代码是否要生成地址无关的了。

注意:含有main的源文件不能用于生成可共享目标文件。

3.目标文件格式(ELF-32)

首先给出目标文件的通用格式:

上述的目标文件格式是一个预览图,目的是为了介绍目标文件的格式分几大板块,第一个是所以目标文件都有的一个文件信息的描述ELF header;第二个是只有可执行目标文件和可共享目标文件才有的程序头表,用于运行加载使用;第三个所以目标文件都会有的节区,不过大多不太一样,某些目标文件会有特殊的节区;最后一个是描述节区详细信息的表。

注意:目标文件的节区顺序(除了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。
提示:ELF头部介绍比较简单,大概看一下字段表示什么意思就行,重点关注后面节区相关的功能作用

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 不包含任何条目
注意:Section Header Table最重要的作用就是告诉链接器,某个节区的名称、大小、偏移以便链接器去链接不同的目标文件。所以Section Header Table主要是为链接器使用的,加载程序时一般不会去查这个表,而是查Program Header Table

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。

备注:在.shstrtab字符串表中没有.text节区的字符串是因为在.rel.text复用了,因为是偏移量所以可以.rel.text的Name为1b,然后.text的偏移量为20

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字段

注意:Type字段和Bind字段都在一个结构体字段st_info中,Type字段为info的低四位,Bind字段为info的高四位
名称 取值 说明
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 保留用于特定处理器

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的全局变量与静态变量

采用这种方式的原因在于链接器符号解析的方式,具体见下一章节。

总结:关于可重定位目标文件的重要的几个节区内容以及头部表已经介绍完成,还有一些可执行目标文件的节区放在之后再讲