《嵌入式 Linux C 语言应用程序设计(修订版)》一2.3 嵌入式Linux编译器GCC的使用

本节书摘来自异步社区《嵌入式 Linux C 语言应用程序设计(修订版)》一书中的第2章,第2.3节,作者 孙琼,更多章节内容可以访问云栖社区“异步社区”公众号查看

2.3 嵌入式Linux编译器GCC的使用

嵌入式 Linux C 语言应用程序设计(修订版)

2.3.1 GCC概述

作为*软件的旗舰项目,Richard Stallman在十多年前刚开始写作GCC的时候,还只是仅仅把它当作一个C程序语言的编译器,GCC的意思也只是GNU C Compiler而已。

经过了这么多年的发展,GCC已经不仅仅能支持C语言,它现在还支持Ada语言、C++语言、Java语言、Objective C语言、PASCAL语言、COBOL语言,并支持函数式编程和逻辑编程的Mercury语言等。而GCC也不再单只GNU C语言编译器的意思了,而是变成了GNU编译器家族了。

正如前文中所述,GCC的编译流程分为了4个步骤,分别为。

预处理(Pre-Processing)。
编译(Compiling)。
汇编(Assembling)。
链接(Linking)。
编译器通过程序的扩展名可分辨编写原始程序码所用的语言,由于不同的程序所需要执行编译的步骤是不同的,因此GCC根据不同的后缀名对它们进行分别处理,表2.6指出了不同后缀名的处理方式。


《嵌入式 Linux C 语言应用程序设计(修订版)》一2.3 嵌入式Linux编译器GCC的使用

2.3.2 GCC编译流程分析

GCC使用的基本语法为:

gcc [option | filename]

这里的option是GCC使用时的一些选项,通过指定不同的选项GCC可以实现其强大的功能。这里的filename则是GCC要编译的文件,GCC会根据用户所指定的编译选项以及所识别的文件后缀名来对编译文件进行相应的处理。

本节从编译流程的角度讲解GCC的常见使用方法。

首先,这里有一段简单的C语言程序,该程序由两个文件组成,其中“hello.h”为头文件,在“hello.c”中包含了“hello.h”,其源文件如下所示。

/*hello.h*/
#ifndef _HELLO_H_
#define _HELLO_H_

typedef unsigned long val32_t;

#endif
/*hello.c*/
#include <stdio.h>
#include <stdlib.h>
#include "hello.h"

int main()
{
          val32_t i = 5;
          printf("hello, embedded world %d\n",i);
}

1.预处理阶段
GCC的选项“-E”可以使编译器在预处理结束时就停止编译,选项“-o”是指定GCC输出的结果,其命令格式为如下所示。

gcc –E –o [目标文件] [编译文件]

表2.6指出后缀名为“.i”的文件是经过预处理的C原始程序。要注意,“hello.h”文件是不能进行编译的,因此,使编译器在预处理后停止的命令如下所示。

[root@localhost gcc]# gcc –E –o hello.i hello.c

在此处,选项‘-o’是指目标文件,由2.6表可知,‘.i’文件为已经过预处理的C原始程序。以下列出了hello.i文件的部分内容。

# 2 "hello.c" 2
# 1 "hello.h" 1

typedef unsigned long val32_t;
# 3 "hello.c" 2

int main()
{ 
 val32_t i = 5;
 printf("hello, embedded world %d\n",i);
}

由此可见,GCC确实进行了预处理,它把“hello.h”的内容插入到hello.i文件中了。

2.编译阶段
编译器在预处理结束之后,GCC首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,就开始把代码翻译成汇编语言,GCC的选项“-S”能使编译器在进行完汇编之前就停止。由表2.6可知,“.s”是汇编语言原始程序,因此,此处的目标文件就可设为“.s”类型。

[root@localhost gcc]# gcc –S –o hello.s hello.i

以下列出了hello.s的内容,可见GCC已经将其转化为汇编了,感兴趣的读者可以分析一下这一行简单的C语言小程序用汇编代码是如何实现的。

.file   "hello.c"
           .section        .rodata
.LC0:
           .string "hello, embedded world %d\n"
           .text
.globl main
           .type   main, @function
main:
           pushl   %ebp
           movl    %esp, %ebp
           subl    $8, %esp
           andl    $-16, %esp
           movl    $0, %eax
           addl    $15, %eax
           addl    $15, %eax
           shrl    $4, %eax
           sall    $4, %eax
           subl    %eax, %esp
           movl    $5, -4(%ebp)
           subl    $8, %esp
           pushl   -4(%ebp)
           pushl   $.LC0
           call    printf
           addl    $16, %esp
           leave
           ret
           .size   main, .-main
           .section        .note.GNU-stack,"",@progbits
           . .ident "GCC: (GNU) 4.0.0 20050519 (Red Hat 4.0.0-8)"

