链接(linking)是将各种代码和数据片段收集并合并成为一个单一文件的过程,这是文件可被加载(复制)到内存并执行。
在某些GCC版本中,预处理器被集成到编译器驱动程序中。
gcc -Og -o prog main.c sum.c -v-O:是优化参数,可以是-Og便于调试,-O1轻度优化,-O2发布版本。-O3高性能需求-v:-verbose,详细显示编译过程。
7.2 静态链接
像Linux LD程序这样的静态链接器(static linker)以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出。输入的可重定位目标文件由各种不同的代码和数据节(section)组成,每一节都是一个连续的字节序列。指令在一节中,初始化了的全局变量在另一个节中,而未初始化的变量又在另外一个节中。
为了构造可执行文件,链接器必须完成两个主要任务:
符号解析(symbol resolution)。目标文件定义和引用符号, 每个符号对应于一个函数、一个全局变量或一个静态变量(即c语言中任何以static属性声明的变量)。符号解析的目的是将每个符号引用正好和一个符号定义关联起来。
重定位(relocation)。编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位条目(relocation entry)的详细指令,不加甄别地执行这样的重定位。
接下来的章节将更详细地描述这些任务。在你阅读的时候,要记住关于链接器的一些基本事实:
目标文件纯粹是字节块的集合。这些块中,有些包含程序代码,有些包含程序数据,而其他的则包含引导链接器和加载器的数据结构。
链接器将这些块连接起来,确定被连接块的运行时位置,并且修改代码和数据块中的各种位置。链接器对目标机器了解甚少。产生目标文件的编译器和汇编器已经完成了大部分工作
7.3 目标文件
目标文件由三种形式:
- 可重定位目标文件。包含二进制代码和数据,其形式可以在链接时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
- 可执行目标文件。包含二进制代码和数据,其形式可以被直接复制到内存并执行。
- 共享目标文件。一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并链接。
编译器和汇编器生成可重定位目标文件(包括共享目标文件)。链接器生成可执行目标文件。从技术上说,一个目标模块(object module)就是一个字节序列,而一个目标文件(object file)就是一个以文件形式存放在磁盘中的目标模块。不过,我们会互换地使用这些术语。
目标文件是按照特定的目标文件格式来组织的,各个系统的目标文件格式都不相同。
1.Windows使用可移植可执行(Portable Executable,PE)格式。
2.MacOS-X使用Mach-O格式。
3.现代x86-64 Linux和Unix系统使用可执行可链接格式(Executable and Linkable Format,ELF)。
尽管我们的讨论集中在ELF上,但是不管是那种格式,基本的概念是相似的。
7.4 可重定位目标文件
➜ 7.1 readelf -a prog
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)ELF可重定位目标文件的格式。ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。
ELF魔数是ELF(可执行与可链接格式)文件的开头四个字节:0x7F 0x45 0x4C 0x46,其中0x45 0x4C 0x46分别对应ASCII字符’E’、’L’、’F’,0x7F是DEL控制符,用于快速标识文件是ELF文件,操作系统加载时会校验这个魔数以确认文件类型。
第五个字节02代表版本,00 01 02分别代表无效,32位ELF,64位ELF。
第六个字节01表示字节序,00 01 02分别代表无效,小端,大端格式。
第七个字节01代表ELF版本
剩下九个字节,一般填0,有些平台会使用这9个字节作为扩展标志。
ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位,可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节的位置和大小是由节头部表表述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
夹在ELF头和节头部表之间的都是节。一个典型的ELF可重定位目标文件包含下面几个节:
.text节:已编译程序的机器代码。.rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表。.data:已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中。.bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分已初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为0。.symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上,每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。.rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。.rel.data:被模块引用或定义的所有全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。.debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。只有以-g选项调用编译器驱动程序时,才会得到这张表。.line:原始C程序中的行号和.text节中机器指令之间的映射。只有以-g选项调用编译器驱动程序时,才会得到这张表。.strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以null结尾的字符串的序列。
7.5 符号和符号表
每个可重定位目标模块m都有一个符号表,它包含m定义和引用的符号的信息。在链接器的上下文中,有三个种不同的符号:
- 由模块m定义并能被其他模块引用的全局符号。全局链接器符号对应于非静态的C函数和全局变量。
- 由其他模块定义并被模块m引用的全局符号。这些符号称为外部符号,对应于在其他模块中定义的非静态C函数和全局变量。
- 只被模块m定义和引用的局部符号。他们对应于带
static属性的C函数和全局变量。这些符号在模块m中任何位置都可见,但是不能被其他模块引用。
认识到本地链接器符号和本地程序变量不同是很重要的。.symtab中的符号表不包含对应于本地非静态程序变量的任何符号。这些符号在运行时在栈中被管理,链接器对此类符号不感兴趣。
有趣的是,定义为带有C static属性的本地过程变量是不在栈中管理的。相反,编译器在.data或.bss中为每个定义分配空间,并在符号表中创建一个有唯一名字的本地链接器符号。比如,假设在同一个模块中的两个函数各自定义了一个静态局部变量x;
int f()
{
static int x = 0;
return x;
}
int g()
{
static int x = 1;
return x;
}在这种情况中,编译器向汇编器输出两个不同名字的局部链接器符号。比如,它可以用x.1表示函数f中的定义,而用x.2表示函数g中的定义。
利用static属性隐藏变量和函数名字
符号表是由汇编器构造的,使用编译器输出到汇编语言.s文件中的符号。.symtab节中包含ELF符号表。这张符号表包含一个条目的数组。图7-4展示了每个条目的格式。
typedef struct elf64_sym {
Elf64_Word st_name; /* Symbol name, index in string tbl */
unsigned char st_info; /* Type and binding attributes */
unsigned char st_other; /* No defined meaning, 0 */
Elf64_Half st_shndx; /* Associated section index */
Elf64_Addr st_value; /* Value of the symbol */
Elf64_Xword st_size; /* Associated symbol size */
} Elf64_Sym;
评论(0)
暂无评论