技术书籍精读笔记:程序员的自我修养-链接,转载与库(贰)

本文深入解析了目标文件的结构与组成,重点以Linux的ELF格式为例。文章首先介绍了目标文件中除常见段外的其他段类型,如.rodata、.comment等,并详细阐述了目标文件的整体结构,包括文件头、段表和各个段的位置关系。通过对比Windows与Linux平台下目标文件的差异,重点讲解了ELF文件头中的魔数含义及其识别机制。文章还详细分析了段表的结构、段类型分类及标志位含义,并提供了32位程序相关类型的长度说明。最后解释了目标文件中段分布的对齐机制及其必要性,为理解目标文件底层原理提供了全面指导。

作者
WildPointer
发布
2025.07.26
专栏
技术书籍精读笔记
阅读
约 11 分钟 / 1,187 次原文浏览
技术书籍精读笔记:程序员的自我修养-链接,转载与库(贰)

前言

通过博客《使用binutils工具分析目标文件(壹)》我们知道了目标文件的主要段的含义,以及代码经过汇编这一阶段后存储在目标文件的哪一个段中,而对于本篇博客则会深入讲解目标文件的总体结构(PS:以Linux下的ELF文件为例,因为作者使用Windows偏多,所以大多数的实验截图是在Windows上进行的,读者如果使用Linux偏多可以在Linux中实验)


目标文件的其他段

除了.text,.data,.bss等一些比较常用的段之外,目标文件还包含以下其他的段:

段名称说明
.rodata1Read only Data,这种段里存放的是只读数据,例如字符串常量,全局const变量
.comment该段存储的是编译器的版本信息,例如"GCC:(GNU)4.2.0"
.debug调试信息
.dynamic动态链接信息
.hash符号哈希表
.line调试时的行号表,即源代码的行号与编译后指令的对应表
.note额外的编译器信息,比如程序的公司名称,分布版本号
.strtabString Table字符串表,用于存储目标文件中用到的各种字符串
.symtabSymbol Table符号表
.shstrtabSection String Table段名表

.plt

.got

动态链接的跳转表和全局入口表

.init

.fini

程序初始化与终结代码段

表1.目标文件中的其它段


目标文件结构

目标文件格式的最前部是目标文件的文件头(Header),包含了描述整个文件的基本属性,比如目标文件的版本,目标机器型号,程序入口地址等。紧接着则是目标文件的各个段,其中目标文件中与段有光的重要结构就是段表,该表描述了目标文件包含的所有段的信息,比如每一个段的段名,段的长度,在文件中的偏移,读写权限及段的其他属性。具体参考下图:

图1.目标文件结构


目标文件中的文件头和魔数

        在Linux中我们可以使用以下命令来查看目标文件的信息

bash
readelf -h xxx.o

在Windows中我们可以使用以下命令查看目标文件的信息

bash
objdumo -f xxx.o

由于作者的实验主要是在Windows中进行的,所以以下截图是位于Windows中的。

图2.Windows中目标文件信息

区别于Windows中输出的信息,Linux下输出的目标文件信息更多,并且Linux中的目标文件会输出有魔数。魔数的作用主要是确认文件的类型,操作系统在加载可执行文件的时候会确认魔数是否正确,不正确会拒绝加载。由于Windows平台下,GCC生成的目标文件遵循COFF或PE,而非Linux的ELF格式,而readelf命令仅支持ELF格式的目标文件,无法解析COFF/PE文件,会报错魔数不匹配,对于Linux中的ELF格式魔数为7f454c46c\XFELE,而Windows中的魔数为4D5ACMZ。

为了方便讲解目标文件头中定义的信息,我们参考Linux中输出的ELF文件的信息:

bash
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:                              Intel 80386    Version:                              0x1    Entry point address:                  0x0    Start of program headers:             0(bytes into file)    Start of section headers:             280(bytes into file)    Flags:                                0x0    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:    8

以上这些是通过readelf指令输出Linux中目标文件(ELF)的信息,最前面的“Magic”的16个字节用于标识ELF文件的平台属性,具体如下图:

图3.ELF中魔数含义

我们只知道在强语言类型中定义一些数据都需要先为这些数据定义类型,比如是字符串string类型,还是整型int。而对于ELF文件中存储的这些数据也有对应的类型,这里我们以32位版本的目标文件头来作为例子,以下是该类型的ELF的结构

