Compile
编译是将源代码变成可执行程序的过程,这个过程可以分为以下步骤:
- 预处理
- 编译
- 汇编
- 链接
下面以gcc
为例解释每个阶段
预处理阶段
预处理阶段主要是处理源代码中以’#’开始的预处理指令,将其转换后直接插入程序文本中,得到另一个C源码,通常以”.i”作为文件扩展名。
例如此时有一个文件 hello.c
:
1 |
|
预处理命令:
1 | gcc -E hello.c -o hello.i |
简单来说预处理阶段的一些处理规则如下:
- 递归处理
#include
命令,将对应文件的内容复制到该指令所在的位置 - 删除所有的
#define
命令,并替换为对应的宏 - 处理所有的条件预处理指令,比如”
#if
,#ifdef
,#endif
等等 - 删除所有注释
- 添加行号和文件名标识
编译阶段
编译阶段会进行一系列的词法分析,语法分析,语义分析和优化,将C源码编译成汇编码。该步骤的操作对象可以是源代码
hello.c
,也可以是预处理后的代码hello.i
,因为实际上gcc将预处理阶段合并到了编译阶段之中。值得注意的是,实际上在编译器进行编译的过程中,不仅仅是简单地把 C 源码写成了汇编码,同时还做了很多的优化和包装,进行了很多的复杂任务。
编译阶段的指令是:
1 | gcc -S hello.c -o hello.s |
第二行代码中的-masm
指定了生成的汇编码是intel格式,而-fno-asynchromous-unwind-tables
用与生成没有cfi
宏的汇编码
生成的hello.s
:
1 | .file "hello.c" |
此处可以看到printf
编译后被替换成了puts
,这是因为printf
在只有一个参数的时候作用跟puts
差不多,所以为了节省性能就直接替换成了puts
补充知识:cfi宏
(Call Frame Information)
cfi
宏是一组用于生成调试信息的伪指令,主要用于生成栈帧调试信息,以便调试器能够还原函数调用栈,尤其是在函数发生异常、崩溃或者中断时。
常见cfi
宏指令:
.cfi_startproc
: 表示函数开始,开启帧信息记录.cfi_endproc
: 表示函数结束,关闭帧信息记录.cfi_def_cfa reg, offset
: 定义 CFA(Canonical Frame Address)为reg + offset
.cfi_def_cfa_offset, offset
: 修改当前 CFA 的偏移.cfi_def_cfa_register reg
: 修改当前 CFA 的基寄存器.cfi_offset reg, offset
: 表示某个寄存器保存到了栈的哪一偏移处.cfi_restore reg
: 恢复寄存器的状态(与 .cfi_offset 相反).cfi_adjust_cfa_offset n
: CFA 偏移增加/减少 n 字节
CFA
(Canonical Frame Address) 是什么
CFA
是标准帧地址,通常是一个栈帧的参考点,调试器通过它来找出:
- 返回地址在哪里
- 参数、局部变量、保存的寄存器在哪里
汇编阶段
在汇编阶段,汇编器根据汇编指令跟机器码的对照表进行翻译,将
hello.s
汇编成目标文件hello.o
汇编命令:
1 | gcc -c hello.s -o hello.o |
汇编的源码既可以是汇编码hello.s
,也可以是源代码hello.c
,gcc会自动先进行预处理和编译,再进行汇编。
此时产生的目标文件是一个重定位文件(Relocatable File),可以使用objdump
命令查看文件内容:
1 | objdump -sd hello.o -M intel |
可以看到此处puts
函数的地址被设置为了00 00 00 00
,这是因为文件还没有进行链接操作,库函数的地址是未知的,所以就用0来代替
注意,前面说到,汇编结束之后产生的是一个可重定位文件,那么什么是重定位,为什么叫可重定位文件呢?
- 刚刚汇编完的文件中,代码和全局变量是分开存储的,而全局变量的地址到底是多少,代码是不知道的,此时就需要由链接器来寻找全局变量的地址。在链接之前,代码中访问全局变量的命令所访问的地址会被填充为0,链接时链接器就会把这些待填充的地址都改为
链接阶段
链接阶段包含两种链接:动态链接和静态链接。
静态链接
静态链接的过程就是把可重定位文件所依赖的库文件全部复制进来,生成一个包含了所需依赖库的目标文件,这个目标文件可以独立运行,不再需要任何的动态库。
这个过程是由静态链接器完成的,静态链接器也被称为链接器。
之所以叫做静态链接,就是因为静态链接的过程完全是在编译的过程中完成的,跟运行时没有什么关系。
静态链接命令:
1 | gcc hello.c -o hello --static |
- 关于pwn:
在pwn中如果一个ELF文件是静态链接的,那么它所使用的库就会全部被编译到ELF文件中,所以是可以直接找到其中的库函数地址的,如果能够利用栈溢出覆盖返回地址,就可以劫持函数调用我想调用的库函数了
动态链接
与静态链接不同,动态链接并不会