目标文件从结构上来说是编译后的可执行文件,只是没有经过链接的过程,也就是说其中可能有些符号或者地址还没有被调整,研究目标文件的内容对认识系统、了解背后的机理有很大的好处
符号用来表示一个地址,这个地址可能是某一段子程序的起始地址,也可以是一个变量的地址
目标文件的格式
目标文件与可执行文件的格式很相似,一般和可执行文件格式采取同一种格式存储,可执行文件格式在目前PC机下主要是Win下的PE和Linux的ELF,从广义上看,目标文件与可执行文件的格式其实几乎是一样的,在 Windows下,我们可以统称它们为PE-COFF文件格式。在Linux 下,我们可以将它们统称为ELF文件
不光是可执行文件( Windows 的.exe和 Linux下的ELF可执行文件)按照可执行文件格式存储。动态链接库(DLL,Dynamic Linking Library) ( Windows的.dll和 Linux 的.so)及静态链接库(Static Linking Library) ( Windows 的.lib和Linux 的.a)文件都按照可执行文件格式存储
对于ELF格式文件一共分为四类
ELF 文件类型 | 说明 | 实例 |
---|---|---|
可重定位文件(Reloatable File) | 这类文件包含了代码和数据,可以被用来连接成可执行文件或共享目标文件,静态链接库也可以归为这一类 | Linux的 .o 、Windows的 .obj |
可执行文件(Executable File) | 可以直接执行的程序,代表是ELF可执行文件,没有扩展名 | 比如 /bin/bash 文件、windows的 .exe |
共享目标文件(Shareed Object File) | 包含代码和数据。使用情况有两种,一种是链接器可以用它跟其他的可重定位文件和共享目标文件链接,产生新的目标文件。第二种是动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像的一部分运行起来 | Linux的.so ,如/lib/glibc-2.5.so 、Windows的DLL |
核心转存文件(Core Dump File) | 当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转存到核心转存文件 | Linux下的 core dump |
目标文件的内容
目标文件的大致内容至少要有编译后的机器代码指令、数据,根据前面的链接的作用应该知道还需要包含一些符号表、调试信息、字符串等等信息,目标文件会将这些信息按照不同的属性以段的方式存储
最经典的程序源代码编译后的机器指令经常放在代码段,而一些数据经常放在数据段,这在汇编语言中也是同样的
为什么要分为数据段和代码段?
- 数据和指令被存储到两个区域,对于数据段来说一般可读写,对于代码段一般只可读,增加了安全性防止指令被修改
- 将数据和代码分开有利于提高程序的局部性,而程序的局部性原理正是缓存的基本原理
- 当程序运行多个副本的时候,内存中只需要保存一份该程序的指令部分,这样将会大幅度的节省空间
下面可以看一个简单的代码编译成目标文件之后分段的结构
ELF文件的开头是一个文件头,描述了整个文件的文件属性,例如是否可执行、静态链接还是动态链接,还包含一个段表,段表其实是一个描述文件中各个段的数组,段表描述了文件中各个段在文件中的偏移位置以及段的属性
下面以一个程序例子观察目标文件各个段
int global_init_var = 84;
int global_uninit_var;
void func1(int i){
printf("%d\n", i);
}
int main(void){
static int static_var = 85;
static int static_var2;
int a = 1;
int b;
func1(static_var + static_var2 + a + b);
return a;
}
gcc -c SimpleSection.c
使用gcc -c得到了SimpleSection.o文件,objdump工具可以查看目标文件内部的结构
很显然的可以看出Size段表示该段的大小,FIle off表示偏移量,那么可以大概画出如下示意图
- .text:代码段
- .data:初始化的全局变量和局部变量保存在.data端
- .bss:未初始化的全局变量和局部静态变量默认值都为0的数据存储位置,.bss只是为未初始化的全局变量和局部静态变量预留位置,并没有内容也不占据空间
- .rodata:只读数据段
- .comment:注释信息段
- .note.GNU-stack:堆栈提示段
- .eh_frame:调试信息的一些端
下面在深入的探究上面一些比较重要的段
代码段.text
使用objdump工具可以查看各个段的内容,-s参数将所有端的内容以十六进制的方式打印,-d参数可以将所有包含指令的段反汇编,可以看到就是反汇编之后的代码和源程序是对应的关系
[root@iz2ze3u71xuet3hjszh72jz dushu]# objdump -s -d SimpleSection.o
SimpleSection.o: file format elf64-x86-64
Contents of section .text:
0000 554889e5 4883ec10 897dfc8b 45fc89c6 UH..H....}..E...
0010 bf000000 00b80000 0000e800 000000c9 ................
0020 c3554889 e54883ec 10c745fc 01000000 .UH..H....E.....
0030 8b150000 00008b05 00000000 01c28b45 ...............E
0040 fc01c28b
Disassembly of section .text:
0000000000000000 <func1>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 89 7d fc mov %edi,-0x4(%rbp)
b: 8b 45 fc mov -0x4(%rbp),%eax
e: 89 c6 mov %eax,%esi
10: bf 00 00 00 00 mov $0x0,%edi
15: b8 00 00 00 00 mov $0x0,%eax
1a: e8 00 00 00 00 callq 1f <func1+0x1f>
1f: c9 leaveq
20: c3 retq
0000000000000021 <main>:
21: 55 push %rbp
22: 48 89 e5 mov %rsp,%rbp
25: 48 83 ec 10 sub $0x10,%rsp
29: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)
30: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 36 <main+0x15>
36: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 3c <main+0x1b>
3c: 01 c2 add %eax,%edx
3e: 8b 45 fc mov -0x4(%rbp),%eax
41: 01 c2 add %eax,%edx
43: 8b 45 f8 mov -0x8(%rbp),%eax
46: 01 d0 add %edx,%eax
48: 89 c7 mov %eax,%edi
4a: e8 00 00 00 00 callq 4f <main+0x2e>
4f: 8b 45 fc mov -0x4(%rbp),%eax
52: c9 leaveq
53: c3 retq
数据段和只读数据段
.data段保存的是那些已经初始化了的全局静态变量和局部静态变量,在SimpleSection.c代码里面一共有两个这样的变量,两个变量每个变量4个字节,一共刚好8字节
SimpleSection.c里面在调用printf的时候,用到了一个字符串常量"%d\n",是一种只读数据所以被放到了.rodata段,所以.rodata是只读数据段,这种只读数据段在语义上支持了C++的const关键字
.data和.rodata的内容为
Contents of section .data:
0000 54000000 55000000 T...U...
Contents of section .rodata:
0000 25640a00 %d..
0x54 0x00 0x00 0x00而这刚好是十进制下的global_init_var的值84
BSS段
.bss段存放的是为初始化的全局变量和局部静态变量,如上面的global_uninit_var、static_var2,但是它的大小却只有4个字节,与两个变量8字节不符合,这是与编译器有关系,有的编译器会将全局的未初始化的变量存放在目标文件的.bss段,有的则不放只是预留一个未定义的全局变量符号,等到最终链接成可执行文件的时候再在.bss端分配空间
其他段
ELF文件结构描述
通过前面已经了解了ELF得大致结构,如下图
那么我们还有ELF文件头没有分析,ELF文件头包含了描述整个文件的基本属性,例如ELF版本、目标机器型号、程序入口地址等等,其中与段有关的重要结构便是段表,该表描述了ELF文件包含的所有段的信息,例如段名、段的长度、在文件中的偏移等等
文件头
使用readelf可以查看文件头的一些信息,ELF文件头结构及相关的常数被定义在"/usr/include/elf.h"里面,ELF文件有32位和64位版本,文件头结构分别叫做"Elf32_Ehdr"和"Elf64_Ehdr",前面的类型是ELF做的一套通用的类型
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file version */
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;
这个结构体定义的内容与输出结果基本是一一对应的,e_ident数组对应了5个参数,各个参数的对应可看下表
段表
段表是保存了这些段的基本属性的结构,段表在ELF文件中的位置由ELF文件头的e_shoff成员决定换算成十六进制也就是0x420,前面使用objdump -h查看了ELF文件中包含的端,实际的情况不一样,objdump只是把关键的段显示了出来,但是还有一些其他辅助型的段,可以使用readelf工具来查看ELF文件的段
-W参数只是让它一行显示
同样的在elf.h也同样有保存段表的结构体:
typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Section size in bytes */
Elf64_Word sh_link; /* Link to another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;
可以看到结构体中的字段与readelf中输出的结果是一一对应的,到这里有一个疑惑,就是SimpleSection.o文件的大小是1888字节,而readelf下来最后的那个段的字节数是0x3b8 + 0x61算下来是1049,这为什么不相符?
sh_type段的类型
sh_flag段的标志位
段的链接信息(sh_link、sh_info) 如果段的类型是与链接相关的(不论是动态链接或静态链接),比如重定位表、符号表等,那么 sh_link 和 sh_info 这两个成员所包含的意义如下表所示。对于其他类型的段,这两个成员没有意义
符号
在本文开篇的时候就已经介绍过,符号其实是函数和变量的统称,而函数名或者变量名就是符号名,在链接的过程中符号是最关键的一部分,所以要对符号进行管理,每一个目标文件都会有一个相应的符号表,这个表里记录了目标文件所用到的所有符号,而符号一共有以下几种类型:
- 定义在本目标文件的全局符号,可以被其他目标文件引用。比如 SimpleSection.o里面的:”funcl“、"“main”和“global_init_var”
- 在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫做外部符号(External Symbol),也就是我们前面所讲的符号引用。比如 SimpleSection.o 里面的“printf”
- 段名,这种符号往往由编译器产生,它的值就是该段的起始地址。比如 SimpleSection.o里面的“.text”、“".data”等
- 局部符号,这类符号只在编译单元内都可见。比如 SimpleSection.o里面的“static_var”和“static_var2”。调试器可以使用这些符号来分析程序或崩溃时的核心转储文件
- 行号信息,即目标文件指令与源代码中代码行的对应关系,它也是可选的
可以使用nm查看目标文件所具有的符号
符号表的结构
ELF文件中的符号表一般是文件中的一个段,一般是".symtab",同样的对应一个结构体
typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
st_info 符号类型和绑定信息
该成员高位表示符号绑定信息,低位表示符号的类型
st_index 符号所在段
符号所在段(st_shndx)如果符号定义在本目标文件中,那么这个成员表示符号所在的段在段表中的下标;但是如果符号不是定义在本日标文件中,或者对于有些特殊符号,sh.shndx的值有些特殊,如表3-17所示
st_value 符号值
符号值一般有以下几种情况:
- 在目标文件中,如果符号的定义并且该该符号不是“COMMON”块类型的,则st_value表示该符号在段中的偏移
- 在目标文件中,并且是“COMMON”块类型的,表示对其属性
- 在可执行文件中,st_value表示符号的虚拟地址
可以使用readelf工具来查看目标文件的符号
这些内容与上面结构体中的字段一一对应,但是有些奇怪的是Name一栏,static_var.1731和static_var2.1732这个名字与我们定义的不相符,其实也可以简单的明白其实就是为了防止变量名称之间冲突
强符号与弱符号
我们都很清楚当遇到符号重复定义的时候编译器会报出错误,这些符号被称为强符号,就是我们一般写的函数或者变量都是强符号,对于c/c++来说编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号
在gcc中可以通过__attribute__((weakref))
这个扩展关键字来声明对一个外部函数的引用为弱引用,例如:
void __attribute__ ((weak)) foo();
int main(){
foo();
}
在链接之前使用gcc -c生成目标代码并不会报错,但是当运行这个可执行文件时会发生运行错误,因为当main函数试图调用foo函数的时候,foo函数的地址为0所以发生非法地址访问的错误,改进是加上if判断
void __attribute__((weak)) f();
int main(void)
{
if (f) f();
return 0;
}
这种弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数;或者程序可以对某些扩展功能模块的引用定义为弱引用,当我们将扩展模块与程序链接在一起时,功能模块就可以正常使用;如果我们去掉了某些功能模块,那么程序也可以正常链接,只是缺少了相应的功能,这使得程序的功能更加容易裁剪和组合。
数的地址为0所以发生非法地址访问的错误,改进是加上if判断
void __attribute__((weak)) f();
int main(void)
{
if (f) f();
return 0;
}
这种弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数;或者程序可以对某些扩展功能模块的引用定义为弱引用,当我们将扩展模块与程序链接在一起时,功能模块就可以正常使用;如果我们去掉了某些功能模块,那么程序也可以正常链接,只是缺少了相应的功能,这使得程序的功能更加容易裁剪和组合。