编译器编译源码生成的文件就叫目标文件,那目标文件里面到底长什么样?
目标文件从结构上将,就是编译后的可执行文件格式,只是没有经过链接的过程。这两种文件和操作系统、编译器密切相关
可执行文件格式涵盖了程序的编译、链接、装载和执行的各个方面,了解它的结构并深入剖析对理解系统、机理大有好处
可执行文件主要有Windows的PE和Linux的ELF,目标文件如前所说,是未进行链接的中间文件(如Windows的.obj
和Linux的.o
)。我们可以将其看做同一个类型的文件
除此之外,动态链接库(DLL,如Windows的.dll
和Linux的.so
)和静态链接库(Windows的.lib
和Linux的.a
)都按照可执行文件格式存储
但如果要细分这些类型文件,则
- 可执行文件:如Windows的
.exe
- 共享目标文件:如Windows的
.dll
和Linux的.so
,一种链接器可以将其和其他可重定位文件和共享目标文件链接产生新的目标文件;第二种动态链接器将其与可执行文件结合,成为进程镜像的一部分 - 可重定位文件:如Windows的
.obj
和Linux的.o
,可以被链接成可执行文件或共享目标文件 - 核心转储文件:当进程意外终止,系统将进程的地址空间的内容和一些终止信息转储到该文件
目标文件
一般按照信息的不同属性,以节(Section)或段(Segment)的形式存储。这里有个问题,在BIOS代码中,段和节是由区别的,而且是包含的关系,这怎么理解???1
2
3
4
5
6
7
8
9
10void
readseg(uint32_t pa, uint32_t count, uint32_t offset)
{
...
while (pa < end_pa) {
readsect((uint8_t*) pa, offset);
...
}
}
一般机器指令放在代码段(.code
或.text
),全局变量和局部静态变量放在数据段中(.data
)
数据和指令分开放置的好处:
- 数据和指令在装载后,会被映射到两个虚拟内存区域,这两个区域可以设置不同的读写权限,防止指令被修改
- 指令和数据分离有利于提高程序的局部性,进而提高缓存的命中率
- 指令共享,当系统运行多个该程序的副本时,内存中只需要保存一份该程序的指令,这将节省大量空间
用例子说话1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22int printf(const char * format,...);
int global_init_var = 84;
int global_uninit_var;
void func1(int i)
{
printf("%d\n",i);
}
int main()
{
static int static_var = 85;
static int static_var2;
int a = 1;
int b;
func1(static_var + static_var2 + a + b);
return a;
}
使用命令objdump -h simplesection.o
来查看ELF文件各个段的信息1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18simplesection.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000057 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 0000000000000000 0000000000000000 00000098 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 0000000000000000 0000000000000000 000000a0 2**2
ALLOC
3 .rodata 00000004 0000000000000000 0000000000000000 000000a0 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 0000002b 0000000000000000 0000000000000000 000000a4 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000cf 2**0
CONTENTS, READONLY
6 .eh_frame 00000058 0000000000000000 0000000000000000 000000d0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
Size
很好理解;File off
指示了段的偏移量,即段所在位置;CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
则表示段的属性,只有标记了CONTENTS
才是在文件中实际存在的
使用参数-s
可以段的内容以十六进制展现出来,-d
可以将所有包含指令的段反汇编
1 | simplesection.o: file format elf64-x86-64 |
接下来一段段分析
代码段
1 | Contents of section .text: |
最左边是偏移量,因此总共0x57
个字节,和上述信息吻合
然后去对照下面的汇编指令,发现一模一样!!!
数据段
.data
保存的是初始化的全局静态变量和局部静态变量,也就是static_var
global_init_var
两个变量
.rodata
保存的是只读数据(read only,如const
修饰的变量和字符串变量),对应源代码中的%d\n
。单独设立该段的好处:在语义上支持了const
,OS在加载的时候可以将该段映射到可读的,保证安全性??
54000000 55000000
也可以看出机器的大端的
BSS段
.bss
存放未初始化的全局变量和局部静态变量,但有些编译器会将此类变量放在.bss
段中,有些则不存放,只是预留一个未定义的全局变量符号
其他段
这些段和其他一些功能有关,先不介绍
我们也可以自定义段,让变量或者代码能放到指定的段中,以实现特定的功能,如为了满足某些硬件的内存和I/O的地址布局,或Linux内核用来完成一些初始化和用户空间复制时出现页错误???
GCC提供__attribute__((section("name")))
属性来将变量和函数放到name
段中
ELF文件
ELF文件的开始是一个File Header,它描述了文件的属性,如文件是否可执行、是静态链接还是动态链接、入口地址、目标硬件、目标OS等信息,除此之外,还有一个Section Table,描述接下来各个段的信息,如各个段在文件的偏移量、段的属性
学习ELF最好的方法就是直接看源码定义,在/usr/include/elf.h
,头文件定义分为32位和64位,我们以32位为准
文件头
1 | /* The ELF file header. This appears at the start of every ELF file. */ |
虽然下图是64位可执行文件的文件头,但是依旧可以寻找一一对应关系
关于魔数还有个小故事,这些故事归结起来就是马屁股和航天飞机,也就是经济学中的路径依赖,映射到互联网就是培养用户习惯,在我们的生活中,处处都是路径依赖,因为事务总不能脱离历史发展,那么有办法摆脱路径依赖,或者有办法找到好的路径吗?
段表
段表描述了ELF文件的每个段的信息,如段名、长度、偏移、读写权利等,编译器、链接器和装载器都是依靠段表来定位和访问每个段
1 | /* Section header. */ |
段的名字只在链接和编译期间有意义,对操作系统没有意义
重定位表
重定位表也是ELF的一个段,对于需要重定位的代码段或数据段,都会有一个相应的重定位表
字符串表
往往把长度不定的字符串集中起来存放,然后使用字符串在表中的偏移来引用字符串
链接的接口:符号
链接的本质就是不同目标文件得到结合,这些目标文件之间必须要固定的规则,才能像积木一样结合在一起
在链接中,将函数和变量统称为符号,符号是链接中的粘合剂,整个链接都是基于符号完成的
每个目标文件都有一个符号表,这个表记录了目标文件中的所有符号,每个符号对应的值,称为符号值,对于变量和函数,符号值就是地址。符号也有分类:
- 定义在目标文件的全局符号,可以被其他目标文件引用
- 定义在其他目标文件的全局符号
- 段名,由编译器产生
- 局部符号
- 行号信息
我们只关心全局符号,其他符号对于链接过程都是无关紧要的,readelf
objdump
nm
都能查看符号表
符号表就是一个elf32_sym结构的数组,每个元素对应一个符号1
2
3
4
5
6
7
8
9
10
11/* Symbol table entry. */
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;
每个元素都会有相关的宏定义,代表不同的含义,具体参考elf.h
特殊符号:在使用ld作为链接器来生成可执行文件时,会定义很多特殊符号,这些符号被定义在ld链接器的链接脚本中。链接器会在程序最终链接成可执行文件的时候将其解析成正确的值,如_executable_start
表示程序起始地址 _etext
表示代码段结束地址等
编译器默认函数和初始化的全局变量为强符号,未初始化的全局变量为弱符号,也可以通过GCC的__attribute__((weak))
定义任何一个强符号为弱符号,且对于强弱符号有以下规则
- 强符号不能重复定义
- 优先选择强符号定义
- 都是弱符号定义时,选择占用空间最大的那个
目标文件会引用定义在其他目标文件中的符号,并在最终链接成可执行文件,如果没有找到该符号定义,就会报错的符号引用称为强引用,反之则为弱引用,可通过__attribute__((weakref))
声明对一个外部引用为弱引用