【C语言】程序运行过程:预处理/编译/汇编/链接
哈喽啊,盆友们。一起来看看C语言中编译预处理的内容吧!😜
1.程序编译的几个阶段
众所不周知,C语言的程序运行分为几个阶段。
咱们可以看看下面这个图,简单了解一二👇
细分开来,编译还分为3个小阶段:预编译(预处理)、编译、汇编
这三个阶段又分别做了什么事情呢?这就需要我们用linux下的gcc编译器来验证了
1.1 预编译
现在我们编写了一个这样的代码,分为两个文件
1 | // Add.c |
1 | // test.c |
在运行窗口中输入以下指令,进行预编译操作,得到test.i
文件
1 | gcc -E test.c -o test.i |
打开该文件,滑倒最底部,查看它与源代码的不同
截取文件部分内容如下
1 | ## 3 "test.c" 2 |
可以发现以下几点:
- 头文件include消失:实际上被展开了;
- 注释被删除
- define定义的符号被替换;
这就是预编译阶段做的3件事,实际上都是一些文本操作,并没有运行该代码。
1.2 编译
输入以下指令,生成test.s
文件
1 | gcc -S test.i -o test.s |
打开该文件,发现我们好像看不太懂它里面写了些什么
实际上,mov
、sub
都是汇编语言,这一步就是把C语言代码转变成了汇编代码。 进行了以下几步操作:
- 语法分析
- 词法分析
- 语义分析
- 符号汇总
这一部分内容可以阅读《程序员的自我修养》这本书,记录一下
1.3 汇编
汇编操作将汇编语句生成最终的二进制指令。
1 | gcc -c test.s -o test.o |
这个指令会生成一个test.o
目标文件
- Linux:目标文件后缀为
.o
; - Windows:目标文件后缀为
.obj
;
.o
目标文件和可执行成句的文件格式elf
,readelf工具可以解析elf格式的文件。
用文本编辑器打开这个文件,可以看到里面的东西都是乱码
实际上这个文件里面存放的是二进制内容。汇编操作就是将汇编代码转换成二进制指令;
其中有非常重要的一部:形成符号表。
1.4 符号表和链接
这个代码中其实包含了两个源文件:test.c
、Add.c
里面出现了两个符号,main和Add;
编译器会先对每个源文件创立一个符号表,为它们添加一个类似地址的参数。在链接阶段的时候,相同符号的地址参数会被设置为一样的。
这里编译会查看Add函数的定义在哪里:如main函数中只是extern,并没有Add的具体实现,最后链接之时,Add符号的地址会被设置为
Add.c
中该符号的地址。
这个操作又叫:符号表的合并和重定位。链接阶段还会执行另外一个操作:合并段表。
使用如下命令,将两个.o
文件链接成最终的可执行文件test。
1 | gcc test.o add.o -o test |
使用./TEST
操作执行该文件,可以看到程序成功输出了Add后的答案
1.5 总结
预编译操作,将头文件展开,删除注释,并执行define的替换和ifdef等预处理语句的操作。
1 | gcc -E test.c -o test.i |
编译操作,进行扫描(词法分析)、语法分析、语义分析、源代码优化、代码生成、目标代码优化,将预处理后的代码转成汇编指令。
1 | gcc -S test.i -o test.s |
汇编阶段,将汇编语句转成二进制指令,保存成.o
目标文件。
1 | gcc -c test.s -o test.o |
链接阶段,链接器将目标文件和库文件(如标准库或者自定义库)合并成一个可执行文件。这个过程包括符号解析、符号重定位等操作。
1 | gcc test.o add.o -o test |
1.6 什么每个c文件都有对应的o文件?
这里涉及到了一个问题:为什么每个.c
文件都会有一个对应的.o
文件,而不是合并成一个?
网上没有搜到相关的答案,问了下GPT,因为这里是gcc采用的分步编译策略,这样的好处是当修改了某个模块的时候,可以只重新编译该模块的文件,而不需要处理其他未修改的文件,可以提高编译速度。同时也方便了c/c++的模块化开发。
2.运行环境
程序执行的过程中:
- 程序先载入内存。在操作系统中,这个操作由系统完成;在独立环境中,程序的载入必须手动完成,也可以是通过可执行代码置入只读内存来完成
- 程序执行开始。调用main函数
- 开始执行代码。这时将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时使用静态内存,存储于静态内存中的变量在程序的整个执行过程中保留它们的值
- 终止程序。正常终止main函数,也可能是意外终止
结语
本篇博客中,我们认识了解了程序运行的几个阶段。这里面还有很多更深层次的问题待我去探究。
下篇博客:预处理详解
点个赞再走呗,谢谢!