编译和连接:预编译(Prepressing)、编译(Compilation)、汇编(Assembly)、链接(Linking)
静态语言的C/C++模块之间通信有两种方式,一个是模块间的函数调用,另一个是模块间的变量访问。他们都需要知道目标函数或目标变量的地址,统一起来,就是模块间符号的引用。类似于拼图,定义符号的模块多出一块区域,引用符号的模块刚刚好缺少那块区域,两者结合就形成一个整体,这就是链接。
可执行文件格式
PC机上的可执行文件格式,Windows下面是PE(Portable Executable),Linux下面的ELF(Executable Linkable Format),他们都是COFF(Commom file format)格式的变种。
目标文件,就是源代码编译后,但未进行链接的那些中间文件(Windows下是.obj,Linux下是.o),他们和可执行文件的内容和结构很相似,所以都采用一个格式存储。
动态链接库(DLL,Dynamic Linking Library),Windows下是.dll,Linux下是.so
静态链接库(Static Linking library),Windows的.llib,Linux下的.a。
程序源代码编译后的机器指令被放在代码段(Code Section),常见命名有.code或者.text
已初始化的全局变量和局部静态变量经常放在数据段(Data Section),常见命名有.data
未初始化的全局变量和局部静态变量一般放在称为.bss段。(默认值都为0)
为什么要分开存放呢?
1. 保护代码段
2. 增加CPU缓冲(数据缓冲、指令缓冲)的命中率
2. 代码复用(代码区、资源区公用,数据区各自保存副本)
以一个简单的simplesection.c的例子来分析ELF文件格式:
objdump -h打印出目标文件的段信息。
CONTENTS属性表示该段在文件中存在。SIZE为段长度,FIle Off为段所在的位置。
.text段开头是ELF文件格式的头信息。占据长度为0x34。
size命令用于查看各个段的长度
objdump命令和readelf -h命令
GCC提供一个扩展命令, 在全局变量或函数之前加上 __attribute__((section(“name”)))的属性,可以把相应的变量或函数放在以”name”作为段名的段中。
ELF文件格式被设计成可以在多个平台下使用,这并不表示同一个ELF文件,可以在不同平台下使用(就将java的字节码文件),而是表示不同平台下的ELF文件,都遵循同一套ELF标准。
ELF文件中,各段的名字只在编译和链接时,才有意义。段的属性中,段的类型和段的标志位这两个属性最为重要。
段中的调试信息,ELF文件有DWARF标准的调试信息格式,现在已经发展到第三版,有DWARF标准委员会管理。Microsoft有它自己的调试信息格式标准,叫CodeView。值得一提的是,增加了调试信息的目标文件比程序本身大好几倍,当发布最终程序时,需要去掉这些调试信息。Linux下有strip工具来去除调试信息。
查看静态库中的工具,ar -t,readelf,objdump -t
静态链接
不同的输入目标文件,结合相关的库文件,经过链接器链接,形成一个可执行文件。
主要有两步步骤:空间与地址分配,符号解析与重定位。
空间与地址分配:扫描所有的输入目标文件,获取各个段长度,收集符号表到一个全局符号表中去。
符号解析与重定位:第一步收集到的信息,读取输入文件中的数据与重定位信息,进行符号解析与重定位,
VMA为虚拟地址,LMA为加载地址。一般情况下,两者值是相同的。但在某些嵌入式系统中,有些程序存在ROM中,VMA与LMA不一致,在加载时,需要指出。
在Linux下面,ELF可执行文件的默认从地址0x0804_8000开始分配的,一般程序的入口地址为_start,这个函数是Linux系统库(Glibc)中的一部分,当我们的程序与Glibc库链接在一起时,形成最终的可执行文件时,这个函数就是程序的初始化部分的入口地址。它在完成一系列初始化后,会调用main函数来执行程序的主体。当main函数执行完成之后,返回到初始化部分,进行一些后续的清理工作,然后结束进程。例如,C++的全局构造函数与解析函数就在main函数执行之前和之后进行执行的。
ABI与API
有没有可能,不同编译器编译出来的目标文件,经过链接后,可以形成一个可执行文件。
MSVC编译的目标文件时PE/COFF格式,GCC编译的是ELF格式,链接器必须同时认识这两种格式才行。
如果两个编译器编译出来的目标文件,能够相互链接,那么两个目标文件,必须满足下列条件:采用相同规定目标文件格式、相同的符号修饰标准、变量的内存分布方式、函数的调用方式等等,这一系列规范称之为ABI(Application Binary Interface)。
API和ABI都是应用程序接口标准,只是描述的接口层面不一样。
API是源码级别的接口,比如POSIX是一个API标准,Windows所规定的应用程序接口是一个API标准。举个例子,POSIX的printf函数,它(POSIX)能保证这个函数定义在所有POSIX标准的系统之间是一样的,但是它不保证printf在实际系统(Intel X86 和 MIPS 都装Linux系统)中执行时,不同入栈顺序、参数的堆栈分布等实际运行的二进制级别是相同的。
ABI是二进制层面的接口,比如C++的对象内存分布(Object Memory Layout)是C++ ABI的一部分。
静态链接库的组织,每一个功能有一个源文件来负责,比如,printf.o由printf.c负责。链接器链接时,是以目标文件为最小单位了,为了避免引入无用函数,每个功能都划分到以文件为单位,而不是以函数为单位,在最终结果中,节省空间浪费。
链接器提供的默认配置足够一般程序使用,在一些特殊要求的程序,比如操作系统内核、BIOS、嵌入式系统等等,则需要额外指出链接过程,如指定输出文件的各个段虚拟地址、段名称等等。链接器生成不同类型的文件时,使用不同的链接脚本。