一、步骤解读(预处理,编译,汇编,链接)
①.c -> .i:.i本质上还是高级语言文件,.i文件中不包括任何宏定义
然后,对.i(预处理后的文件)进行词法、语法、语义分析【就是编译】,优化后生成汇编代码文件
②.i -> .s:.s还是文本文件(汇编语言文件,人可读,机器不可读)
③.s -> .o:对汇编语言程序进行汇编,得到可重定位目标文件.o。.o不是文本了,是机器指令序列(也叫做机器语言程序、机器代码,由计算机直接识别)。
【注】机器指令和汇编指令是一一对应的。
④.o ->(无后缀) :与库函数(许多.o可重定位目标文件)进行链接,得到可执行程序(如下图,本质上是二进制代码的合并)
【注意】操作之间的关系
预处理、编译和汇编针对一个.c文件进行,得到一个对应的.o文件。
链接是将多个可重定位目标文件合并,生成可执行目标文件。
【链接命令举例】
$gcc -static -o (自定义的可执行程序的文件名) main.o test.o(静态链接)
二、预处理的步骤(和#密切相关,注意:#pragma要保留!)
【例子】条件编译指令
如图,根据INITIALIZE是否有定义,预处理后得到不同的源程序。
三、编译、汇编
1、汇编语言出现后:用助记符表示操作码、寄存器,用符号表示位置 。
图中,L0是一个地址,属于符号定义。而调用一个符号对应的函数(如图中jmp L0)就是一个符号的引用。
注意:符号的引用可以在不同模块间使用,也就是说,一个模块定义的符号可以被另一个模块引用。链接完成之后,才是一个完成的程序。
链接的优点:a.模块化 b.效率高(分开编译,重新编译被修改的源程序文件,再重新链接即可)
【练习】指出哪些是定义,哪些是引用
【解答】
1、红框内是符号定义,蓝框内是符号的引用。所以一般规律是,只要在函数或变量名前面有类型(void int等),就是个符号定义。
2、虽说左右两个swap都是定义,但是左端的swap声明,仅仅用以说明swap是一种外部定义,是一种弱定义,在main函数找不到它的“真正的定义”。右端的swap则是强定义。要区分开!
3、(易错)int 后的temp和“=”左的temp不是符号定义或符号的引用,因为temp在栈中,只能在内部使用,不能被外部引用:
因此,“符号”肯定不能是非静态局部变量!
从本题还可知:定义和引用可以不在一个程序模块(.c文件)中!
四、链接(本章重点)
<add>:
0: 55 push %ebp
1: 89 e5 mov %esp, %ebp
3: 83 ec 10 sub $0x10, %esp
6: 8b 45 0c mov 0xc(%ebp), %eax
9: 8b 55 08 mov 0x8(%ebp), %edx
c: 8d 04 02 lea (%edx,%eax,1), %eax
f: 89 45 fc mov %eax, -0x4(%ebp)
12: 8b 45 fc mov -0x4(%ebp), %eax
15: c9 leave
16: c3 ret
【test的反汇编文件(objdump -d test )】
80483d4<add>:
80483d4: 55 push %ebp
80483d5: 89 e5 mov %esp, %ebp
80483d7: 83 ec 10 sub $0x10, %esp
80483da: 8b 45 0c mov 0xc(%ebp), %eax
80483dd: 8b 55 08 mov 0x8(%ebp), %edx
80483e0: 8d 04 02 lea (%edx,%eax,1), %eax
80483e3: 89 45 fc mov %eax, -0x4(%ebp)
80483e6: 8b 45 fc mov -0x4(%ebp), %eax
80483e9: c9 leave
80483ea: c3 ret
【结论】指令等都相同。但两种文件的起始地址不同,test的反汇编文件是虚拟地址
2、链接的本质
每个.o文件中都有若干个基本区:如代码区和数据区。如在main.o中,有代码区、数据区。每个.o文件都有确定的格式来组织。这些区又称为节。合并相同“节”,就可以得到可执行目标文件。
其中,.bss(未初始化变量)和.data(初始化变量)都对应数据读写段,.text对应只读代码段。段的概念后续会涉及。
3、链接的步骤(符号解析+重定位)
①确定标号引用关系(符号解析)
②合并相关.o文件(代码区和代码区合并,数据区和数据区合并……)
【如下图】(我们知道每个.o文件的起始地址是0)合并之后到虚拟地址区域时,每个标号的偏移量相同(比方说在p0.o里,B,C相对于0的偏移量,与链接后相对于虚拟地址的偏移量不变,但地址变化)
故:
③确定每个标号的地址
④在指令中写入新地址(这就是“可重定位”的含义)
Step1.符号解析(对应①)
先找到定义和引用的符号,比如:
void swap() {…} /* 定义符号swap */
swap(); /* 引用符号swap */
int *xp = &x; /* 设xp为静态变量,则本行定义符号 xp, 引用符号 x */
编译器会把定义的符号放在符号表(一种结构数组)中:
表项里包括了符号名、长度、位置等信息。
Step2.重定位(对应②③④)
合并节形成段、计算在存储器映像的虚拟地址(绝对地址)、引用的地址改成重定位后的地址信息。
4、可执行文件在存储器(解释程序在存储器中怎么存储)
如图,黄框内容映射在“只读代码段”,蓝框内容映射在“读写数据段”。也就是:这两个段的内容都由可执行目标文件导入。
所有程序的虚拟地址的起始都是0x08048000(这是最低地址,当然,也可能从比该地址高的地方开始导入段).
最后真正运行还要进行一次转换(第六章有阐述)。