可以看到,这一小段C语言的程序在汇编中已经复杂很多了,这也是C语言作为中级语言的优势所在。

3.汇编阶段
汇编阶段是把编译阶段生成的“.s”文件生成目标文件,读者在此使用选项“-c”就可看到汇编代码已转化为“.o”的二进制目标代码了。如下所示。

[root@localhost gcc]# gcc –c hello.s –o hello.o

4.链接阶段

在成功编译之后,就进入了链接阶段。在这里涉及一个重要的概念:函数库。

在这个程序中并没有定义“printf”的函数实现,在预编译中包含进的“stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实现“printf”函数的呢?

最后的答案是:系统把这些函数实现都已经被放入名为libc.so.6的库文件中去了,在没有特别指定时,GCC会到系统默认的搜索路径“/usr/lib”下进行查找,也就是链接到libc.so.6库函数中去,这样就能实现函数“printf”了,而这也就是链接的作用。

完成了链接之后,GCC就可以生成可执行文件,其命令如下所示。

[root@localhost gcc]# gcc hello.o –o hello

运行该可执行文件,出现正确的结果。

[root@localhost gcc]# ./hello
hello, embedded world 5

2.3.3 GCC警告提示

本节主要讲解GCC的警告提示功能。GCC包含完整的出错检查和警告提示功能,它们可以帮助Linux程序员写出更加专业和优美的代码。

读者千万不能小瞧这些警告信息,在很多情况下,含有警告信息的代码往往会有意想不到的运行结果。

首先读者可以先看一下以下这段代码:

#include<stdio.h>
void main(void)
{
          long long tmp = 1;
          printf("This is a bad code!\n");
}

虽然这段代码运行的结果是正确的,但还有以下问题。

main函数的返回值被声明为void,但实际上应该是int。
使用了GNU语法扩展,即使用long long来声明64位整数,不符合ANSI/ISO C语言标准。
main函数在终止前没有调用return语句。
GCC的警告提示选项有很多种类型,主要可分为“-Wall”类和非“-Wall”类。

1.Wall类警告提示
这一类警告提示选项占了GCC警告选项的90%以上,它不仅包含打开所有警告等功能,还可以单独对常见错误分别指定警告,这些常见的警告选项如表2.7所示(这些选项可供读者在实际操作时查阅使用)。


《嵌入式 Linux C 语言应用程序设计(修订版)》一2.3 嵌入式Linux编译器GCC的使用


《嵌入式 Linux C 语言应用程序设计(修订版)》一2.3 嵌入式Linux编译器GCC的使用

这些警告提示读者可以根据自己的不同情况进行相应的选择,这里最为常用的是“-Wall”,上面的这一小段程序使用该警告提示后的结果是:

[root@ft charpter2]# gcc -Wall wrong.c -o wrong 
wrong.c:4: warning: return type of 'main' is not 'int'
wrong.c: In function 'main':
wrong.c:5: warning: unused variable 'tmp'

可以看出,使用‘-Wall’选项找出了未使用的变量tmp以及返回值的问题,但没有找出无效数据类型的错误。

2.非Wall类警告提示
非Wall类的警告提示中最为常用的有以下两种:“-ansi”和“-pedantic”。

(1)“-ansi”

该选项强制GCC生成标准语法所要求的告警信息,尽管这还并不能保证所有没有警告的程序都是符合ANSI C标准的。使用该选项的运行结果如下所示:

[root@ft charpter2]# gcc -ansi wrong.c -o wrong
wrong.c: In function 'main':
wrong.c:4: warning: return type of 'main' is not 'int'
可以看出,该选项并没有发现“long long”这个无效数据类型的错误。

(2)“-pedantic”

该选项允许发出ANSI C标准所列的全部警告信息,同样也保证所有没有警告的程序都是符合ANSI C标准的。使用该选项的运行结果如下所示:

[root@ft charpter2]# gcc -pedantic wrong.c -o wrong    
wrong.c: In function 'main':
wrong.c:5: warning: ISO C90 does not support 'long long'
wrong.c:4: warning: return type of 'main' is not 'int'
可以看出,使用该选项查看出了“long long”这个无效数据类型的错误。

2.3.4 GCC使用库函数

1.Linux函数库介绍
函数库可以看做是事先编写的函数集合,它可以与主函数分离,从而增加程序开发的复用性。Linux中函数库可以有3种使用的形式:静态、共享和动态。

静态库的代码在编译时就已连接到开发人员开发的应用程序中,而共享库只是在程序开始运行时才载入。

动态库也是在程序运行时载入,但与共享库不同的是,动态库使用的库函数不是在程序运行使开始载入,而是在程序中的语句需要使用该函数时才载入。动态库可以在程序运行期间释放动态库所占用的内存,腾出空间供其他程序使用。

