前言
程序的编译过程包含4步:预编译、编译、汇编、链接。本文通过一个实例对这一过程进行详细讲解。
预编译
预编译用于对源代码文件进行初步的处理,主要处理规则如下:
- 将所有的”#define“删除,并且展开所有的宏定义。
- 处理所有条件编译指令,如“#if”、“#ifdef”、“#ifndef”、“#else”、“#elif”、“#endif”。
- 处理“#include”预编译指令,将被包含的文件插入到预编译指令的位置。
- 删除所有的注释“//”、“/* */”。
- 添加行号和文件名标识,如# 4 “hello.c”,这样编译器产生编译错误或警告时可以显示行号。
实例:
创建一个hello.c,文件内容如下:
#include <stdio.h>
#define MARK 1
//这是一段示例代码
void output()
{
#if MARK
printf("MARK is 1\n");
#else
printf("MARK is 0\n");
#endif
}
/*
作者:inputA
*/
int main()
{
output();
return 0;
}
输入如下指令对hello.c进行预编译:
gcc -E hello.c -o hello.i
得到hello.i文件
打开hello.i发现文件内容非常多,在文件最后有如下一段代码:
开头的# 4 "hello.c"
表示接下来的代码段源自文件hello.c,并且是从该文件的第4行开始的。
输出的.i
文件中删除了注释、删除了MARK
宏定义,并且根据MARK
的值为1删除掉了printf("MARK is 0\n");
这段代码。
删除内容后的空白行依然被保留,这是为了在编译时出现编译错误或警告方便显示行号。
搜索printf可以在hello.i文件中找到如下这段代码:
在它上面最相邻的行号和文件名标识
为:
表示printf是在/usr/include/stdio.h
中声明的一个外部函数。
编译
编译用于将预编译之后代码转换成汇编代码。
在刚在的实例中输入如下指令:
gcc -S hello.i -o hello.s
即可生成hello.i对应的汇编文件hello.s。
打开hello.s之后可以发现是一堆汇编代码。
汇编
汇编用于将汇编代码转换成机器码。
在刚在的实例中输入如下指令:
gcc -c hello.s -o hello.o
即可生成hello.s对应的机器码文件hello.o
输入如下指令:
objdump -S hello.o
即可查看hello.o的反汇编代码,内容如下:
链接
链接用于生成最后的可执行文件。
在刚才的实例中输入如下指令:
gcc -o hello hello.o
即可生成最后的可执行文件hello
输入./hello即可执行hello文件,执行结果如下:
输入如下指令对hello可执行文件进行反汇编:
objdump -S hello
可以看到output函数对应的反汇编代码如下:
其中对应的调用printf函数从原来的
b: e8 00 00 00 00 callq 10 <output+0x10>
变成了
645: e8 c6 fe ff ff callq 510 <puts@plt>
可见在链接过程中调用函数的跳转地址发生了变化,查找0x510
处的代码如下:
这段代码与动态链接有关,整段代码可以分成两部分。
510: ff 25 ba 0a 20 00 jmpq *0x200aba(%rip) # 200fd0 <puts@GLIBC_2.2.5>
表示从0x200fd0
这个地址取出printf的地址,并将其赋值给PC指针,然后跳转过去。
如果发现0x200fd0
这个地址里的内容为空,就会执行如下代码:
516: 68 00 00 00 00 pushq $0x0
51b: e9 e0 ff ff ff jmpq 500 <.plt>
这段代码用于跳转到动态链接器将printf函数的地址填写到0x200fd0
这个位置处。
所以第二段代码只在程序第一次运行时会执行,在0x200fd0
地址内容不为空后将不会再执行。