# GCC 编译过程
# 编译步骤
一般来说,编译步骤分为
- 预处理:将头文件和宏进行替换
- 编译:将预编译后的 C 语言文件,编译为汇编文件
- 汇编:将汇编文件编译为 n 个二进制文件
- 链接:将二进制文件链接为一个
# GCC 的使用方法
gcc [option] filename |
# 2.1 gcc 使用示例
例如,我们要 编译一个
gcc main.c // 输出一个名为a.out的可执行程序, 然后可以执行./a.out
gcc -o hello main.c // 输出名为hello的可执行程序, 然后可以执行./hello
gcc -o hello main.c -static // 静态链接
gcc -c -o hello.o main.c // 先编译(不链接)
gcc -o hello hello.o // 再链接
静态链接会将涉及到的库 .os
文件都装载到二进制文件中,所以文件会相对的大得多
但是在 windows, 动态编译和静态编译文件大小一致,可能和库文件的构成相关
# gcc 常用选项
选项 | 功能 |
---|---|
-v | 查看 gcc 编译器的版本,显示 gcc 执行时的详细过程 |
-o <file> | 指定输出文件名为 file, 这个名称不能跟源文件名同名 |
-E | 只预处理,不会编译、汇编、链接 t |
-S | 只编译,不会汇编、链接 |
-c | 编译和汇编,不会链接 |
# 手动编译流程
如一开始所说的一个 c/c++ 文件要经过 预处理 、 编译 、 汇编 和 链接 才能变成可执行文件
# 预处理
在 C/C++ 源文件中,以 “#” 开头的语句被称为预处理命令
例如,包含命令 “#include”、宏定义命令 “#define”、条件编译命令 “#if”、“#ifdef” 等
预处理就是将要包含 (include) 的文件插入原文件中、将宏定义展开、根据条件编译命令选择要使用的代码,最后将这些东西输出到一个 “.i” 文件中等待进一步处理
在这一步不检测语法错误,在预处理之后文件还是 C
文件
对应的指令为
gcc -E -o filename.i filename.c
```makefile
编译前的源文件
```C
#include <stdio.h>
#define MAX_VALUE 10
int main(int argc, char const *argv[])
{
printf("%d\r\n", MAX_VALUE);
return 0;
}
编译后,可以看到 printf
中的 MAX_VALUE
被替换为 10
而且代码量明显多了很多东西,这些大部分都是 stdio
内部的文件
这也告诉我们,一句输出并没有想象的那么简单,机器背后做了很多的任务
# 编译
编译是将预处理过的 C/C++ 代码 (例如上述的 “.i” 文件)“翻译” 成汇编代码
翻译的代码长短和内存大小,取决于优化等级和编译器的算法
在这一步就会进行语法检测,毕竟是一种语言翻译为另一种语言,先得检测词法、语法以及语意,确保原语言没有错误之后才能进行翻译
对应的指令:
gcc -S -o filename.s filename.i | |
// 或 | |
gcc -S -o filename.s filename.c |
编译后上面的.i 文件变成,将会生成对应的汇编文件
如果添加 -O1
的优化选项就会出现不一样的代码,效果如下图
按照 gcc 的介绍,越高优先级的代码执行效率越高,占的 RAM 也会减少
但是,毕竟编译器不是人,它解决问题的思路很有可能是局部最优,全局贪心,这就可能导致一些累加逻辑被优化
例如:
void iic_delay(uitn8_t ntime)
{
while(ntime--)
;
}
对于上面这种代码 使用 -O3
的优化等级,可能就会被优化
不仅如此,越高优先级的优化,在代码调试的时候就会存在越大的困难,因为生成汇编文件已经和原本的 C 语言相差甚远,尽管能正常执行,但一旦出错就很难定位错误
# 优化参数表
类型 | 功能 | 说明 |
---|---|---|
O0 | 不进行优化 | 默认不加就是 O0 |
O1 | 优化等级 1 | \ |
O2 | 优化等级 2 | \ |
O3 | 优化等级 3 | \ |
Og | 优化等级 1, 并生成调试信息 | 在 O1 优化的基础上保留调试信息,当然要配合 -g 一起使用 |
Os | 优化等级 2, 去掉反向优化 | 不是所有的优化都会减小空间,当出现优化反而变大可以使用 - Os |
Ofast | 优化等级 3, 通过非常规的方式进行优化 | 通过打破一些国际标准 (比如一些数学函数的实现标准) 来实现的,一般不推荐 |
# 汇编
汇编是将第二步输出的汇编代码翻译成符合一定格式的机器代码.
在 Linux 系统上一般表现为 ELF 目标文件 (OBJ 文件).
最明显的就是使用 keil 时会生成大量的 xx.o
文件,这些都是编译出来
对应的执行命令如下:
gcc -o filename.o filename.s |
“反汇编” 是指将机器代码转换为汇编代码,这个功能往往在程序调试中使用
反汇编的代码和汇编的代码大概率不相同
# 链接
链接就是将上步生成的 OBJ 文件和系统库的 OBJ 文件、库文件链接起来,最终生成了可以在特定平台运行的可执行文件
gcc -o filename filename0.o filename1.o filename2.o |
# 更快捷的方法
一般来说我们不会需要查看 x.i
和 x.s
文件,所以可以跳过这两个步骤,直接生成 `x.o, 然后再进行链接
无需每一步指定生成规定的文件类型
例如:
gcc -c -o filename1.o filename1.c
gcc -c -o filename2.o filename2.c
gcc -o filename filename1.o filename2.o
# 指定头文件目录
在写代码的时候我们,必然会用到库函数,那么这些库函数在哪里呢
一般来说,编译器会默认使用系统目录,也可以由用户指定
系统目录,一般是指,工具链里的某个 include 目录,如何确定呢?
可以通过下面这条命令,让其在终端中输出
echo 'main(){}'| gcc -E -v - // 它会列出头文件目录、库目录(LIBRARY_PATH) |
如果不想用标准库,那么可以使用 -nostdinc
来实现,不使用标准库来编译
如果要添加自己的头文件库进入编译,可以使用下面这个语句
-I <IncDir> |
# 指定库文件
同样的库文件,也是我们会用到,一般来说,这些库可能不是开源的 .lib
文件
与头文件相同,编译器会默认使用系统目录,也可以由用户指定
可以通过下面这条语句定位使用的库在哪个文件
echo 'main(){}'| gcc -E -v - // 它会列出头文件目录、库目录(LIBRARY_PATH) |
如果不想用标准库,那么可以使用 -nostdlib
来实现终端输出
如果用户想要添加自己指定库文件目录
可以使用下面这条指令来指定自己的库文件所在目录
-L <LibDir> |
与头文件不同,头文件本身只会被预处理到 .c
文件中,用于生成 .i
文件
但 .lib
文件不同它可以指定单个文件
-l <abc> // 链接 libxx.so 或 lib.x |
目前对,GCC 的编译过程,了解的只有这些,可能以后会继续添加
如果文章中有什么不当或者错误,望大佬们斧正
# 参考文献
[1] https://www.100ask.net/p/t_pc/course_pc_detail/video/v_5f928b93e4b028be590ba441?product_id=p_5f926a49e4b00f13286a0cec&content_app_id=&type=6
[2] 《嵌入式Linux应用开发完全手册》
[3] https://blog.csdn.net/qijitao/article/details/130111003#:~:text=1. gcc 中指定优化级别的参数有:-O0、-O1、-O2、-O3、-Og、-Os、-Ofast。,2. 在编译时,如果没有指定上面的任何优化参数,则默认为 -O0,即没有优化。
大道五十,天衍四十九,人遁其一!