由于共享库和动态库并没有在程序中包括库函数的内容,只是包含了对库函数的引用,因此代码的规模比较小。

系统中可用的库都存放在/usr/lib和/lib目录中。库文件名由前缀lib和库名以及后缀组成。根据库的类型不同,后缀名也不一样。

Stnd001注意 共享库和动态库的后缀名由.so和版本号组成。

静态库的后缀名为.a。
如:数学共享库的库名为libm.so.5,这里的标识字符为m,版本号为5,libm.a则是静态数学库。在Linux系统中系统所用的库都存放在/usr/lib和/lib目录中。

2.相关路径选项
由于库文件的通常路径不是在系统默认的路径下,因此,首先要使用调用路径选项来指定相关的库文件位置,这里首先讲解两个常用选项的使用方法。

(1)“-I dir”

在GCC中使用头文件在默认情况下是在主程序中所设定的路径,那么如果想要改变该路径,用户则可以使用“-I”选项。“-I dir”选项可以在头文件的搜索路径列表中添加dir目录。这时,GCC就会到相应的位置查找对应的目录。

比如在“/root/workplace/gcc”下有两个文件:

hello.c
#include<my.h>
int main()
{
      printf("Hello!!\n");
      return 0;
}
my.h
#include<stdio.h>

这样,就可在GCC命令行中加入“-I”选项,其命令如下所示。

[root@localhost gcc] gcc hello.c –I /root/workplace/gcc/ -o hello

这样,GCC就能够执行出正确结果。

0115小技巧 在include语句中,“<>”表示在标准路径中搜索头文件,在Linux中默认为“/usr/include”。故在上例中,可把hello1.c的“#include”改为“#include "my.h"”,这样就不需要加上“-I”选项了。
(2)“-L dir”

选项“-L dir”的功能与“-I dir”类似,其区别就在于“-L”选项是用于指明库文件的路径。例如有程序hello_sq.c需要用到目录“/root/workplace/gcc/lib”下的一个动态库libsunq.so,则只需键入如下命令即可。

[root@localhost gcc] gcc hello_sq.c –L /root/workplace/gcc/lib –lsunq –o hello_sq

Stnd001注意 ‘-I dir’和‘-L dir’都只是指定了路径,而没有指定文件,因此不能在路径中包含文件名。
3.使用3种类型链接库
使用上述3种类型的链接库的方法很相似,都是使用选项是“-l”(注意这里是小写的“L”)。该选项是用于指明具体使用的库文件。由于在Linux中函数库的命名规则都是以“lib”开头的,因此,这里的库文件只需填写lib之后的内容即可。

如:有静态库文件libm.a,在调用时只需写作“-lm”;同样对于动态库文件libm.so;在调用时也只需写作“-lm”即可,其整体调用命令类似如下:

[root@localhost gcc] gcc -o dynamic -L /root/lq/testc/lib/dynamic.o -lmydynamic

那么,若系统中同时存在文件名相同的静态库文件和动态库文件时,该链接选项究竟会调用静态库文件还是动态库文件呢?

经测试后可以发现,系统调用的是动态库文件,这是由于Linux系统中默认的是采用动态链接的方式。这样,若用户要调用含有同名动态库文件的静态库文件,则在“-l”后需要显示地写出包含后缀名的文件名,如:要调用libm.a库文件时就需写作“-llibm.a”。

2.3.5 GCC代码优化

GCC可以对代码进行优化,它通过编译选项-On来控制优化代码的生成,其中n是一个代表优化级别的整数。对于不同版本的GCC来讲,n的取值范围及其对应的优化效果可能并不完全相同,比较典型的范围是从0变化到2或3。

不同的优化级别对应不同的优化处理工作,如使用优化选项-O主要进行线程跳转(Thread Jump)和延迟退栈(Deferred Stack Pops)两种优化。

使用优化选项-O2除了完成所有-O1级别的优化之外,同时还要进行一些额外的调整工作,如处理器指令调度等;选项-O3则还包括循环展开和其他一些与处理器特性相关的优化工作。

虽然优化选项可以加速代码的运行速度,但对于调试而言将是一个很大的挑战。因为代码在经过优化之后,原先在源程序中声明和使用的变量很可能不再使用,控制流也可能会突然跳转到意外的地方,循环语句也有可能因为循环展开而变得到处都有,所有这些都将使调试工作异常坚难。

建议在调试的时候最好不使用任何优化选项,只有当程序在最终发行的时候才考虑对其进行优化。

上一篇:linux内核中的C语言常规算法(前提:你的编译器要支持typeof和type)


下一篇:Linux升级NTPD服务器-编译安装ntp-4.2.8p9与配置