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 #在 gcc 命令后面紧跟源文件
    

    我们能够发现在桌面上多了一个名为 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 #在 gcc 命令后面紧跟源文件
    

    我们能够发现在桌面上多了一个名为 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 的基本包之一,它提供访问计算机的基本功能,包含了像 printfputs 等方法,这就是为什么它经常包含在最基本的 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 产生警告的代码,将这一段代码保存到桌面上。

    //demo.c
    #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 函数的返回值声明为 intvar 变量定义为长整形(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 变化到 23

  • 编译时使用选项 -O 可以告诉 GCC 同时减小目标代码的长度和执行时间,其效果等价于 -O1
  • 选项 -O2 告诉 GCC 除了完成 -O1 级别的优化之外,同时还要进行一些额外的调整工作,如处理器指令调度等。
  • 选项 -O3 则除了完成 -O2 级别的优化之外,还包括循环展开和其他一些与处理器特性相关的优化工作。
  • 通常来说,数字越大优化的等级越高,同时也意味着程序的运行速度越快。许多程序员喜欢使用 -O2 选项,因为它在优化长度、编译时间和代码大小之间取得了一个比较理想的平衡点。
  • 现在我们来看一段效率很低的代码:

    #include<stdio.h>
    int main(void)
        double counter;
        double result;
        double temp;
        for(counter = 0; counter < 2020.0*2020.0*2020.0/20.0+2020; counter += (5-1)/4)
            temp = counter / 1979;
            result = counter;
        printf("Result is %lf\n", result);
        return 0;
    

    在终端输入以下命令查看运行时间:

    $ gcc demo.c -o demo #不加任何优化选项进行编译
    $ time ./demo #借助Linux提供的time命令,统计出改程序在运行时所需的时间
    

    可以明显感受到不加任何优化选项进行编译时运行的缓慢,现在我们使用优化选项 -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
    粉丝