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

1.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根据不同的扩展名对它们进行相应的处理,如表1.6所示指出了不同扩展名的处理方式。

表1.6 GCC所支持扩展名解释

1.3.2 GCC编译流程分析

GCC使用的基本语法如下:

gcc [opti0n | 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 int u32_t;
#endif
/*hello.c*/
#include <stdio.h>
#include "hello.h"
int main()
{
    u32_t i=5;
    printf("hello, embedded world %d\n",i);
    return 0;
}

1.预处理阶段

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

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

表1.6指出扩展名为“.i”的文件是经过预处理的C源程序。要注意,“hello.h”文件是不能进行编译的:

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

在此处,选项“-o”指定要生成的文件。由表1.6可知,“.i”文件为已经过预处理的C源程序。以下列出了hello.i文件的部分内容:

# 2 "hello.c" 2
# 1 "hello.h" 1
typedef unsigned int u32_t;
# 3 "hello.c" 2
int main()
{
    u32_t i=5;
    printf("hello, embedded world %d\n",i);
    return 0;
}

由此可见,GCC在预处理阶段把“hello.h”的内容添加到了hello.i。

2.编译阶段

编译器在预处理结束之后进行编译。GCC首先要检查代码的规范性、是否有语法错误等,以确定代码实际要做的工作。在检查无误后,就开始把代码翻译成汇编语言,GCC的选项“-S”能使编译器在进行完编译之后就停止。由表1.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会到系统默认的搜索路径/lib或/usr/lib下进行查找。找到libc.so.6后对需要的函数进行处理,这样就能在程序执行时调用函数“printf”,而这也就是链接的作用。

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

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

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

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

1.3.3 GCC警告提示

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

千万不要忽视这些警告信息,在很多情况下,含有警告信息的代码往往会有意想不到的运行结果。

读者可以先阅读以下代码:

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

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

· ain函数的返回值被声明为void,但实际上应该是int。

· 使用了GNU语法扩展,即使用long long来声明64位整数,不符合ANSI/ISO C语言标准。

· main函数在终止前没有调用return语句。

GCC的警告提示选项有很多种类型,主要可分为Wall类和非Wall类。

1.Wall类警告提示

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

表1.7 GCC的Wall类警告提示选项

这些警告提示读者可以根据自己的不同情况进行相应的选择,这里最为常用的是“-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”这个无效数据类型的错误。

1.3.4 GCC使用库函数

1.Linux函数库介绍

函数库可以看做是事先编写的函数集合,它可以与主函数分离,使得程序模块化,从而增加代码的复用性。Linux中函数库包括两类:静态库和共享库。

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

由于在使用共享库时程序中并不包括库函数的实现代码,只是包含了对库函数的引用,因此程序代码的规模比较小。

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

注意

共享库的扩展名由.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就能够正确地编译程序。

小持巧

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

2)-L <dir>

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

[root@localhost gcc] gcc hello_sq.c -L/root/workspace/gcc/lib -lsunq -o hello_sq

注意

“-I <dir>”和“-L< dir>”都只是指定了路径,而没有指定文件,因此不能在路径中包含文件名。

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

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

GCC默认链接的是共享库,这是由于Linux系统中默认的是采用动态链接的方式。如果用户要链接同名的静态库,则在“-l”之前需要添加选项“-static”。例如,链接libm.a库文件的选项是“-static -lm”。

1.3.5 GCC代码优化

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

不同的优化级别对应不同的优化处理工作。如使用优化选项-O主要进行线程跳转(Thread Jump)和延迟退栈(Deferred Stack Pops)两种优化。使用优化选项-O2除了完成所有-O1级别的优化之外,同时还要进行一些额外的调整工作,如处理器指令调度等;选项-O3则还包括循环展开和其他一些与处理器特性相关的优化工作。

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

建议开发人员在调试程序的时候不使用任何优化选项。只有当程序完成调试,准备发布的时候再对其进行优化。