从代码到可执行文件步骤(以gcc ,main.c为例)
1 .预处理(cpp) main.i
命令:gcc -E main.c -o main.i
2 .编译(cc1) main.s
命令:gcc -S main.i -o main.s
或gcc -S main.c -o main.s
3 .汇编(as) main.o 可重定位目标文件
命令:gcc -c main.s -o main.o
或gcc -c main.c -o main.o
4 .链接(ld)
将多个.o文件及一些必要的系统文件组合起来,创建一个可执行目标文件
命令:gcc -static -o myproc main.o test.o
将main.o,test.o链接成可执行目标文件myproc
链接主要任务
- 符号解析 将函数,全局变量,静态变量(static)的定义和引用关联起来
- 重定位 链接器通过把每个符号定义和一个存储器位置联系起来,修改所有对这些符号的引用,使得它们指向这个存储器位置,从而重定义这些节。
目标文件
1 .可重定位目标文件
包含二进制代码和数据,可以在编译时与其他可重定位目标文件合并,组成可执行文件。
2 . 可执行文件
包含二进制代码和数据,可直接拷贝到存储器中执行。
3 . 共享目标文件
特殊的可重定位目标文件,可在加载或运行时被动态加载到存储器,并链接。
可重定位目标文件
- ELF头,以一个16字节的序列开始,描述生成该文件的系统的字的大小和字节顺序。剩下字节包括帮助链接器语法分析和解释目标文件的信息,包括ELF头的大小、目标文件的类型(可重定位、可执行、共享的)、机器类型(如IA32)、节头部表的文件偏移,以及节头部表(见图最后)中的条目大小和数量。夹在ELF和节头部表中间的都是节。
- .text,已编译的机器代码。
- .rodata,read only data,只读数据,比如printf语句中的格式串和switch的跳转表。
- .data,已初始化的全局C变量。局部变量在运行时保存在栈中,既不在.data,也不在.bss中。
- .bss,未初始化的全局C变量。这个节不占据实际的磁盘空间。区分初始化和未初始化是为了空间效率。(意思是,.data磁盘实际保存的只有初始化的全局变量)
- .symtab,符号表,程序中定义和引用的函数和全局变量的信息。每个ELF文件都有。
- .rel.text,当链接噐把这个目标文件和其他文件结合时,.text节中的许多位置都需要修改。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面调用本地函数的指令则不需要修改。然而可执行目标文件中并不需要重定位信息,除非用户指定。
- .rel.data,被模块引用或定义的任何全局变量的重定位信息。一般而言,任何已被初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。
- .debug,调试符号表。条目是程序中定义的局部变量和类型定义,定义和引用的全局变量,原始的C源文件。
- .line,原始C源程序行号和.text节机器指令之间的映射关系。要求-g编译。
- .strtab,一个字符串表,每个字符串以null结尾。包括.symtab和.debug中的符号表,节头部中的节名字。
以下面代码main.c 为例,查看其生成的可重定位目标文件的ELF头
/* main.c */
/* $begin main */
int sum(int *a, int n);
int array[2] = {1, 2};
int main()
{
int val = sum(array, 2);
return val;
}
/* $end main */
命令:gcc -c main.c -o main.o
readelf -h main.o
运行结果如下:
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: REL (可重定位文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x0
程序头起点: 0 (bytes into file)
Start of section headers: 720 (bytes into file)
标志: 0x0
本头的大小: 64 (字节)
程序头大小: 0 (字节)
Number of program headers: 0
节头大小: 64 (字节)
节头数量: 12
字符串表索引节头: 11
Magic:45 4c 46分别是E L F三个字母的ASCII码
Class(类别):格式为64位
Data(数据):补码表示,按小端方式存放
Version(版本):1
OS/ABI: 表示操作系统的类型 这里是UNIX - System V
Type:ELF文件类型为可重定位文件REL
Machine:在64位机器上编译的目标代码
程序入口地址为0,是可重定位的文件类型,是链接式,不是执行式,不可运行
程序头起点:没有程序头表,偏移量为0
节头表起始位置:起始地址为720字节
本头的大小:ELF(这个头)节头大小为64字节
程序头大小:0
程序头的数目:0
节头大小:64字节
节头数量:12个表项
字符串表索引节头:节头表中第11项是字符串表
符号和符号表
每个可重定位目标模块m都有一个符号表(.symtab)
1.由m定义并能被其他模块引用的全局符号。 eg.非静态的C函数以及非静态的C全局变量。
2.只被m定义和引用的本地符号。 eg.带static的C函数和static全局变量。以及static局部变量。
3.由其他模块定义,并被m引用的全局符号,称为外部符号。 eg.定义在其他模块中的C函数和变量。
PS:在函数内部定义的static变量,不在栈中管理。而是在.data和.bss中为每个定义分配空间,并且在.symtab中创建一个名字唯一的本地符号。
符号解析
符号解析是链接的两个主要任务之一,方法是将每个引用和一个确切的定义联系起来。那如果多个目标文件同时定义了相同的符号怎么办?
- 强、弱符号
强符号:函数和已初始化的全局变量。
弱符号:未初始化的全局变量。 - Linix链接器处理多重定义的符号的规则:
1.不允许有多个强符号
2.一个强符号,多个弱符号,选强符号
3.多个弱符号,随便选一个
静态链接过程
1.链接器按照命令行输入顺序,从左到右扫描可重定位目标文件和存档文件(静态库)。
2.在此次扫描中,链接器维持一个可重定位目标文件的集合E(这个集合的文件会被合并起来形成可执行文件),一个未解析符号集合U(引用了但是尚未定义),以及一个已定义符号集合D(前面输入文件已定义)。刚开始E、U、D都是空的。
3.
- 对于命令行的每个输入文件f,先判断f是目标文件还是存档文件。目标文件直接把f加入E,然后根据f的内容修改U,D集合。
- 若f是存档文件,尝试匹配U中未解析的符号。如果某个存档文件成员m,定义了一个符号来解析U中的一个引用,那么就将m加入E中,并且根据m的内容来修改U,D集合。对存档文件的所有成员反复进行这个过程,直到U,D不再发生变化。然后继续处理下一个文件。
- 如果链接器完成了对所有输入文件的扫描后,U是非空的,那么链接器就会输出一个错误并终止。若U为空,则表明各个符号解析成功,它会合并和重定位E中的目标文件,输出可执行目标文件。
- 所以,一般将库放在命令行的结尾。若是有特殊的需求,比如循环引用,也可以在命令行上重复导入某个库。(出现这种情况,更好的办法应该是,这两个相互依赖的模块放在同一个.a存档文件中)。
重定位
- 重定位节和符号定义
这一步链接器将所有相同类型的节合并为同一类型的新的节。例如来自输入模块的.data节全部被合并成一个节,这个节成为输出的可执行目标文件中的.data节。这一步完成时,程序中的每一个指令和全局变量都有唯一的运行时存储器地址了。 - 重定位节中的符号引用
链接器修改代码节和数据节中对每个符号的引用,使它们指向正确的运行时地址。这一步依赖于代码和数据的重定位条目。.rel.text和.rel.data节。前者存放代码的重定位条目,后者存放已初始化数据的重定位条目。
可执行目标文件
ELF头部描述文件的总体格式。它还包括程序的入口点,也就是程序运行时第一条执行的指令地址。.text、.rodata、.data节和之前的可重定位目标文件中的对应节类似,只是它们已经被重定位到最终的运行时存储器地址。.init节定义了一个小函数_init,程序的初始化代码会调用它。由于可执行文件时完全链接(重定位过),因此不再需要.rel.text和.rel.data节。
以main.c和sum.c为例,查看可执行目标文件的elf头。
/* sum.c */
/* $begin sum */
int sum(int *a, int n)
{
int i, s = 0;
for (i = 0; i < n; i++) {
s += a[i];
}
return s;
}
/* $end sum */
命令:gcc -c main.c sum.c
gcc -static -o pro main.o sum.o
readelf -h pro
运行结果如下:
ELF 头:
Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - GNU
ABI 版本: 0
类型: EXEC (可执行文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x400a30
程序头起点: 64 (bytes into file)
Start of section headers: 842616 (bytes into file)
标志: 0x0
本头的大小: 64 (字节)
程序头大小: 56 (字节)
Number of program headers: 6
节头大小: 64 (字节)
节头数量: 33
字符串表索引节头: 32
区别:
Type:ELF文件类型为可执行文件EXEC
程序入口地址为0x400a30,是可执行的文件类型
程序头起点:偏移量为64
程序头大小:56B
动态链接共享库
- 即使使用静态库,加载时也会出现重复加载到存储器的浪费情况,比如C标准库。
- 共享库(shared library)也称共享目标(shared object),Unix中,通常用.so后缀表示。(静态库是.a)。Windows中用.dll文件表示(dynamic linking library)。
给定的文件系统中,对于一个库只能有一个.so文件;其次,在存储器中,一个共享库的.text节的一个副本可以被不同的运行进程共享。 - 链接时,不会有任何.so的代码和数据节被拷贝到可执行目标文件中,只拷贝了一些重定位和符号表信息,以便于运行时可以解析对.so中的代码和符号引用。
- 对共享库的引用有两种,一是在程序执行之前,被加载时,动态加载器加载和链接共享库;二是,在运行时通过动态加载器加载、链接特定的库,而无需再编译时链接那些库到应用中。