0.简介
上一篇文章中我们了解了编译的具体流程,本文我们将开始去对编译生成的目标文件进行说明,介绍其中的核心结构,揭示其中各个部分的作用以及一些典型的问题。
1.整体结构介绍
目标文件一般和可执行文件采用一种格式存储,在linux中一般使用ELF(Executable Linkable Format)格式存储;Windows中一般采用PE(Portable Executable)格式存储,这两种格式都来源于COFF文件格式,本文主要探讨ELF格式文件,但原理都是类似的。ELF文件在Linux中有四种:可重定位文件(.o 我们主要介绍的内容)、可执行文件(直接可执行程序)、共享目标文件(.so)、核心转储文件(core dump)。
可重定位文件中整体结构可分为下面几个部分:
本文例子依旧使用test.c,只不过为了让test.c可以覆盖到我们的主要段,对其代码进行一些改写。
// test.c
#include <stdio.h>
int nInitData = 10;
int nUninitData;
void func(int nTmp)
{
printf("%d", nTmp);
}
int main()
{
static int nStaticInit = 111;
static int nStaticUninit;
int a = 99;
int b;
func(nInitData + nUninitData + nStaticInit + nStaticUninit + a + b);
return 0;
}
2.ELF文件头
ELF的文件头包含的内容如下,以32位为例:
typedef struct elf32_hdr{
unsigned char e_ident[EI_NIDENT]; // ELF文件标识字节,固定为0x7F 'E' 'L' 'F'
// [0-3]: 魔数(Magic Number),固定为0x7F, 0x45, 0x4C, 0x46
// [4]: 版本号(1=ELF32, 2=ELF64)
// [5]: 数据编码(1=小端, 2=大端)
// [6]: 文件格式版本(通常为1)
// [7-15]: 未使用或保留字段
Elf32_Half e_type; // 文件类型(2字节无符号整数)
// 0: 未知类型
// 1: 可重定位文件(.o)
// 2: 可执行文件
// 3: 共享目标文件(.so)
// 4: 核心转储文件(core dump)
Elf32_Half e_machine; // 目标机器架构(2字节)
// 3: i386架构
// 8: MIPS架构
// 0x28: ARM架构
// 0x3E: x86-64架构
// 0xB7: AArch64(ARM64)
Elf32_Word e_version; // ELF格式版本号(4字节)
// 1: 当前版本
Elf32_Addr e_entry; // 程序入口点虚拟地址(4字节)
// 可执行文件: 程序启动的第一条指令地址
// 可重定位文件: 通常为0
Elf32_Off e_phoff; // 程序头表(Program Header Table)的文件偏移(4字节)
// 可执行文件/共享库: 非零值
// 可重定位文件: 通常为0
Elf32_Off e_shoff; // 节头表(Section Header Table)的文件偏移(4字节)
// 无节头表时为0
Elf32_Word e_flags; // 与特定架构相关的标志位(4字节)
// i386: 0x00000000
// ARM: 0x05000000表示硬件浮点支持
Elf32_Half e_ehsize; // ELF文件头大小(2字节)
// 对于ELF32: 固定为52字节(0x34)
Elf32_Half e_phentsize; // 程序头表中每个条目的大小(2字节)
// ELF32: 固定为32字节(0x20)
Elf32_Half e_phnum; // 程序头表中的条目数量(2字节)
// 0表示无程序头表
Elf32_Half e_shentsize; // 节头表中每个条目的大小(2字节)
// ELF32: 固定为40字节(0x28)
Elf32_Half e_shnum; // 节头表中的条目数量(2字节)
// 0表示无节头表
Elf32_Half e_shstrndx; // 节名称字符串表所在的节索引(2字节)
// SHN_UNDEF(0): 无节名称表
// 其他值: 指向包含节名称的字符串表节
} Elf32_Ehdr;
我们使用gcc -c test.c来进行.o文件生成,然后使用readelf -h test.o查看其头部信息。
3.段表信息
接下来我们来看一下段表的信息,使用objdump -h test.o可以得到如下内容(主要的段):
段表的结构在内核代码中定义如下(32位为例):
typedef struct {
Elf32_Word p_type; // 段类型(PT_LOAD等)
Elf32_Off p_offset; // 段在文件中的偏移
Elf32_Addr p_vaddr; // 段在内存的虚拟地址
Elf32_Addr p_paddr; // 物理地址(通常等同vaddr)
Elf32_Word p_filesz; // 段在文件中的大小
Elf32_Word p_memsz; // 段在内存中的大小
Elf32_Word p_flags; // 权限标志(PF_R/W/X)
Elf32_Word p_align; // 内存对齐要求
} Elf32_Phdr;
4.各个段信息
了解了elf文件头和段表,我们来看一下各个段的内容和作用,还是使用objdump命令,本次使用objdump -s -d,-s打印所有段,-d按照反汇编打印,我们逐个段去看。
4.1 代码段(text)
代码段即可执行程序中的指令内容,其内容如下:
4.2 数据段(data)以及只读数据段(rodata)未初始化变量(bss)
数据段(data)存放初始化了的各全局变量或者局部静态变量的值,可以发现,两个值分别对应nInitData和nStaticInit:
只读数据段(rodata)存放的是只读数据(比如const修饰的变量或者常量字符,这个地方是%d:
未初始化变量段(bss)存放的是未初始化的全局变量和局部静态变量,对应上面代码中的应该是nUninitData和nStaticUninit。但是查看bss大小为4不是8。
此时去查看符号对应的段可以发现,nUninitData对应的Ndx是Com也就是comment段。
4.3 其他段
其他常见的段有debug段(存放调试信息)、dynamic段(动态链接信息)、strtab(字符串表)等,当然我们也可以自定义段,通过使用如下方式就可以自定义段,自定义段用处很多,比如将代码固定到某个内存区域、隔离敏感数据(设置内存保护属性)等。
__attribute__((section(".mysection")))
int my_var = 42;
4.4 查看程序所有段的方式
使用readelf -S test.o查看即可
5.链接相关内容介绍
链接相关的段主要有符号表、重定位表、字符串表、动态链接符号表和动态链接字符串表。
5.1 符号表
符号表用以记录文件中的定义和引用的符号(变量,函数),是链接器需要的核心结构,使用readelf -s test.o可以查看其符号表的信息,其具体字段含义如下。
1)名称(Name):符号的标识符(如 main、global_var)。
2)值(Value):符号的地址(在 .o 文件中通常是相对段的偏移量,链接后变为绝对地址)。
3)类型(Type):如 STT_FUNC(函数)、STT_OBJECT(变量)、STT_FILE(文件名)。
4)绑定(Binding):符号类型。
STB_GLOBAL:全局符号,可被其他文件引用(如非静态函数)。
STB_LOCAL:局部符号,仅在当前文件可见(如静态函数或变量)。
STB_WEAK:弱符号(如未定义的全局变量),优先级低于强符号。
5)所在段(Section Index):符号所属的段(如 .text、.data),未定义符号为 SHN_UNDEF。
其在内核中的表示结构为(以32位为例):
typedef struct elf32_sym {
// 符号名在字符串表中的索引(字节偏移量)
Elf32_Word st_name;
// 符号值(地址或常量值)
Elf32_Addr st_value;
// 符号大小(如函数长度、变量占用字节数)
Elf32_Word st_size;
// 符号类型和绑定属性(低4位为类型,高4位为绑定)
unsigned char st_info;
// 保留字段(未使用,通常为0)
unsigned char st_other;
// 符号关联的节索引(SHN_*常量或节区表头索引)
Elf32_Half st_shndx;
} Elf32_Sym;
5.2 重定位表
可以使用readelf -r test.o查看重定位表,其主要字段如下:
1)偏移量(Offset):需要修正的地址在段内的位置。
2)重定位类型(Type):指示如何修正地址,例如:
R_X86_64_PC32:32 位相对地址修正(如 PC 相对寻址的函数调用)。
R_X86_64_64:64 位绝对地址修正(如全局变量引用)。
其在内核中定义如下(64位为例):
typedef struct {
Elf64_Addr r_offset; // 需要修改的位置(段内偏移)
Elf64_Xword r_info; // 符号索引和重定位类型
Elf64_Sxword r_addend;// 附加常数
} Elf64_Rela;
5.3 其他相关段
其他段作用会在静态链接以及动态链接实际使用文章中详细说明。
6.典型链接问题分析
在上面介绍中我们可以看到未定义的符号以及各种符号类型(强弱,全局、本地),处理这些符号就是链接的主要作用之一。大部分链接错误的原因也就是这些符号的处理有问题。
6.1 "undefined reference" 错误
其可能原因如下:
1)缺少目标文件或库文件。
2)符号名拼写错误。
3)函数声明与定义不匹配(如参数类型不一致)。
4)静态库 (.a) 或动态库 (.so) 未正确链接。
我们可以使用nm -u命令查看未定义的符号,然后去查看是否正确链接,然后使用readelf查看对应的库是否导出了该符号,看是否是版本问题。
nm -u a.o # 查看未定义符号
readelf -Ws lib.so # 检查库是否导出该符号
6.2 重复定义错误
在上面介绍符号表时提到了符号的类型(强符号,弱符号等)两个强符号会报冲突,可能是多地定义,也可能是头文件没有使用#ifndef保护。
6.3 动态库找不到
可以在LD_LIBRARY_PATH环境变量中配置,也可以在编译命令中指定。
6.4 通用查看方法
可以使用如下命令生成链接的符号地址和段布局文件进行分析。
gcc -o test test.c -Wl,-Map=test.map # 生成链接地图文件
7.总结
本文介绍了目标文件的主要内容,同时简要提及了链接的一些问题。但静态链接究竟做了什么?为什么链接后程序会变大?静态库到底以什么形式存在?些将在下一篇文章(静态链接)中进行说明。