C语言编译流程分析
C语言编译流程
| 流程 | 功能 |
|---|---|
| 预处理 | 头文件展开,宏替换,去掉注释 |
| 编译 | 将预处理生成的文件转换成汇编文件 |
| 汇编 | 将汇编文件转换成二进制文件 |
| 链接 | 将函数库中相应代码组合到目标文件中 |
流程分析
编译器对工程的文件依次编译,工程中包括main.c,test.c,test.h。
mian.c
#include "test.h"
#define TEST_NUM 5
void main()
{
int a,b,num;
a = TEST_NUM;
b = 5;
num = test(a,b);
}
test.c
#include "test.h"
int test(int a,int b)
{
return a + b;
}
test.h
int test(int a,int b);
以main.c为例,经过预处理(gcc -E main.c -o main.i)过后可以看到头文件、宏定义被替换。
main.i
# 1 "main.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "main.c"
# 1 "test.h" 1
int test(int a,int b);
# 2 "main.c" 2
void main()
{
int a,b,num;
a = 5;
b = 5;
num = test(a,b);
}
这里其实就解决了我们遇到的头文件重复包含的问题。
我们在编写头文件时,通常是下面的这种格式:
#ifndef __xx_H
#define __xx_H
...
#endif
在了解预处理后,我们便很容易知道为什么要这么写。假设main.c中包含了test1.h和test2.h两个头文件,而这两个头文件又同时需要用到加法功能,所以他们都包含了add.h。如果不使用上述代码块,就会发现在main.i中出现了两次add.h中的声明,形成代码冗余。
经过预处理生成.i文件后,接下来就是进行编译(gcc -S main.i -o main.s)生成.s文件。
.file "main.c"
.text
.def ___main; .scl 2; .type 32; .endef
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $32, %esp
call ___main
movl $5, 28(%esp)
movl $5, 24(%esp)
movl 24(%esp), %eax
movl %eax, 4(%esp)
movl 28(%esp), %eax
movl %eax, (%esp)
call _test ;
movl %eax, 20(%esp) ;
nop
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE0:
.ident "GCC: (MinGW.org GCC-8.2.0-3) 8.2.0"
.def _add; .scl 2; .type 32; .endef
生成的汇编代码,我们暂时只关注call _test和movl %eax, 20(%esp)部分(.xxx为伪指令,作用是告诉编译器该如何操作,并不是程序的一部分),这两条指令的含义分别是跳转到test函数的首地址和将test(a,b)的返回值给num,对应.c的num = test(a,b)语句。
之所以关注这一条语句,是因为.i在转换成汇编语言的过程中,编译器在碰到不认识的变量,函数,类…,首先是查找它有无声明,如果没有,则直接报错;如果有,则根据对应的定义空出一定的存储空间并进行指令转化。比如在编译到此条语句时,编译器碰到了不认识的函数test(),它会首先查找声明,发现在声明中存在,并且还具有返回值,所以,形成在此处的汇编指令就是call _test和movl …。其中的_test以符号形式存在,并不是test()函数入口地址。(test()函数在test.c中定义,此处仅仅是编译main.c,传入test()入口地址这件事交给链接器来做)
同时这也解释了为什么声明时要指明变量类型,编译器不知道类型就不知道怎么替代c代码。上述例子中的test()函数如果返回值为空,我们的汇编代码就不会出现movl …的赋值操作。
编译结束后,来到汇编(gcc -c main.s -o main.o)阶段,生成.o的对象文件,转化成了二进制表示的机器语言,同时,他还会生成一张重定位表,记录函数、变量及其对应的地址等。至此,main.c生成了它的对象文件main.o,同样的,test.c经过相同的过程也生成了test.o。
重定位表:

进入链接阶段,编译器会扫描所有的.o文件,先查找main函数,然后根据main函数中的代码执行流程来组织代码结构。当碰到之前保留的符号(_test)时,会去所有的.o文件重定位表中查找相应符号对应的地址,找到后会将.o中的符号替换为地址,例如call _test被替换成call 0x00345678。
到此,编译工作结束。
其实,理解编译的基本原理,很多工程上遇到的问题就迎刃而解了。比如一个工程中的模块化编程(led.c,motor.c,sensor.c…),我们这样编写不仅是遵循“高内聚低耦合”的原则,从编译的角度来看,修改某一处的代码只会影响它所在的.c文件,重新编译的也只有那个.c文件,这样的编程思路极大加快了项目的编译速度。
浙公网安备 33010602011771号