1. GCC编译器简介
GCC 可以编译如 C、C++、Object C、Java、Fortran、Pascal、Modula-3、和 Ada 等多种语言,而且 GCC 又是一个交叉平台编译器,它能够在当前 CPU 平台上为多种不同体系结构的硬件平台开发软件,因此尤其适合在嵌入式软件领域的开发编译。在使用 GCC 编译程序时,编译过程可以被细分为四个阶段:
预处理(Pre-Processing)
编译(Compiling)
汇编(Assembing)
链接(Linking)
GCC 提供了 30 多条警告信息和三个警告级别,使用它们有助于增强程序的稳定性和可移植性,此外,GCC 还对标准的 C 和 C++语言进行了大量的扩展,提高程序的执行效率,有助于编译器进行代码优化,能够减轻编程的工作量。GCC 通过文件后缀名来区别输入文件的类别。
2. GCC的使用
GCC 仅仅是一个编译器,没有界面,必须在命令行模式下使用。通过
gcc
命令就可以将源文件编译成可执行文件。GCC 既可以一次性完成 C 语言源文件的编译,也可以分步骤完成,我们先完整演示如何一次性完成源文件的编译,让大家对 GCC 的使用有一个初步的了解。
以下面简单的 C 语言程序为例,打开 Sublime Text 输入代码,保存到桌面上,命名为
demo.c
。
#include <stdio.h>
int main()
puts("shiyanlou");
return 0;
查看gcc版本号 gcc -v
在 Linux 下编译 C 语言程序,一般的 Linux 发行版都内建的有 GCC,无需用户再自行安装。打开 Xfce 终端,使用以下命令查看 GCC 版本:
gcc -v
最后的gcc version后面的数字就是型号,说明gcc 安装好了,如果没有安装使用
Ubuntu等基于Debian发行版Linux可以使用如下命令安装:
apt -get install gcc
Fedora等基于RPM发行版Linux可以使用如下命令安装:
yum install gcc
生成可执行文件
输入以下命令,生成可执行程序文件:
$ cd Desktop
$ gcc demo.c
我们能够发现在桌面上多了一个名为 a.out
的文件,这就是最终生成的可执行文件。
这样我们就完成了编译和链接的过程。值得一提的是,Linux 不像 Windows 那样以文件后缀来区分可执行文件,Linux 下的可执行文件后缀理论上可以是任意的,这里的 .out
只是用来表明它是 GCC 的输出文件。不管源文件的名字是什么,GCC 生成的可执行文件的默认名字始终是 a.out
。
如果不想使用默认的文件名,那么可以通过 -o
选项来自定义文件名:
$ gcc demo.c -o demo.out
这样生成的可执行程序的名字就是 demo.out
。可执行文件也可以不带后缀,因为 Linux 下可执行文件的后缀仅仅是一种形式上的:
$ gcc demo.c -o demo
这样生成的可执行程序的名字就是 demo
。
输入以下命令,生成可执行程序文件:
$ cd Desktop
$ gcc demo.c
我们能够发现在桌面上多了一个名为 a.out
的文件,这就是最终生成的可执行文件。
这样我们就完成了编译和链接的过程。值得一提的是,Linux 不像 Windows 那样以文件后缀来区分可执行文件,Linux 下的可执行文件后缀理论上可以是任意的,这里的 .out
只是用来表明它是 GCC 的输出文件。不管源文件的名字是什么,GCC 生成的可执行文件的默认名字始终是 a.out
。
如果不想使用默认的文件名,那么可以通过 -o
选项来自定义文件名:
$ gcc demo.c -o demo.out
这样生成的可执行程序的名字就是 demo.out
。可执行文件也可以不带后缀,因为 Linux 下可执行文件的后缀仅仅是一种形式上的:
$ gcc demo.c -o demo
这样生成的可执行程序的名字就是 demo
。
通过 -o
选项也可以将可执行文件输出到其他目录,并不一定非得在当前目录下,例如:
$ gcc democ -o ../demo.out
../
表示上一层目录,如果不写,默认是当前目录。这里的上一层目录是 shiyanlou
,打开该目录能够看到生成的可执行文件 demo.out
。
表示将可执行文件输出到上一层目录,并命名为 demo.out
。
运行可执行程序
既然我们已经生成了可执行程序,现在我们来学着如何运行它。执行以下命令:
$ ./a.out
./
表示当前目录,整条命令的意思是运行当前目录下的 a.out
程序。如果不写 ./
,Linux 会到系统路径下查找 a.out
,而系统路径下显然不存在这个程序,所以会运行失败。
注意: 如果程序没有执行权限,可以使用 sudo chmod 777 a.out
命令来增加权限。
2️⃣ GCC编译过程(四阶段)
上一实验我们学习了如何使用 GCC 一次性完成编译和链接的整个过程,一般来说我们在学习 C 语言过程中都这么做,因为这样最方便。从程序员的角度看,只需简单地执行一条 GCC 命令就可以了,但从编译器的角度来看,却需要完成一系列非常繁杂的工作。为了更好地理解 GCC 的工作过程,我们可以把 GCC 编译过程分成几个步骤单独进行,并观察每一步的运行结果。
GCC 预处理阶段
GCC 编译阶段
GCC 汇编阶段
GCC 链接阶段
1. 预处理阶段 -E .i
GCC 预处理阶段第一个任务是头文件展开,例如一开始的 #include <stdio.h>
,那么预处理阶段就会把这个 stdio.h
文件加载到你的 .c
中去。
第二个任务就是宏定义和条件编译处理,ANSI 标准可用的预处理宏定义和条件编译命令主要有这些:
#define
#error
#include
#else
#endif
#ifdef
#ifndef
#undef
#line
#pragma
该阶段会生成一个中间文件 *.i
,但实际工作中通常不会专门生成这种文件,基本上用不到,如果非要生成这种文件,可以使用 -E
参数让 GCC 在预处理结束后停止编译过程。
我们还是使用上一节实验的 C 语言源程序并以 demo.c
命名保存在桌面:
#include <stdio.h>
int main()
puts("shiyanlou");
return 0;
打开终端,执行以下命令,让 GCC 在预处理结束后停止编译过程。
$ cd Desktop
$ gcc -E demo.c -o demo.i
此时桌面上就会生成 demo.i
文件。
这个是预编处理头文件、宏定义和条件编译命令 后生成的.i文件,也是编译未完成
2. 编译阶段 -S .S
在编译阶段,GCC 把预处理后的结果编译成汇编或者目标模块。输入的是中间文件 *.i
,编译后生成汇编语言文件 *.S
,这个阶段对应的 GCC 命令如下:
$ gcc -S demo.i -o demo.S
此时桌面上就会生成 demo.S
文件。
生成汇编语言文件.S
3. 汇编阶段 -C .o
在汇编阶段,编译器把编译出来的结果汇编成具体 CPU 上的目标代码模块。输入汇编文件 *.S
,输出机器语言 *.O
。这个阶段可以通过使用参数 -C
来完成。
$ gcc -C demo.S -o demo.o
这样救生成了可编译文件 机器语言文件.o
4. 链接阶段 -o
链接阶段把多个目标代码模块连接生成一个大的目标模块,输入机器代码文件 *.o
,汇集成一个可执行的二进制代码文件。这一步骤可以通过以下命令完成:
$ gcc demo.o -o demo
$ gcc demo.c -o demo (推荐)
在线上环境里执行 gcc demo.o -o demo
命令时会发生如下报错:
这里的 GCC 包中的 crt1.o
功能简单理解为:crt 是 libc 的基本包之一,它提供访问计算机的基本功能,包含了像 printf
、puts
等方法,这就是为什么它经常包含在最基本的 C 应用程序中。
在这里因为线上环境版本问题会有报错,所以我们执行第二条命令 gcc demo.c -o demo
,这并不影响我们的学习,因为在本小节我们需要理解的是 GCC 编译过程以及它的基础原理,在平常使用中我们并不会像这样分步地对程序进行编译。
最后用./执行可执行二进制文件就行了
本小节我们学习了以下知识点:
GCC 预处理阶段
GCC 编译阶段
GCC 汇编阶段
GCC 链接阶段
本小节我们需要理解的是 GCC 编译过程以及它的基础原理,在平常使用中我们并不会像这样分步地对程序进行编译,一般都是直接使用 gcc demo.c -o demo
命令。
3️⃣ GCC 警告提示和代码优化
本小节主要学习 GCC 的警告提示和代码优化功能。GCC 包含完整的出错检查和警告提示功能,它们可以帮助 Linux 程序员写出更加专业和优美的代码。
我们千万不能小瞧这两个功能,在很多情况下,含有警告信息的代码往往会有意想不到的运行结果。代码优化则能通过编译器分析源码,找出其中尚未达到最优的部分,然后对其重新进行组合,目的是改善程序的执行功能。GCC 提供的警告提示和代码优化功能十分强大,我们接下来就对此进行介绍。
GCC 警告提示功能
GCC 代码优化
GCC 常用选项
1. GCC 警告提示功能
- pedantic
我们先来看一段能让 GCC 产生警告的代码,将这一段代码保存到桌面上。
#include <stdio.h>
void main(void)
long long int var = 2020;
printf("This is a bad code!\n");
这段代码有以下问题:
main
函数的返回值被声明为 void
,但实际上应该是 int
。
使用了 GNU 语法扩展,即使用 long long
来声明 64 位整数,不符合 ANSI/ISO C 语言标准。
main
函数在终止前没有调用 return
语句。
下面来看看 GCC 是如何帮助程序员来发现这些错误的,当 GCC 在编译不符合 ANSI/ISO C 语言标准的源代码时,如果加上了 -pedantic
选项,那么使用了扩展语法的地方将产生相应的警告信息。
打开终端,执行以下命令:
$ gcc -pedantic demo.c -o demo
现在我们对上面的程序进行修改,将 main
函数的返回值声明为 int
,var
变量定义为长整形(long int
),在 main
函数的最后增加返回语句return 0;
,如下所示:
#include <stdio.h>
int main(void)
long int var = 2020;
printf("This is a bad code!\n");
return 0;
然后再次使用 GCC 的 -pedantic
进行编译。
这个时候可能就有人有疑问了,明明有三个错误,为什么终端提示只有一个呢?
需要注意的是,-pedantic
编译选项并不能保证被编译程序与 ANSI/ISO C 语言标准完全兼容,它仅仅只能用来帮助程序员离这个目标越来越近。换句话说,-pedantic
选项能够帮助大家发现一些不符合 ANSI/ISO C 语言标准的代码,但不是全部。
事实上只有 ANSI/ISO C
语言标准中要求进行编译器诊断的那些问题才有可能被 GCC 发现并提出警告。
- Wall
除了-pedantic
之外,GCC 还有一些其他编译选项也能够产生有用的警告信息。这些选项大多以 -W
开头,其中最有价值的当数 -Wall
了,使用它能使 GCC 阐释尽可能多的警告信息:
GCC 给出的警告信息虽然从严格意义上说不能算作是错误,但很可能错误就藏身于这些地方。我们应该避免产生警告信息,所以我建议在用 GCC 编译源代码时始终带上 -Wall
选项,这对找出常见的隐式编程错误很有帮助。
2. GCC代码优化
代码优化是指编译器通过分析源代码,找出其中尚未达到最优的部分,然后对其重新进行组合,目的是改善程序的执行性能。
GCC 提供的代码优化功能非常强大,它通过编译选项 -On
来控制优化代码的生成,其中 n
是一个代表优化级别的整数。对于不同版本的 GCC 来讲,n
的取值范围及其对应的优化效果可能并不完全相同,比较典型的范围是从 0
变化到 2
或 3
。
编译时使用选项 -O
可以告诉 GCC 同时减小目标代码的长度和执行时间,其效果等价于 -O1
。
选项 -O2
告诉 GCC 除了完成 -O1
级别的优化之外,同时还要进行一些额外的调整工作,如处理器指令调度等。
选项 -O3
则除了完成 -O2
级别的优化之外,还包括循环展开和其他一些与处理器特性相关的优化工作。
通常来说,数字越大优化的等级越高,同时也意味着程序的运行速度越快。许多程序员喜欢使用 -O2
选项,因为它在优化长度、编译时间和代码大小之间取得了一个比较理想的平衡点。
现在我们来看一段效率很低的代码:
int main(void)
double counter
double result
double temp
for(counter = 0
temp = counter / 1979
result = counter
printf("Result is %lf\n", result)
return 0
在终端输入以下命令查看运行时间:
$ gcc demo.c -o demo
$ time ./demo
可以明显感受到不加任何优化选项进行编译时运行的缓慢,现在我们使用优化选项 -O
来对代码进行优化处理:
$ gcc -O demo.c -o demo
$ time ./demo
从运行结果可以看出,我们使用优化选项来对代码进行优化处理后,程序的性能得到了很大幅度的改善,从原来将近 6s 降低到 1s。如果我们使用优化的等级更高的选项 -O2
,-O3
:
你会发现-O2
、-O3
与 -O
的效果相差并不大,这是代表他们没有任何区别吗?
并不是。因为我们在这里只是一个很简单的程序,如果在大型的项目中,数字越大则优化的等级越高。
尽管 GCC 代码优化功能很强大,但是我们仍然要要求能写出高质量代码,这样编译器就不会做更多的工作。优化虽然能给程序带来更好的执行性能,但在一些场合中应避免优化代码:
程序开发的时候
:优化等级越高,消耗在编译上的时间就越长,因此在开发的时候最好不要使用优化选项,只有到软件发行或开发结束的时候,才考虑对最终生成的代码进行优化。
资源受限的时候
:一些优化选项会增加可执行代码的体积,如果程序在运行时能够申请到的内存资源非常紧张(如一些实时嵌入式设备),那就不要对代码进行优化,因为由这带来的负面影响可能会产生非常严重的后果。
跟踪调试的时候
:在对代码进行优化的时候,某些代码可能会被删除或改写,或者为了取得更佳的性能而进行重组,从而使跟踪和调试变得异常困难。
!4️⃣ GCC常用选项
学习到这里,GCC 的使用我们就告一段落了,下一节实验将会开始 GDB 的学习之旅。通过之前的学习,我们学习了一些 GCC 的选项,GCC 是一个功能强大的编译器,其编译选项非常多,有些选项通常不会用到,因此将所有的编译选项全部列出讲解是不明智的。下面只对一些 GCC 编译器的常用选项进行讲解,这些选项在实际编程过程中非常实用。
GCC 的常用选项如下表所示:
选项名 | 作用 |
---|
-c | 通知 GCC 取消连接步骤,即编译源码并在最后生成目标文件。 |
-Dmacro | 定义指定的宏,使它能够通过源码中的 #ifdef 进行检验。 |
-E | 不经过编译预处理程序的输出而输送至标准输出。 |
-g3 | 获得有关调试程序的详细信息,它不能与 -o 选项联合使用。 |
-Idirectory | 在包含文件搜索路径的起点处添加指定目录。 |
-llibrary | 提示连接程序在创建最终可执行文件时包含指定的库。 |
-O -O2 -O3 | 将优化状态打开,该选项不能与 -g 选项联合使用。当出现多个优化时,以最后一个为准。 |
-O0 | 关闭所有优化选项。 |
-S | 要求编译程序生成来自源代码的汇编程序输出。 |
-v | 启动所有警报。 |
.h | 预处理文件(标头文件)。 |
-Wall | 在发生警报时取消编译操作,即将警报看作是错误。 |
-w | 禁止所有的报警。 |
-share | 此选项将尽量使用动态库,所以生成文件比较小,但是需要系统由动态库。 |
-shared | 产生共享对象文件。 |
-g | 在编译结果中加入调试信息。 |
-ggdb | 加入 GDB 调试器能识别的格式。 |
本小节我们学习了以下知识点:
GCC 警告提示功能
GCC 代码优化
GCC 常用选项
对于 GCC 的学习就告一段落了,从下一节实验开始我们将学习 GDB 的使用。
Halfup
嵌入式软件
22.5k
粉丝