上一篇介绍了编译连接的过程,提到了目标文件是通过汇编过程生成的,最终链接生成可执行文件,这篇介绍一下目标文件里面到底有什么。
本文导图
格式概述
目标文件相对于最终的可执行文件而言,结构上已经和可执行文件的结构基本一样了。只是还没有经过链接过程,某些符号、地址还没被重定位,大部分内容都已经具备了。
可执行文件的格式在Windows(PE-Portable Executable)和Linux(ELF- Executable Linkable Format)、Mac(Mach-O)不同,前两者都是基于COFF(Common file format)格式的,除此之外还有其他不常见的,如Intel/Microsoft的OMF、Unix a.out、MS-Doc .Com格式等。
广义上将,目标文件与可执行文件格式几乎一样,可以看成同一种类型的文件。
动态链接库(window .dll、Linux .so)及静态链接库(window lib、Linux .a)、Mac(dylb,tbd)都是按照可执行文件的格式存储。静态库稍微不同,因为他是把很多目标文件集合在一起,需要在头部加上一些索引。也就是包含了很多目标文件的一个目标文件包。
ELF格式文件更可以做如下归纳(目标文件.o 静态库、可执行文件、动态库):
查看格式
file命令查看相应的文件格式。
Mac下
Linux下
下面以ELF结构作为分析
浅析目标文件内部结构
目标文件至少包含编译后的机器指令代码、数据、和链接所需要的信息比如、符号表、调试信息、字符串等,下面的内容围绕着这几个部分进行总结。
按照信息的不同属性以节(Section)也叫段(Segment)的形式存储,表示一个一定长度 的区域。这些区域里面分别存储了上诉编译后的信息。
- 代码段:编译之后的机器指令放在代码段(Code Section),一般是叫
.code
或.text
。 - 数据段:全局变量和静态变量数据放在数据段(Data Section),一般叫
.data
- BBS段:未初始化的全局变量、未初始化静态变量放在
.bbs
段。
具体来讲,下面代码与目标文件对应关系:
- 文件头:ELF文件有个文件头,描述整个文件的文件属性,如是否可执行、静态库还是动态库、及入口地址(可执行文件)、目标硬件、操作系统等。其中还包含一个段表,段表示描述各个段的一个数组,包含各个段在文件的偏移位置、段的属性。文件头后面就是各个段的内容,比如代码段保存指令,数据段保存数据。——其中段表非常重要。
未初始化的全局变量、局部静态变量虽然默认值都是0,但是没必要在
.data
段为它们分配空间(浪费),存放0是没必要的。所以放在.bbs
段,也就给他们预留个位置而已,没有内容,所以在文件中不占空间。
总体来说,程序代码被编译后分成两种段,程序指令(代码段)和程序数据(数据端及.BSS
)
指令和数据分开的好处:
- 指令和数据分别映射到两个虚拟区域,数据可读可写,而指令是只读的,可以设值区域的权限,防止恶意修改程序指令。——权限,安全
- 分开有利于程序的局部性(在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域。)CPU有几大的缓存体系。——局部性原理
- 复用,如果系统有多个程序的副本,那么指令都是一样的。这个也是动态库非常大的优势,节省内存开销——复用、复用!!!
实验目标文件内容
实验代码:
使用objdump -h 查看目标文件内部结构。
在Mac上实验结果
objdump -h hello.o
hello.o: file format Mach-O 64-bit x86-64
Sections:
Idx Name Size Address Type
0 __text 00000067 0000000000000000 TEXT
1 __data 00000008 0000000000000068 DATA
2 __cstring 00000004 0000000000000070 DATA
3 __bss 00000004 0000000000000120 BSS
4 __compact_unwind 00000040 0000000000000078 DATA
5 __eh_frame 00000068 00000000000000b8 DATA
在Linux实验结果
除了之前讲到的三个段,这里多了只读数据端,注释端,堆栈提示段。其中包含了一些属性关键词,如长度(size)、端的位置(File off)、该段是否在文件中存在(Contents)、段的各种属性(Alloc)。可以看到.bbs
段没有contents所以在文件中没有内容。而堆栈提示段size为0。
将上面的内容整理一下:
可以通过size命令查看可执行文件的各个端大小(注意dec所有段的十进制和,hex是16进制的和)
Mac上实验(文件格式是Mach-o)
Linux上实验(文件格式是ELF)
深入目标文件内部结构
下面将深入了解目标文件中的各个段,他们的作用及含义。
核心段
上面提到过核心段有代码段、数据端、BBS段。下面分别介绍!
代码段
代码段可以使用objdump的-s
参数打印出来,-d
可以将包含指令的段反汇编信息。
- 最左边是偏移地址,紧跟着的是16进制信息(每两个数字为一个字节,一般都是以字节位单位查看。)
Mac上实验(文件格式是Mach-o):最前面的部分是反汇编内容,接下来是各个段的内容。
$ objdump -s -d hello.o
hello.o: file format Mach-O 64-bit x86-64
Disassembly of section __TEXT,__text:
_fun1:
0: 55 pushq %rbp
1: 48 89 e5 movq %rsp, %rbp
4: 48 83 ec 10 subq $16, %rsp
8: 48 8d 05 61 00 00 00 leaq 97(%rip), %rax
f: 89 7d fc movl %edi, -4(%rbp)
12: 8b 75 fc movl -4(%rbp), %esi
15: 48 89 c7 movq %rax, %rdi
18: b0 00 movb $0, %al
1a: e8 00 00 00 00 callq 0 <_fun1+0x1F>
1f: 89 45 f8 movl %eax, -8(%rbp)
22: 48 83 c4 10 addq $16, %rsp
26: 5d popq %rbp
27: c3 retq
28: 0f 1f 84 00 00 00 00 00 nopl (%rax,%rax)
_main:
30: 55 pushq %rbp
31: 48 89 e5 movq %rsp, %rbp
34: 48 83 ec 10 subq $16, %rsp
38: c7 45 fc 00 00 00 00 movl $0, -4(%rbp)
3f: c7 45 f8 6f 00 00 00 movl $111, -8(%rbp)
46: 8b 05 00 00 00 00 movl (%rip), %eax
4c: 03 05 00 00 00 00 addl (%rip), %eax
52: 03 45 f8 addl -8(%rbp), %eax
55: 03 45 f4 addl -12(%rbp), %eax
58: 89 c7 movl %eax, %edi
5a: e8 00 00 00 00 callq 0 <_main+0x2F>
5f: 31 c0 xorl %eax, %eax
61: 48 83 c4 10 addq $16, %rsp
65: 5d popq %rbp
66: c3 retq
Contents of section __text:
0000 554889e5 4883ec10 488d0561 00000089 UH..H...H..a....
0010 7dfc8b75 fc4889c7 b000e800 00000089 }..u.H..........
0020 45f84883 c4105dc3 0f1f8400 00000000 E.H...].........
0030 554889e5 4883ec10 c745fc00 000000c7 UH..H....E......
0040 45f86f00 00008b05 00000000 03050000 E.o.............
0050 00000345 f80345f4 89c7e800 00000031 ...E..E........1
0060 c04883c4 105dc3 .H...].
Contents of section __data:
0068 54000000 55000000 T...U...
Contents of section __cstring:
0070 25640a00 %d..
Contents of section __bss:
<skipping contents of bss section at [0120, 0124)>
Contents of section __compact_unwind:
0078 00000000 00000000 28000000 00000001 ........(.......
0088 00000000 00000000 00000000 00000000 ................
0098 30000000 00000000 37000000 00000001 0.......7.......
00a8 00000000 00000000 00000000 00000000 ................
Contents of section __eh_frame:
00b8 14000000 00000000 017a5200 01781001 .........zR..x..
00c8 100c0708 90010000 24000000 1c000000 ........$.......
00d8 28ffffff ffffffff 28000000 00000000 (.......(.......
00e8 00410e10 8602430d 06000000 00000000 .A....C.........
00f8 24000000 44000000 30ffffff ffffffff $...D...0.......
0108 37000000 00000000 00410e10 8602430d 7........A....C.
0118 06000000 00000000 ........
# instanza @ InstanzadeMacBook-Pro in ~/Desktop/Temp [12:12:42]
$
Linux实验(文件格式是ELF)
-
contents of section .text
就是数据已十六进制打印出来的内容。中间4列是16进制内容,最右边是.text段的ASCII码形式。 -
.text
段包含是.c文件两个函数的指令,比如第一个字节是0x55,就是func1()函数第一条push %ebp
指令,最后一个0xc3代表main函数最后一个指令ret
数据段
.data
段保存了初始化的全局静态变量和局部静态变量。上面代码中有两个这样的变量,每个变量4个字节一共8个字节。所以.data
这个段大小为8个字节。——这是个非常准确的计算,不会存在多一个、少一个字节的情况
注意printf
的时候,有一个字符串常量“%d\n”
。在linux中放到了.rodata
段,分别有四个字符%、d、\n(换行符)、空字符
。一个字符一个字节(8位ASCII码),所以一共四个字节。在Mac上放到了字符串常量区。
以Mac下例子为例:
Contents of section __cstring:
0070 25640a00 %d..
通过查ASCII表,得到对应情况:%——25,d——64,\n(换行符)——0a,空字符(NULL)——00。刚好和字符串常量区对应
在来看一起.data
段(Mac下):
0060 c04883c4 105dc3 .H...].
Contents of section __data:
0068 54000000 55000000 T...U...
根据偏移范围可以知道.data
端一共8个字节。和前面用size看到的不一样,size看到的是12个字节。
Linux下
可以看到Linux也是8个字节。字节从低到高分别是0x54、0x00、0x00、0x00。刚好是十进制的84,特别注意这里的顺序,字节从低到高还是从高到底排列涉及到CPU的字节序问题,也就是大端小端。后面四个字节也刚好是85
BBS段
Mac中的bss段如下:
Contents of section __bss:
<skipping contents of bss section at [0120, 0124)>
可以知道具体的值放到了0120到0124之间,一共四个字节。
Linux下
同样也是四个字节。和global_uninit_var
、static_var2
合起来大小8个字节不符。
BBS段最终是通过符号表决定的。在Linux下只有static_var2存放到了bbs段。而global_uninit_var没有存放到任何段。只是一个未定义的common符号,这和语言及编译器实现有关。有一点可以明确——编译单元内部可见的静态变量是存在bbs段的。
其他段
ELF中除了.text、.data、.bbs
三个最常用的段还有其他段。具体如下:
以
.
开头的都是系统保留的。如果想要自定义端,则不能以.
开头。
GCC提供了扩展机制,可以将变量或者代码放到你指定的段中去。用以下方式实现:
以上只是ELF文件的轮廓,下面用一张图总结:
几点说明一下:
- 文件头:描述怎个文件的基本属性,文件版本,目标机器型号,程序入口地址。
- 段表:描述了ELF文件包含所有段的信息,比如每个段的段名,长度,文件中的偏移,读写权限,和其他属性。
下面开始从文件头开始
文件头
Linux用readelf来查看文件头,Mac用otool查看(otool -h hello.o
)
在Linux中
系统定义文件头数据结构:
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
各个字段代表什么意思根据名称就知道了。关键要知道在哪里定义这些常数的。ELF文件头定义在user/include/elf
。mach.o类型的目标文件也有对应的头文件。
下图是对readelf和ELF头文件定义的各个成员的含义解释。这里先大致了解下,后面会详解!
ELF魔数(e_ident前四个字节)
通过readelf之后看到最前面的16个字节刚好是e_ident,这16个ELF标准用来标识ELF文件平台属性,比如ELF字长(32位、64位),字节序、文件版本等。
下表是对e_ident数组各个成员的说明:
- 前四个字节是所有ELF文件必须相同的标识码:0x7F、0x45、0x4c、0x46。第一个字节赌赢DEL控制符,后三个对应ELF。所有的可执行文件最开始的几个字节都是魔数,用来确认文件类型,操作系统在加载的时候会确认魔数是否正确,不正确就不会加载。
- 下一个字节标识文件类型;0x01标识32,0x02标识64
- 第六个字节是字节序
- 第七个字节:规定ELF文本版本
- 后面9个字节:标准还没有定义,一般填0
文件类型
e_type表示文件类型,之前提到过3种ELF文件类型。如下表
机器类型
e_machine表示平台属性
段表
ELF最终段结构由段表决定,编译器、链接器和 装载器都是依赖段表来定位和访问各个端的属性。段表的位置有ELF文件头的e_shoff决定,如上面的例子段表的偏移为0x118。
直接来查看所有的段信息
这里的数据其实在代码层面来讲都是由对应的数据结构的。无论是mach.o还是ELF。比如这里的Elf32_Shdr结构体是对一个段的封装,段表也就是由Elf32_Shdr组成的数组。对上图而言一个有11个这样的元素。
段表元素数据结构
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_
各个字段的含义
现在把整个.o文件所有段的位置及长度都分析清楚了。
- 段表长度为0x1b8,一共440个字节,包含11个段描述符,每个段描述符为40个字节,这个长短刚好是结构Elf32_Shdr的长度。
- 文件最后一段.re.text结束后,长度为0x450也就是1104个字节,刚好是.o文件的大小。
中间有段表和.rel.text都因为对齐的原因,与前面的短之间有一个字节和两个字节的间隔。注意这里是为了对齐。
段类型(sh_type、sh_flags)
段名(sh_name)只有在编译、链接过程有意义,但不能真正表示段的类型,段名(sh_name)其实只是一个索引指向字符串表(SHT_STRTAB)的某个位置,在编译器和链接器中起作用的是段类型字段和段标志位字段。
段类型:
段标志位
系统保留段
段的链接信息(sh_link、sh_info)
段的类型与链接相关的话都需要sh_link、sh_info,无论是动态库还是静态库。比如重定位表,符号表。对于其他段这两个成员没意义
重定位表(段类型SHT_REL)
在刚才的段中,有一个.rel.text
的短,类型(sh_type)为SHT_REL
,也就是这个段包含的是重定位表。
链接器在处理目标文件的时候,需要对目标文件某些部分重定位,虽然在代码段和数据端中用的是绝对地址的引用位置。重定位信息都记录在重定位表里面,每一个需要重定位的代码段或数据段都会有一个相应的重定位表,比如上面的
.rel.text
就是对.text
的重定位表。因为.text
段有一个绝对地址的引用,就是printf函数的调用。这里的决定地址引用也就是写死的地址。而.data
段没有绝对地址引用。
字符串表(段类型SHT_STRTAB)
ELF用到很多字符串 ,如段名、变量名、因为字符串长度不确定,所以固定的解构表示比较困难。一种很常见做法就是把字符串集中放到一个表中,使用字符串的时候在表中的偏移来引用字符串。如下图
字符串表一般有两种一种用来保存普通的字符串,比如符号的名字;另一种段表字符串用来来保存段表中用到的字符串,比如段表名。字符串表中包含有若干个以’null’结尾的字符序列。
只要分析ELF头文件,就可以得到段表和段表字符串表的位置,从而解析整个ELF文件。
符号表(symbol table)
符号
假如B文件要用到A文件的函数foo,那么在目标文件A定义了函数foo,目标文件B引用了A中的foo函数。每个函数、变量都有自己的名字,将函数名、变量名统称为符号名。
每一个目标文件都会一个相应的符号表,记录了目标文件所要用到的符号,每个定义的符号有一个对应的值,叫做符号值,对于变量和函数来说,符号值就是他们的地址。出了变量和函数外还有其他几种符号,可以将符号表中的符号进行分类。
符号表包含的信息用于定位和重定位程序中的符号定义和引用。目标文件的其它部分通过一个符号在这个表中的索引值来使用 该符号。索引值从 0 开始计数,但值为 0 的表项(即第一项)并没有实际的意义, 它表示未定义的符号。这里用常量 STN_UNDEF 来表示未定义的符号。
符号一般有如下类型:
- 定义在目标文件的全局符号:可以被其他目标文件引用。如上面的func1,main,global_init_var
- 引用其他目标文件中的全局符号:一般叫做外部符号,如 printf
- 段名:由编译器产生,它的值就是该段的其实起始地址。如
.text
、.data
。 - 局部符号:只在编译单元内部可见,如static_var、static_var2。调试器可以是用哪个这些符号来分析程序。对于链接没什么作用,链接器也会忽略他们。
- 行号符号:目标文件指令与源代码中代码行的对应关系。
可以使用nm
查看符号表的内容
Mac上
Linux上
符号表结构(SHT_SYMTAB)
符号表示文件中的一个段,段名.symtab
。符号表数据结构比较简单Elf32_sym(符号表项),每个结构体对应一个符号。
其结构体如下:
typedef struct {
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
各个字段的含义
符号类型和属性(st_info)
由低4位表示符号类型,高28位表示符号属性信息。
符号所在段(st_shndx)
如果符号定义在本目标文件,则这个成员表示符号所载的段在段表中的下标。因为符号是为段而定义,在段中被引用。本数据成员即指明了相关联的段。本数据成员是一个索引值,它指向相关联的段在段表中的索引。在重定位过程中,段的位置会改变,本数据成员的值也随之改变,继续指向段的新位置。
如果没有定义在本目标文件,则sh_shndx有些特殊。
符号值(st_value)
每个符号都有一个对应的值。如果是一个变量或者函数,符号值就是其地址。总的来说有以下几种情况:
- 如果是符号定义且不是
Common块
类型(st_shndx不为SHN_COMMON),则st_value表示该符号在段中的偏移,即符号所对应的函数、变量位于st_shndx指定段。这种是最常见的,比如func1、main、global_init_var。 - 如果是COMMON块的类型,则st_value表示该符号的对齐属性,如global_uninit_var
- 在可执行文件中,st_value表示符号的虚拟地址。
符号表内容解析
内容如下:
- 第一列表示符号表数组下标,从0开始共15个符号
- 第二列就是符号值——st_value
- 第三列Size为符号大小——st_size
- 第四列、第五列分别为符号类型和绑定信息,对应st_info的低四位和高28位
- 第六列不知道
- 第七列表示符号所属的段——st_shndx
- 第八列表示符号名称
根据前面的内容做出如下解析:
- func1、main是函数所有在代码段,代码段Ndx为1,所以.text为1.并且类型是STT_Func,并且是全局可见,所以是STB——GLOBAL。
- global_uninit_var是一个SHN_COMMON类型的符号,本身并没有在BSS段。
- ....
- 依次类推可以把各个符号都解析出来
需要说明的:static_var和static_var2变成了static_var.1533和static_var.1534,是因为进行符号修正。其次绑定的属性是STB_LOCAL,表示只在编译单元可见。类型是STT_SECTION类型的符号,表示下标为Ndx段的短名。但是符号没有显示。比如2号符号Ndx为1那么就是.text段。那么符号名字就是.text
。可以使用objdump -t
查看段名符号。
特殊符号
当使用链接器链接的时候,链接器会定义很多特殊符号,这些特殊符号没有在程序定义,但是可以直接声明并应用它。链接器将这些特殊符号放在了链接脚本中,链接器会在程序最终链接为可执行文件的时候,将其解析为正确的值。
常见特殊符号
符号修饰、函数签名(防止冲突)
最开始编译器产生目标文件的时候,符号名和相应的变量函数名一样。但是如果已经定义这些符号就会产生目标文件冲突。为了解决目标文件冲突,就在对应的符号名签名加一些字符以示区分。如在符号名前、后加上_
。
如果模块较多,命名规范不严格,同样可能导致冲突。于是就增加了命名空间的方法来解决。
所以看到上面的static_var和static_var2变成了static_var.1533和static_var.1534。也就是进行了一次符号修饰。
C++符号修饰
C++强大而复杂,为了支持C++特性,发明了符号修饰和符号改编机制。
对于不同类,同名函数,引入了一个函数签名的概念。函数签名包含一个函数的信息,函数名,参数类型及所在的命名空间、其他细心,用于识别不同的函数。
编译器在链接处理符号的时候,会使用名称修饰的方法,使得每个函数签名对应一个修饰后的名称。如下图:
弱符号、强符号()
经常在编程中将一个符号重复定义,如果出现定义错误则说明这种事强符号。有些符号可以定义为弱符号,比如未初始化的全局变量,也可以使用__attribut__((weak))
定义一个强符号为弱符号。
它们的规则如下:
- 不允许强符号被多次定义,也就是不同目标文件不能有相当的强符号(iOS中经常出现的符号冲突就是这个意思)
- 如果符号在某个目标文件是强符号,其他文件是弱符号,那么选择强符号
- 如果在所有文件中都是弱符号则选择其中占空间最大的一个
弱引用、强引用
注意不是iOS中强弱引用
如果没有找到该符号的定义,链接器就会报未定义错误这种成为强引用。与之相对的是弱引用,如果是弱引用,链接器不认为是个错误,一般对未定义的弱引用,链接器默认为0,或者其他值,以便程序识别。在动态库中使用到,和COMMON块概念联系很紧密
可以使用__attribute__((weakref))
扩展自声明对一个外部函数为弱引用。如果把它编译为可执行文件,并不会报链接错误。但是当运行的时候,就会发生非法地址访问。
可以用if加以判断,防止这种情况。
这种弱符号和弱引用对于动态库来讲非常非常有用,比如动态库中定义的弱符号可以被用户定义的强符号覆盖,使得程序可以使用自定义版的库函数。
调试信息
目标文件还可能保存调试信息,比如在函数里面设置断点,可以监视变量变化,可以单步行进等,前提都是编译器必须提前将源代码与目标代码之间的关系(如目标代码中的地址对应源代码中的哪一行、函数和变量的类你先给,结构体的定义,字符串保存到目标文件里面)保存到目标文件。
如果在gcc 中加入-g
参数就可以在目标文件里面加上调试信息。如gcc -c -g hello.c
比如:
objdump -h hello.o
hello.o: file format Mach-O 64-bit x86-64
Sections:
Idx Name Size Address Type
0 __text 00000067 0000000000000000 TEXT
1 __data 00000008 0000000000000068 DATA
2 __cstring 00000004 0000000000000070 DATA
3 __bss 00000004 00000000000004e4 BSS
4 __debug_str 0000009e 0000000000000074 DATA
5 __debug_loc 00000000 0000000000000112 DATA
6 __debug_abbrev 00000087 0000000000000112 DATA
7 __debug_info 000000e0 0000000000000199 DATA
8 __debug_ranges 00000000 0000000000000279 DATA
9 __debug_macinfo 00000001 0000000000000279 DATA
10 __apple_names 000000c8 000000000000027a DATA
11 __apple_objc 00000024 0000000000000342 DATA
12 __apple_namespac 00000024 0000000000000366 DATA
13 __apple_types 00000047 000000000000038a DATA
14 __compact_unwind 00000040 00000000000003d8 DATA
15 __eh_frame 00000068 0000000000000418 DATA
16 __debug_line 00000062 0000000000000480 DATA
ELF采用一个DWARF的标准的调试信息格式。在Xcode中也有DWARF的身影。
调试信息在目标文件和可执行文件中占用很大的空间,往往比程序的代码和数据大很多倍(这一点在iOS开发中用Instrument调试的时候最终就是用得这个文件实现的。)
小结
这一部分内容比较多,主要是解析目标文件格式。如文件头、段、段表、重定位表、符号表、字符串表各自的数据结构是如何的。其实对ELF文件解析的内容内容远远不止这些,也有专门的官方文档对ELF格式记性详细的说明。
最重要的是明白各个部分的关系
扩展阅读
可执行文件格式
理解ELF文件格式
ELF文件格式分析
Comparison of executable file formats
MachOView