编译过程
在linux系统下,使用gcc可以完成整个编译过程,gcc是什么?
它并不是一个编译器,而是一个驱动程序。编译过程中每个环节由具体的组件负责,编译过程由cc1负责、汇编过程由as汇编器负责、链接过程由ld负责。
- 软件构建过程通常分为4个阶段:预编译(预处理)、编译、汇编、链接。
预编译(预处理)
我们在编译程序时可以通过加 -E选项告诉编译器仅作预处理
gcc -E xxx.c -o xxx.i
(1)文件包含
文件包含命令指示预处理器将源文件的内容全部复制到当前源文件中。
(2)宏定义
宏可以提高程序的通用性和易读性,减少不一致性和输入错误,便于维护。
在预处理过程中,预编器将宏名替换为具体的值。
(3)条件编译
在大多数情况下,源程序中所有的语句都参加编译,但有的时候用户希望按照一定的条件去编译源程序的不同部分,这时可以使用条件编译。
编译
编译程序对预处理过的结果进行词法分析、语法分析、语义分析,然后产生中间代码,并对中间代码进行优化,目标是使最终生成的可执行代码执行时间更短、占用的空间更小,最后生成相应的汇编代码。
可以使用gcc -S xxx.c指定编译过程只进行编译,不进行汇编和链接。
frame pointer和base pointer均指向栈帧的底部,在IA32(Intel Architecture 32bit)中,通常使用寄存器ebp保存这个位置。在汇编语言中,在函数的开头和结尾处分别会插入一小段代码,分别称为Prologue和Epilogue。
Prologue保存主调函数的frame pointer,这是为了在子函数调用结束后,恢复主调函数的栈帧。同时为子函数准备栈帧。其主要操作包括:
1.保存主调函数的frame pointer,将保存在寄存器ebp的frame pointer压栈。在退出子函数时可以从栈中恢复主调函数的frame pointer。
2.将esp赋值给ebp,即将子函数的frame pointer指向主调函数的栈顶。
3.修改栈顶指针esp,为子函数的本地变量分配栈空间。main函数并不是程序中第一个调用的函数,main函数也是一个被调函数,其也有栈帧。程序中第一个被调用的函数_start也会自己模拟一个栈帧。
Epilogue功能与Prologue相反,其主要操作包括:
- 将栈指针esp指向当前子函数的栈帧的frame pointer,也就是说,指向当前栈帧的栈底,而这个位置,恰好是Prologue保存的主调函数的frame pointer。然后,通过指令pop将主调函数的frame pointer弹出到ebp中。
2.将调用子函数时call指令压栈的返回地址从栈顶pop到EIP中,并跳转到EIP处继续执行。如此,CPU就返回到主调函数继续执行。
汇编
- 汇编器将汇编代码翻译为机器指令,汇编器的汇编过程相对编译器比较简单。汇编器的工作除了生成机器码外,汇编器还要在目标文件中创建辅助链接时需要的信息,包括符号表、重定位表。
链接
- 链接是编译过程的最后一个阶段,链接器将一个或者多个目标文件和库,包含动态库和静态库,链接为一个单独的文件。链接器的工作可以分为两个阶段:
- 第一阶段是将多个文件合并为一个单独的文件。对于可执行文件,还需要为指令及符号分配运行时地址。
- 进行符号重定位。
如果在链接过程中有静态库,在链接静态库时,并不是将整个静态库中包含的目标文件全部复制一份到最终的可执行文件中,而是仅仅链接库中使用的目标文件。
动态库在可执行文件中不会有任何副本
1. 动态加载库需要知道可执行程序依赖的动态库,这样在加载可执行程序时才能加载其依赖的动态库。所以,在链接时,链接器将根据可执行程序引用的动态库中的符号的情况在dynamic段中记录可执行程序所依赖的动态库。
2.链接器需要在重定位表中创建重定位记录,这样当动态链接器加载动态库时,将依据重定位记录动态库引用的这些外部符号。