一、前言
在一般的开发过程中,我们往往离不开 编译 这个操作。有时候遇到一些编译错误的时候,第一反应就是查找百度。但如果我们对编译的整体有个比较清晰的认知,很多常见的错误我们是可以通过错误提示直接分析出错误的原因的。那么这个系列就是简单的记录下学习此书的知识,也希望对各位读者有所帮助。
注意:此系列仅讲述Linux系统下的C语言编译
二、目标文件
2.1 ELF格式
我们可以都知道一个程序是加载到系统中去执行,那么这个程序文件我们通常称呼为 可执行文件。比如windows 下 的 exe文件。而 Linux 下则没有后缀,每种系统下的 可执行文件 都有自己的格式,Linux 下的文件为 ELF格式。
目标文件 是代码 编译 后但未进行 链接 的 中间文件。 Linux 的 .o文件 即为 目标文件。从广义上看,目标文件 和 可执行文件 的格式几乎一致,可以看成一种类型的文件。
Linux 使用 ELF格式 存储的文件包括:
- 可执行文件
- 静态链接库
- 动态链接库
- 目标文件(可定位文件)
- 核心转储文件(core Dump file)
ELF文件类型 | 说明 | 文件 |
---|---|---|
可定位文件 | 文件包含了 代码 和 数据 ,可被用来链接为 可执行文件 或者 共享目标文件 | .a 文件和 .o文件 |
可执行文件 | 可以直接被执行,在 Linux 中此类文件没有拓展名 | 无拓展名 |
共享目标文件 | 包含了 代码和数据, 一般分 2种情况。一是与其他 可定位文件 和 共享目标文件 进行链接产生 新的目标文件。二是将多个 共享目标文件 和 可执行文件 结合,作为进程的一部分来运行 | .so文件 |
核心转储文件 | 当进程意外终止时,系统可以将此进程的地址信息及其他信息存储到该文件中 | core dump |
2.2 目标文件内容
目标文件 往往包含了许多内容,包括 数据、代码、链接信息 等,其中 链接信息 包括 符号表、调试信息、字符串表等等。,根据这些内容组织形式的不同可以分为 2 种形式:
- section:按照笔者理解,section 是 ELF文件 的基本单位
- segment:由多个类型相同的 section 组成
在上面的图中,链接视图 有个 节区头部表(section talbe) ,在链接中也称为 段表,存储了所有 section 的信息。而 执行视图 有个 程序头部表(program header),存储了所有 segment 的信息,按照笔者的理解,它也可以称为 段表。两者的区别是在不同视图的不同表现。在下文中都简称为 段表(英语注释),读者可以根据语境判断使用的是哪种段表。
通常情况下,目标文件 包含以下几个常见的 section:
- 代码段(code section):存放编译后的 机器指令
- 数据段(data section):存放 初始化 的 全局变量 和 局部静态变量
- bss段(bss section):存放 未初始化 的 全局变量 和 局部静态变量,值得一提的是bss段 并不像 数据段 一样占据硬盘空间。由于 bss段 中变量的默认值为 0,所以 bss仅 记录这些变量的属性,不为这些变量开辟空间。只有等程序运行的时候,就会会 bss段 中的数据分配内存。
一般情况下,会将 数据段 和 bss段 统称为 数据段
代码段 和 数据段 为什么要分开存储?
- 增加程序安全性
- 使用cache提高访问速度
- 节省内存
2.2 ELF文件头
在 /usr/lib/elf.h 中,我们可以看到下面的数据结构,它描述的就是 ELF文件 的头部。
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;
举个例子,读取 ELF头 后输出如下:
图片中的输出与结构体是一一对应的:
- e_ident:对应上图中的红圈内的 5个 属性:
- Magic:ELF魔数,用于表示 ELF文件 的 平台属性。
- 1-4字节(7f 45 4c 46):ELF魔数,每个 文件 开头都为这 4个字节
- 5字节:表示 ELF文件位数。01 为 32位,02 为 64位
- 6字节:表示 大小端。01 为 小端,02 为 大端
- 7字节:表示 ELF主版本号。一般为 1。
- Magic:ELF魔数,用于表示 ELF文件 的 平台属性。
- e_type:ELF文件类型,即 可定位文件、可执行文件 和 共享目标文件
- e_machine:ELF文件 所于宁的机器平台,在图中可以看到为 ARM 。
- e_version:ELF版本号,一般为 1
- e_entry:如果为可执行文件,则表示 入口地址,否则该成员为 0
- e_phoff:program headers 在文件中的 偏移。
- e_shoff:段表(section table headers) 在文件中的 偏移
- e_flags:用于标识 ELF文件平台 的相关属性
- e_ehsize:ELF文件头 大小
- e_phentsize:一个 program header 的大小
- e_phnum:program header 的数量,一般就是指 segment 的数量
- e_shentsize:一个 section header 的大小
- e_shnum:section header 的数量,也就是 section 的数量。
- e_shstrndx:描述 section talbe 的 字符串段表 在 section table 中的下标。该字段一般指的是 .shstrtab 在段表中的下标
2.3 段表
段表(section table) 用于保存程序各个段的 基本属性。段表 在 ELF文件 中的位置由 ELF头 的 e_shoff 成员决定。
段表 本质是一个 Elf32_Shdr 结构体的数组,数组长度等于段的个数。每个 Elf32_Shdr 又被称为 段描述符(Section Descriptor)。一般来说,第一个元素的无效的段描述符,其类型为 NULL。如下图所示:
该结构体的代码如下:
typedef struct
{
Elf32_Word sh_name; /* Section name (string tbl index) */
Elf32_Word sh_type; /* Section type */
Elf32_Word sh_flags; /* Section flags */
Elf32_Addr sh_addr; /* Section virtual addr at execution */
Elf32_Off sh_offset; /* Section file offset */
Elf32_Word sh_size; /* Section size in bytes */
Elf32_Word sh_link; /* Link to another section */
Elf32_Word sh_info; /* Additional section information */
Elf32_Word sh_addralign; /* Section alignment */
Elf32_Word sh_entsize; /* Entry size if section holds table */
} Elf32_Shdr;
- sh_name:该成员是一个 下标,标识该段的段名在 .shstrtab(字符串表) 中的偏移。
- sh_type:段类型,一般有以下的类型:
类型 | 值 | 含义 |
---|---|---|
SHT_NULL | 0 | 无效段 |
SHT_PROGBITS | 1 | 程序段( 代码段 和 数据段 都是此类型) |
SHT_SYMTAB | 2 | 符号表段 |
SH_STRTAB | 3 | 字符串表段 |
SHT_RELA | 4 | 重定位段,该段包含了 重定位信息 |
SHT_HASH | 5 | 符号表段的哈希表段 |
SHT_DYNAMIC | 6 | 动态链接信息段 |
SH_NOTE | 7 | 提示性信息 |
SH_NOBITS | 8 | 该段在文件中无内容,比如bss段 |
SHT_REL | 9 | 该段也包含了 重定位信息 |
SHT_SHLIB | 10 | 保留 |
SHT_DNYSYM | 11 | 动态链接符号表段 |
- sh_flags:段标志,一般有以下标志:
标志 | 值 | 含义 |
---|---|---|
SHF_WRITE | 1 | 该段在进程空间中可写 |
SHF_ALLOC | 2 | 该段在进程空间中需要分配内存空间,一般 代码段、数据段和bss段 都有这种标志 |
SHF_EXECINSTR | 4 | 标识该段在进程空间中可以被执行,一般是 代码段 |
- sh_addr:段虚拟地址。如果该段能被加载到内存,则表示该段在内存中的起始虚拟地址。
- sh_offset:段偏移。如果该段存在于程序文件中,则表示该段在文件中的偏移。一般来说 BSS段 的该属性没有意义,因为其不占据文件空间
- sh_size:段长度
- sh_link | sh_info:段链接信息,对于需要链接的段(比如重定位段),这2个属性意义如下。如果段不需要进行链接则这2个属性没有意义。
段类型 | sh_link | sh_info |
---|---|---|
SHT_DYNAMIC | 该段所使用的的 字符串表 在段表中的 下标 | 无 |
SHT_HASH | 该段所使用的的 符号表 在段表中的 下标 | 无 |
SHT_REL SHT_RELA |
该段所使用的的 符号表 在段表中的 下标 | 该 重定位表 所作用的段在段表中的下标 |
其他 | 未定义 | 无 |
- sh_addralign:表示段是否有 地址对齐 要求,0无要求,1有要求。
- sh_entsize:该段中每一个固定项的长度,该属性为0则表示该段不包含大小固定的项。比如 符号表 中的每一个符号所占大小都一样,那么此时就需要 sh_entsize 来表示每一个符号(项)的大小。
2.4 重定位段
链接器 在处理目标文件时,必须要对目标文件中的某些部位进行 重定位, 即 代码段 和 数据段 使用 绝对地址 的地址。重定位信息记录在 重定位表 中,对于每个要重定位的 代码段 或 数据段 ,都有一个相应的 重定位表。比如代码段 .txt 对应 .rel.txt 这个重定位表。
每个 重定位表 就是 ELF文件 的一个段。后续文章会对 重定位表 进行讲解。
2.5 字符串表段
ELF文件 会使用到很多字符串,比如 段名、变量名 等。在 ELF文件 中,引用字符串则给出该字符串在 字符串表 中的偏移即可。
字符串表 在 ELF文件 中常常以 段 的形式存在。一般有以下2个:
- .strtab(字符串表):用于保存普通的字符串。比如 符号名
- .shstrtab(段表字符串标):用于保存段表中用到的字符串,最常见的就是 段名。
2.6 符号表
2.6.1 符号
在 链接 时,要解决的问题就是 目标文件 之间的 地址引用 问题,即对 函数 和 变量 地址的引用。每个函数和变量都有自己独一无二的名字,这样才能避免链接时混淆不同变量和函数。在链接中,将 函数 和 变量 统称为 符号,而 函数名 或 变量名 则是 符号名。
整个链接过程都是基于符号来完成,所以对于符号管理是链接时很关键的一个环节。每个 目标文件 都有自己的 符号表。符号表 记录了 目标文件 中所用到的所有符号。每个定义的符号都有一个值,即 符号值。一般来说,函数 和 变量 的 符号值 就是地址。
一般情况下,符号有以下几类:
- 定义在 本目标文件 中的全局符号
- 在本目标文件中引用到的 全局符号,却没有定义在 本目标文件中,这一般称为 外部符号
- 段名,其一般为编译器产生,其值为对应段的 起始地址
- 局部符号,一般只在编译单元内部可见。其在链接过程中也没有作用,一般链接过程也会忽略掉
- 行号信息,即 目标文件 与 源代码 中代码行的对应关系。
2.6.2 符号表
符号表 在 ELF文件 中就是一个 段,段名为 .symtab。在代码表现为 Elf32_Sym 结构体数组。 其代码结构体如下:
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
st_name:符号名,该成员表示该符号名在字符串表中的 下标。
-
st_value:符号对应的值,一般有如下几种情况:
- 在 非COMMON块 中,表示 符号 在 其所在段 中的偏移。
- 在 COMMON块 中,则表示该符号的 对齐属性
- 在 可执行文件 中,表示符号的 虚拟地址。
-
st_info:表示 符号类型(低4位) 和 绑定信息(高28位)
- 绑定信息:
宏定义名 值 说明 STB_LOCAL 0 局部符号,对于目标文件的外部不可见 STB_GLOBAL 1 全局符号,外部可见 STB_WEAK 2 弱引用 - 符号类型:
宏定义名 值 说明 STT_NOTYPE 0 未知类型符号 STT_OBJECT 1 该符号为 数据对象,比如变量、数组 ST__FUNC 2 符号是个函数或其他可执行代码 STT_SECTION 3 该符号表示一个段,此类符号必须是 STB_LOCAL STT_FILE 4 该符号表示文件名,一般都是目标文件所对应的 源文件名,一定是 STB_LOACL,且 符号所在段 必须为 SHN_AVS st_shndx:如果 符号 定义在本目标文件中,那么该成员表示 符号所在段 在 段表 中的下标。如果 符号 不是定义在本目标文件中或者其他情况,则如下表:
宏定义名 | 值 | 说明 |
---|---|---|
SHN_ABS | 0xfff1 | 符号包含一个绝对的值,比如 文件名符号 |
SHN_COMMON | 0xfff2 | 该符号表示 COMMON块 类型,比如 为初始化的全局变量 |
SHN_UNDEF | 0 | 表示符号未定义,该符号被本文件引用但定义在其他文件中 |
2.6.3 弱符号和强符号
在编程中常常碰到 符号重复定义,即多个 目标文件 中含有相同名字全局符号的定义。
一般情况下分为 弱符号 和 强符号:
- 弱符号:编译器默认 函数 和 初始化的全局变量 为 强符号。
- 强符号:未初始化的全局变量 为弱符号。
一般 GCC编译器 可以使用 attribute_((weak)) 来定义任意的强符号为 弱符号。强符号 和 弱符号 都是针对 定义 来说,不是针对符号的 引用。
一般链接器在处理 强符号 和 弱符号 时有如下规则:
- 不允许 强符号 被多次定义。
- 如果一个符号在某个目标文件中的 强符号,在其他文件中都是 弱符号,那么就选择 强符号。
- 如果一个符号在所有目标中都是 弱符号,那么选择占用空间最大的一个。
2.6.4 弱引用和强引用
- 强引用:如果没找到符号定义,链接器就会报符号 未定义错误。
- 弱引用:如果该符号有定义,则链接器将该符号的引用进行决议。如果该符号为定义,则链接器不报错。一般对于未定义的弱引用,链接器默认其为0,或者是一个特殊的值。
强引用 和 弱引用 对于库十分有用,如下:
- 库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数
- 程序可以对某些扩展工鞥模块的引用定义为弱引用,当我们将扩展模块与程序链接在一起时,功能模块就可以正常使用
- 如果我们去掉了某些功能模块,那么程序也可以正常链接,只是缺少了相应的功能