bash
typedef struct{    unsigned char e_ident[16];    // ELF魔数,文件类别,​​字节序,​​ELF版本号和操作系统/ABI标识    Elf32_Half e_type;            // 文件类型    Elf32_Half e_machine;         // ELF文件的CPU平台属性    Elf32_Word e_version;         // ELF版本号    Elf32_Addr e_entry;           // 入口地址    Elf32_off e_phoff;            // 程序头表文件偏移    Elf32_off e_shoff;            // 段表在文件中的偏移    Elf32_Word e_flags;           // ELF标志位    Elf32_Half e_ehsize;          // ELF文件头本身的大小    Elf32_Half e_phentsize;       // 程序头表项大小    Elf32_Half e_phnum;           // 程序头表项数量    Elf32_Half e_shentsize;       // 段表描述符的大小    Elf32_Half e_shnum;           // 段表描述符的数量    Elf32_Half e_shstrndx;        // 段表字符串表所在的段在段表中的下标} Elf32_Endr;

对于这些类型都有对应的长度,具体参考下表:

类型描述原始类型长度(字节)
Elf32_Half32位版本的无符号短整型uint16_t2
Elf32_Word32位版本无符号整型int32_t4
Elf32_Addr32位版本程序地址uint32_t4
Elf32_off32位版本的偏移地址uint32_t4

表2.32位程序的类型描述


目标文件中的段表

由于目标文件中存在很多的段,而在运行的时候需要检索这些段信息,为了方便检索这些信息则产生了段表,即存储这些段的基本属性,并且描述了目标文件中的各个段的信息。也就是说目标文件的段结构就是由段表决定的,编译器,链接器和装载器都是依靠段表来定位和访问各个段的属性的,而段表在目标文件的具体位置则是由目标文件头中的e_shoff成员决定的。

为了输出目标文件中的段表信息,我们在Linux中可以使用以下命令:

bash
readelf -S xxx.o

为了方便分析段表的各个类型和代表的含义,以下是输出的段表信息:

bash
There are 11 section headers,starting at offset 0x118: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   00005b  00    AX     0     0      4    [2] .rel.text    REL         00000000   000428   000028  08           9     1      4    [3] .data        PROGBITS    00000000   000090   000008  00    WA     0     0      4    [4] .bss         NOBITS      00000000   000098   000004  00    WA     0     0      4    [5] .rodata      PROGBITS    00000000   000098   000004  00    A      0     0      1    [6] .comment     PROGBITS    00000000   00009c   00002a  00    0      0     1    [7] .note.GNU-stack   PROGBITS   00000000    0000c6    000000  00 0   0     1    [8] .shstrtab    STRTAB      00000000   0000c6   000051  00    0      0     1    [9] .symtab      SYMTAB      00000000   0002d0   0000f0  10           10    10     4    [10] .strtab     STRTAB      00000000   0003c0   000066  00    0      0     1Key to Flags:    W(write),A(alloc),X(execute),M(merge),S(strings)    I(info),L(link order),G(group),X(unknown)    O(extra OS processing required),O(OS specific),p(processot specific)

类似于目标文件中的文件头,段表中的数据存储也是通过定义结构体的形式来实现的,具体的定义如下:

bash
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;

对于段的类型,可以参考下表:

常量值含义
SHT_NULL0无效段
SHT_PROGBITS1程序段。代码段,数据段都是这种类型
SHT_SYMATB2标识该段的内容为符号表
SHT_STRTAB3标识该段的内容为字符串表
SHT_RELA4重定位表,该段包含了重定位信息
SHT_HASH5符号表的哈希表
SHT_DYNAMIC6动态链接信息
SHT_NOTE7提示性信息
SHT_NOBITS8标识该段在文件中没有内存,例如.bss段
SHT_REL9该段包含了重定位信息
SHT_SHLIB10保留段
SHT_DNYSYM11动态链接的符号表

表3.段的类型

对于段的标志为,主要标识该段在进程虚拟地址空间中的属性,比如是否可写,是否可执行等信息,具体参考下表:

常量值含义
SHF_WRITE0标识该段在进程空间可写
SHF_ALLOC2标识该段在进程空间中要分配空间
SHF_EXECINSTR4标识该段在进程空间中可以被执行,一般指代码段

表4.段的标志位

对于一些系统保留的段的类型和标志位,可以参考下表了解:

段名段的类型段的标志位
.bssSHT_NOBITSSHF_ALLOC + SHF_WRITE
.commentSHT_PROGBITSnone
.dataSHT_PTOGBITSSHF_ALLOC + SHF_WRIRE
.data1SHT_PTOGBITSSHF_ALLOC + SHF_WRIRE
.debugSHT_PTOGBITSnone
.dynamicSHT_DYNAMICSHF_ALLOC + SHF_WRIRE
.hashSHT_HASHSHF_ALLOC
.lineSHT_PTOGBITSone
.noteSHT_NOTEnone
.rodataSHT_PROGBITSSHF_ALLOC
.rodata1SHT_PROGBITSSHF_ALLOC
.shstrtabSHT_STRTABnone
.strtabSHT_STRTAB如果该目标文件有可装载的段须要用到该字符串表,那么该字符串表也得被装载到进程空间,则有SHF_ALLOC标志位
.symtabSHT_STRTAB同字符串表
.textSHT_PROGBITSSHF_ALLOC + SHF_EXECINSTR

表5.系统保留段的类型和标志位

对于段的链接信息,如果段的类型是与链接相关的(无论是动态链接还是静态链接),比如重定位表,符号表等,那么sh_link和sh_info则可以参考下表:

sh_typesh_linksh_info
SHT_DYNAMIC该段所使用的字符串表在段表中的下标0
SHT_HASH该段所使用的符号表在段表的下标0
SHT_REL该段所使用的相应符号表在段表中的下标该重定位表所作用的段在段表中的下标
SHT_RELA
SHT_SYMTAB操作系统相关的操作系统相关的
SHT_DYNSYM
otherSHN_UNDEF0

表6.段的链接信息


目标文件中的段总体信息

由于CPU的物理访问机制,每次访问4/8/16字节的整数倍地址,而0x167是439,不是4/8/16的整数倍。如果不填充字节的话会导致在访问Section Table时,第一次读取的地址位0x164到0x167但只有0x167属于Section Table的有效地址,导致后续的访问都需要进行数据的拼接处理。所以最终的目标文件中段的分布如下图:

图4.目标文件中段的分布

13次原文点赞;这里的喜欢仅保存在本机
WildPointer

专注系统编程、工程实践与底层技术,记录 C++、Qt、OpenCV 与 VTK 的学习和实践。