今天我们来玩玩gcc。
是因为突然发现ESP-IDF用的是CMake,要了解CMake最好就要先学习Makefile有个基础,学习Makefile最好就要先熟悉gcc,所以就有了今天这篇文章。
首先我们要明确一个问题,那就是gcc/g++是什么,它们有什么用。
gcc
是 GNU Compiler Collection(GNU 编译器集合)的缩写,是一个用于编程语言的编译器,特别是 C、C++、Fortran、Objective-C、Objective-C++、Ada、Go 以及其他一些语言。它最初是为 GNU 操作系统开发的,但如今已被广泛应用于各种 Unix-like 系统(包括 Linux)和其他操作系统(如 Windows,通过 MinGW 或 Cygwin)。
gcc
的主要用途是将源代码(例如 C 或 C++ 代码)编译成机器代码(通常是可执行文件或库)。它支持多种优化选项和调试选项,使得开发人员能够根据需要调整编译过程。
简单来说gcc就是编译器,它可以将我们写的.c文件变成.exe的可执行文件(当然了,不止.c文件,可执行文件的后缀也不一定是.exe)。
而g++也属于gcc,不过是专门为了编译C++而产生的特化版本。
由于大家初学C语言的时候,大概率是看着教学视频学的,并且视频里一般都是教大家下载IDE,在IDE里进行编程的,因此我们不需要去关注具体是如何使用编译器去编译我们的c文件的。但是到了Linux环境下,就没有那么轻松了(虽然Linux也可以下载安装IDE,但是很多情况下我们用的不是有图形界面的Linux,而是命令行,这样也比较帅对叭),我们需要手动敲命令去对我们写好的程序进行编译。
安装gcc
那么第一步,我们需要先安装上gcc。
我们打开虚拟机,或者你有服务器可以远程连接上也可以,再或者你远程连接你的虚拟机也OK。
我这边以Ubuntu为例(因为手头上安装的虚拟机是Ubuntu的)
我们在命令行里输入下面这个命令
sudo apt-get install gcc
如果成功下载,那么我们可以直接进入下一步。
如果下载不了,显示没有这个软件包,那么我们需要加个源。
执行下面的命令去修改源列表文件。
vim /etc/apt/sources.list
添加一行下面这个内容(不会用vim的小伙伴在上面那个命令执行过后按下i键,然后用上下左右键移动到文件的最下面,然后将下面的复制进去,接着按下esc键,再打个 :冒号,然后输入qw保存退出,如果显示权限不够那就出去改个权限。)。
deb [arch=amd64] http://archive.ubuntu.com/ubuntu focal main universe
下一步更新源。
sudo apt-get update
更新完之后下载gcc。
sudo apt-get install gcc
再输入下面命令查看gcc版本,如果能正常看到那么就表示我们下载成功。下载g++的话只需要把上面下载命令中的gcc替换成g++即可。
gcc -v
编译的流程
接下来我们来了解一下.c文件是如何变成.exe文件的。
一共是四个步骤,分别是预处理,汇编,编译,链接。曾经VS帮我们做好了一切,如今我们需要还我们欠下的债了,深刻理解这四个步骤也有助于我们理解我们自己写下的代码(没错,就算是你自己写的代码你也不一定能够彻底明白,反正我上星期写的代码我这星期已经看不懂了)。
预处理
预处理这一步主要会做下面这些步骤。
- 代码包含:预处理器处理
#include
指令,这通常意味着将头文件的内容直接插入到源文件中。 - 宏替换:所有
#define
定义的宏都会被展开,即在代码中进行相应的文本替换。 - 条件编译:处理如
#if
、#ifdef
等条件编译指令,这些指令通常用于在不同环境下编译不同的代码路径。 - 注释删除:预处理器会移除所有注释部分,这些注释对编译后的代码执行没有影响。
- 生成中间文件:预处理过程后通常会生成一个中间文件(如.i文件),供后续编译过程使用
在gcc中预处理的选项是-E,我们只需要使用gcc -E 这个命令就可以对我们写的.c文件进行预处理了。但是光预处理还没有,我们需要把预处理的结果保存起来,这时候就需要使用-o选项来指定文明存放预处理结果的文件。
使用例子如下。其中test.c是写的.c文件,test.i是预处理结果
gcc -E test.c -o test.i
然后test.c的内容是下面这样的。
很简单的内容,包含了个头文件,使用宏定义代替了一个打印语句。在main函数里使用宏定义并且用了个打印语句。
经过预编译之后我们得到test.i文件。实际上可以叫任何名字,只需要在上面预编译的命令中修改即可,甚至后缀都不一定非得是 .i ,在Linux中后缀是没有实际作用的,它唯一的作用是对于让人可以通过后缀判断出这个文件是什么类型的,而对于Linux来说,后缀是无所谓有无所谓无的东西。
虽然名字可以随意,但是我们约定俗成的命名规则就是保留.c文件的名字,然后修改后缀。
接着我们看看预处理后得到的test.i是什么内容。
原本很简单的一段代码,经过预处理之后变成八百多行了。
文件开头我们看不懂,我们拉到最下面。
主要看最后面的main函数,里面的宏定义被替换成了printf语句。并且注释也没有了。还将我们的头文件展开了,没错,前面差不多八百行代码就是原文件中的#include <stdio.h>展开后的结果。
这就是预处理。
编译
编译这个步骤会对上个步骤得到的 .i 文件做以下处理,最终生成汇编文件。
- 词法分析:编译器首先识别出源代码中的每个单词,如变量名、运算符等。
- 语法分析:之后,编译器解析这些单词组成的语法结构,如表达式、控制结构等。
- 语义分析:确保所有变量都被正确声明,且操作都是合理的,例如类型检查。
- 优化:编译器会对中间代码进行优化,以提升执行效率,去除不必要的操作。
- 生成汇编代码:最终,编译器生成对应的汇编代码文件(如.s文件),此代码是机器语言的低级表示。
gcc中汇编的选项是 -S ,得到的汇编文件的后缀一般我们都给 .s ,所以使用的命令如下。
gcc -S test.i -o test.s
我们可以来查看一下生成的汇编文件。
虽然还是很多,但是已经压缩成48行了。汇编就已经是很接近底层的了。
汇编
- 代码翻译:汇编器将汇编代码转换成机器码,生成目标文件(如.o文件)。
- 符号引用解决:这一阶段还会处理代码中的符号引用,确定所有引用的目标地址。
下一步就是汇编了,这有点小迷惑人,因为编译的结果是汇编文件,而汇编的结果是目标文件。
gcc中执行汇编步骤的选项是 -c ,示例命令如下。
gcc -c test.s -o test.o
输入的是 .s 文件,输出的是 .o文件。这一步就将我们的汇编转成机器码了,当我们再去查看的时候就不是我们能够看懂的了(虽然我估计大部分人连汇编都看不懂了(包括我))。
还有个需要说的,就是汇编这一步,可以直接从 .c到 .o,也就是可以跳过前两步直接到第三步。
链接
最后一步链接,就可以生成可执行文件了。
- 合并代码:链接器将所有的目标文件以及它们所需要的库文件合并,形成最终的可执行文件。
- 解析外部引用:解决不同文件之间相互调用的函数或者变量地址解析问题。
- 生成可执行文件:最终输出一个可执行的程序文件,该文件可以直接在操作系统上运行。
gcc中链接这一步骤不需要选项,也就是像下面这样。
gcc test.o -o test
直接输出没有后缀的可执行文件(可以让它的后缀为exe方便理解,但是在Linux中我们一般直接不要后缀)
那么我们就已经有了一个可执行文件了,应该怎么执行呢?
./test
像上面这样一个点加个斜杠再加上可执行文件的名字就可以啦。
结果是下面这样的。
可能有小伙伴会说这个gcc也太麻烦了,我还是用IDE吧。
那其实我们要可执行文件的话可以不用按照这四个步骤一步步来,现实中我们不能一步登天,但是在gcc里可以,我们可以直接执行下面这个命令,直接从 .c 编译成可执行文件。
gcc test.c -o test
这样就简单很多了。
上面就是gcc最基本的用法了,懂得上面这几步之后,理论上去学个Makefile什么的问题不大。
不过我们可以再深入了解了解gcc,这个拥有着强大功能的工具不可能就做这么点事对吧。
指定头文件目录
假如我们现在写了一个含有功能函数的文件(或者是下载来的),那么我们在主函数要用对应的函数,那么就需要包含头文件对吧。
现在我在main.c文件的同级目录下专门建了个文件夹存放工具文件。并且我在main.c里包含了这些文件。像下面这样。
然后我们再编译就会发现gcc找不到我们的头文件。
这时候就要请出我们gcc指定头文件的选项 -I 了。
gcc -I ./my_tools test.c -o test
这样包含头文件就没问题了,但是又出了新的问题,那就是找不到函数定义了。
这是因为我们需要把被包含的文件一起放进来编译。
这样就没问题了,可是这样子写的话会非常麻烦,光是包含一个头文件就要加这一长串,万一我包含了很多呢?这就要请出我们的Makefile了(下一篇也可能是下n篇文章我们再说)。
总之我们先知道指定头文件目录的这个选项即可。
设置优化级
-O选项可以设置优化级,这边是大写的O,别和之前小写的o混一起了。
GCC编译器提供了多种优化级别,通过使用-O
后跟一个数字(从0到3),可以指定希望GCC应用的优化等级。
-
优化级别0 (-O0)
- 关闭所有编译优化。这是默认的编译设置,主要用于调试阶段,因为优化可能会改变程序的执行方式,从而影响调试的准确性。
-
优化级别1 (-O1)
- 开启基本优化。这一级别的优化包括将常用值分配给寄存器、消除无用的代码等简单优化。这是在不影响调试的前提下提高程序性能的一个平衡选择。
-
优化级别2 (-O2)
- 开启中级优化。除了包含-O1中的所有优化外,还包括了更复杂的优化如分支预测、循环展开、内联函数等。这可以显著提高程序的运行速度,但可能会增加编译时间和最终可执行文件的大小。
-
优化级别3 (-O3)
- 开启高级优化。它包括-O2中的所有优化,并尝试进一步执行如函数内联扩展等更高级的优化技术。此级别适用于当程序性能是关键考虑因素时,但需要注意这可能会使编译时间变长,并且在某些情况下可能影响程序的稳定性或可预测性。
-
额外优化级别 (-Os)
- -Os:专注于生成尽可能小的可执行文件大小。适用于嵌入式系统和那些对程序大小有严格要求的场景。
默认的优化级别是O0,一般情况下我们不动它的。
添加宏定义
这个会用的多一点。
-D后接我们要在编译时候注册的宏。
我现在修改一下我们之前的test.c文件。
然后我们直接编译,运行的结果是这样的。
当我们使用了-D选项,则是下面这样的效果。
通过这样的小例子,相信大家就对-D有了简单的认识了。
今天在这边就介绍到这边了,对于我们后续学习Makefile来说是足够的。
感觉不过瘾,想更深入了解gcc的小伙伴可以直接
man gcc
也可以去查官方文档。
GCC online documentation- GNU Projecthttps://gcc.gnu.org/onlinedocs/