3.3  gcc编译器

GNU CC(简称为gcc)是GNU项目中符合ANSI C标准的编译系统,能够编译用C、C++和Object C等语言编写的程序。gcc不仅功能强大,而且可以编译如C、C++、Object C、Java、Fortran、Pascal、Modula-3和Ada等多种语言,而且gcc是一个交叉平台编译器,它能够在当前CPU平台上为多种不同体系结构的硬件平台开发软件,因此尤其适合在嵌入式领域的开发编译。本章中的示例,除非特别注明,否则均采用4.x.x的gcc版本。

 

表3.6所示为gcc支持编译源文件的后缀及其解释。

表3.6 gcc所支持后缀名解释

后  缀  名

所对应的语言

后  缀  名

所对应的语言

.c

C原始程序

.s/.S

汇编语言原始程序

.C/.cc/.cxx

C++原始程序

.h

预处理文件(头文件)

.m

Objective-C原始程序

.o

目标文件

.i

已经过预处理的C原始程序

.a/.so

编译后的库文件

.ii

已经过预处理的C++原始程序

 

3.3.1  gcc编译流程解析

如本章开头提到的,gcc的编译流程分为了4个步骤,分别为:

n 预处理(Pre-Processing);

n 编译(Compiling);

n 汇编(Assembling);

n 链接(Linking)。

下面就具体来查看一下gcc是如何完成以上4个步骤的。

 

首先看一下hello.c的源代码:

 

#include <stdio.h>

int main()

{

      printf("Hello! This is our embedded world!\n");

      return 0;

}

(1)预处理阶段。

在该阶段,对包含的头文件(#include)和宏定义(#define、#ifdef等)进行处理。在上述代码的预处理过程中,编译器将包含的头文件stdio.h编译进来,并且用户可以使用gcc的选项“-E”进行查看,该选项的作用是让gcc在预处理结束后停止编译过程。

 

注意

gcc指令的一般格式为:gcc [选项] 要编译的文件 [选项] [目标文件]

其中,目标文件可缺省,gcc默认生成可执行的文件,名为:编译文件.out 

 

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

 

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

 

typedef int (*__gconv_trans_fct) (struct __gconv_step *,

         struct __gconv_step_data *, void *,

         __const unsigned char *,

         __const unsigned char **,

         __const unsigned char *, unsigned char **,

         size_t *);

# 2 "hello.c" 2

int main()

{

    printf("Hello! This is our embedded world!\n");

    return 0;

}

 

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

 

(2)编译阶段。

接下来进行的是编译阶段,在这个阶段中,gcc首先要检查代码的规范性、是否有语法错误等,以确定代码实际要做的工作,在检查无误后,gcc把代码翻译成汇编语言。用户可以使用“-S”选项来进行查看,该选项只进行编译而不进行汇编,结果生成汇编代码。

 

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

 

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

 

     .file  "hello.c"

     .section  .rodata

     .align 4

.LC0:

     .string        "Hello! This is our embedded world!"

     .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

     subl $12, %esp

     pushl $.LC0

     call puts

     addl $16, %esp

     movl $0, %eax

     leave

     ret

     .size  main, .-main

     .ident "GCC: (GNU) 4.0.0 200XYZ19 (Red Hat 4.0.0-8)"

     .section   .note.GNU-stack,"",@progbits

 

(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”了,而这也正是链接的作用。

 

函数库有静态库和动态库两种。静态库是指编译链接时,将库文件的代码全部加入可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名通常为“.a”。动态库与之相反,在编译链接时并没有将库文件的代码加入可执行文件中,而是在程序执行时加载库,这样可以节省系统的开销。一般动态库的后缀名为“.so”,如前面所述的libc.so.6就是动态库。gcc在编译时默认使用动态库。

 

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

 

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

 

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

 

[root@localhost gcc]# ./hello

Hello! This is our embedded world!

 

3.3.2  gcc编译选项分析

gcc有超过100个可用选项,主要包括总体选项、告警和出错选项、优化选项和体系结构相关选项。以下对每一类中最常用的选项进行讲解。

 

(1)常用选项。

gcc的常用选项如表3.7所示,很多在前面的示例中已经有所涉及。

 

表3.7 gcc常用选项列表

选    项

含    义

-c

只编译不链接,生成目标文件“.o”

-S

只编译不汇编,生成汇编代码

-E

只进行预编译,不做其他处理

-g

在可执行程序中包含标准调试信息

-o file

将file文件指定为输出文件

-v

打印出编译器内部编译各过程的命令行信息和编译器的版本

-I dir

在头文件的搜索路径列表中添加dir目录

 

前一小节已经讲解了“-c”、“-E”、“-o”、“-S”选项的使用方法,在此主要讲解另外2个非常常用的库依赖选项“-I dir”。

n “-I dir”

正如上表中所述,“-I dir”选项可以在头文件的搜索路径列表中添加dir目录。由于Linux中头文件都默认放到了“/usr/include/”目录下,因此,当用户希望添加放置在其他位置的头文件时,就可以通过“-I dir”选项来指定,这样,gcc就会到相应的位置查找对应的目录。

 

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

 

/* hello1.c */

#include<my.h>

int main()

{

     printf("Hello!!\n");

     return 0;

}

/* my.h */

#include<stdio.h>

 

这样,就可在gcc命令行中加入“-I”选项:

 

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

 

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

 

小知识

在include语句中,“<>”表示在标准路径中搜索头文件,““””表示在本目录中搜索。故在上例中,可把hello1.c的“#include<my.h>”改为“#include “my.h””,就不需要加上“-I”选项了。

 

(2)库选项。

gcc库选项如表3.8所示。

 

表3.8 gcc库选项列表

选    项

含    义

-static

进行静态编译,即链接静态库,禁止使用动态库

-shared

1.可以生成动态库文件

2.进行动态编译,尽可能地链接动态库,只有当没有动态库时才会链接同名的静态库(默认选项,即可省略)

-L dir

在库文件的搜索路径列表中添加dir目录

-lname

链接称为libname.a(静态库)或者libname.so(动态库)的库文件。若两个库都存在,则根据编译方式(-static还是-shared)而进行链接

-fPIC(或-fpic)

生成使用相对地址的位置无关的目标代码(Position Independent Code)。然后通常使用gcc的-static选项从该PIC目标文件生成动态库文件

 

我们通常需要将一些常用的公共函数编译并集成到二进制文件(Linux的ELF格式文件),以便其他程序可重复地使用该文件中的函数,此时将这种文件叫做函数库,使用函数库不仅能够节省很多内存和存储器的空间资源,而且更重要的是大大降低开发难度和开销,提高开发效率并增强程序的结构性。实际上,在Linux中的每个程序都会链接到一个或者多个库。比如使用C函数的程序会链接到C运行时库,Qt应用程序会链接到Qt支持的相关图形库等。

 

函数库有静态库和动态库两种,静态库是一系列的目标文件(.o文件)的归档文件(文件名格式为libname.a),如果在编译某个程序时链接静态库,则链接器将会搜索静态库,从中提取出它所需要的目标文件并直接复制到该程序的可执行二进制文件(ELF格式文件)之中;动态库(文件名格式为libname.so[.主版本号.次版本号.发行号])在程序编译时并不会被链接到目标代码中,而是在程序运行时才被载入。

 

下面举一个简单的例子,讲解如何怎么创建和使用这两种函数库。

 

首先创建unsgn_pow.c文件,它包含unsgn_pow()函数的定义,具体代码如下所示。

 

/* unsgn_pow.c:库程序 */

unsigned long long unsgn_pow(unsigned int x, unsigned int y)

{

    unsigned long long res = 1;    

    if (y == 0)

    {

        res = 1;

    }

    else if (y == 1)

    {

        res = x;

    }

    else

    {

        res = x * unsgn_pow(x, y - 1);

    }

    return res;

}

 

然后创建pow_test.c文件,它会调用unsgn_pow()函数。

 

/* pow_test.c */

#include <stdio.h>

#include <stdlib.h>

int main(int argc, char *argv[])

{

unsigned int x, y;

unsigned long long res;

 

if ((argc < 3) || (sscanf(argv[1], "%u", &x) != 1) 

|| (sscanf(argv[2], "%u", &y)) != 1)

{

printf("Usage: pow base exponent\n");

exit(1);

}

res = unsgn_pow(x, y);

printf("%u ^ %u = %u\n", x, y, res);

exit(0);

}

 

我们用unsgn_pow.c文件可以制作一个函数库。下面分别讲解怎么生成静态库和动态库。

 

n 静态库的创建和使用。

 

创建静态库比较简单,使用归档工具ar将一些目标文件集成在一起。

 

[root@localhost lib]# gcc -c unsgn_pow.c 

[root@localhost lib]# ar rcsv libpow.a unsgn_pow.o

a - unsgn_pow.o

 

下面编译主程序,它将会链接到刚生成的静态库libpow.a。具体运行结果如下所示。

[root@localhost lib]#  gcc -o pow_test pow_test.c -L. –lpow

[root@localhost lib]#  ./pow_test  2  10

2 ^ 10 = 1024

 

其中,选项“-L dir”的功能与“-I dir”类似,能够在库文件的搜索路径列表中添加dir目录,而“-lname”选项指示编译时链接到库文件libname.a或者libname.so。本实例中,程序pow_test.c需要使用当前目录下的一个静态库libpow.a。 

 

n 动态库的创建和使用。

 

首先使用gcc的-fPIC选项为动态库构造一个目标文件

 

[root@localhost lib]# gcc -fPIC -Wall -c unsgn_pow.c

 

接下来,使用-shared选项和已创建的位置无关目标代码,生成一个动态库libpow.so。

 

[root@localhost lib]# gcc -shared -o libpow.so unsgn_pow.o

 

下面编译主程序,它将会链接到刚生成的动态库libpow.so。

 

[root@localhost lib]#  gcc -o pow_test pow_test.c -L. –lpow

 

在运行可执行程序之前,需要注册动态库的路径名。其方法有几种:修改/etc/ld.so.conf文件,或者修改LD_LIBRARY_PATH环境变量,或者将库文件直接复制到/lib或者/usr/lib目录下(这两个目录为系统的默认的库路径名)。

 

[root@localhost lib]#  cp libpow.so /lib

[root@localhost lib]#  ./pow_test  2  10

2 ^ 10 = 1024

 

动态库只有当使用它的程序执行时才被链接使用,而不是将需要的部分直接编译入可执行文件中,并且一个动态库可以被多个程序使用故可称为共享库,而静态库将会整合到程序中,因此在程序执行时不用加载静态库。 从而可知,链接到静态库会使用户的程序臃肿,并且难以升级,但是可能会比较容易部署。而链接到动态库会使用户的程序轻便,并且易于升级,但是会难以部署。

 

(3)告警和出错选项。

gcc的告警和出错选项如表3.9所示。

 

表3.9 gcc警告和出错选项选项列表

选    项

含    义

-ansi

支持符合ANSI标准的C程序

-pedantic

允许发出ANSI C标准所列的全部警告信息

-pedantic-error

允许发出ANSI C标准所列的全部错误信息

-w

关闭所有告警

-Wall

允许发出gcc提供的所有有用的报警信息

-werror

把所有的告警信息转化为错误信息,并在告警发生时终止编译过程

 

下面结合实例对这几个告警和出错选项进行简单的讲解。

有以下程序段:

 

#include<stdio.h>

 

void main()

{

     long long tmp = 1;

     printf("This is a bad code!\n");

     return 0;

}

这是一个很糟糕的程序,读者可以考虑一下有哪些问题。

n “-ansi”

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

 

[root@localhost gcc]# gcc –ansi warning.c –o warning

warning.c: 在函数“main”中:

warning.c:7 警告:在无返回值的函数中,“return”带返回值

warning.c:4 警告:“main”的返回类型不是“int”

 

可以看出,该选项并没有发现“long long”这个无效数据类型的错误。

n “-pedantic”

打印ANSI C标准所列出的全部警告信息,同样也保证所有没有警告的程序都是符合ANSI C标准的。其运行结果如下所示:

 

[root@localhost gcc]# gcc –pedantic warning.c –o warning

warning.c: 在函数“main”中:

warning.c:5 警告:ISO C90不支持“long long”

warning.c:7 警告:在无返回值的函数中,“return”带返回值

warning.c:4 警告:“main”的返回类型不是“int”

 

可以看出,使用该选项查出了“long long”这个无效数据类型的错误。

n “-Wall”

打印gcc能够提供的所有有用的报警信息。该选项的运行结果如下所示:

 

[root@localhost gcc]# gcc –Wall warning.c –o warning

warning.c:4 警告:“main”的返回类型不是“int”

warning.c: 在函数“main”中:

warning.c:7 警告:在无返回值的函数中,“return”带返回值

warning.c:5 警告:未使用的变量“tmp”

 

使用“-Wall”选项找出了未使用的变量tmp,但它并没有找出无效数据类型的错误。

另外,gcc还可以利用选项对单独的常见错误分别指定警告。

 

(4)优化选项。

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

 

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

 

虽然优化选项可以加速代码的运行速度,但对于调试而言将是一个很大的挑战。因为代码在经过优化之后,原先在源程序中声明和使用的变量很可能不再使用,控制流也可能会突然跳转到意外的地方,循环语句也有可能因为循环展开而变得到处都有,所有这些对调试来讲都将是一场噩梦。所以笔者建议在调试的时候最好不使用任何优化选项,只有当程序在最终发行的时候才考虑对其进行优化。

 

(5)体系结构相关选项。

gcc的体系结构相关选项如表3.10所示。

 

表3.10 gcc体系结构相关选项列表

选    项

含    义

-mcpu=type

针对不同的CPU使用相应的CPU指令。可选择的type有i386、i486、pentium及i686等

-mieee-fp

使用IEEE标准进行浮点数的比较

-mno-ieee-fp

不使用IEEE标准进行浮点数的比较

-msoft-float

输出包含浮点库调用的目标代码

-mshort

把int类型作为16位处理,相当于short int

-mrtd

强行将函数参数个数固定的函数用ret NUM返回,节省调用函数的一条指令

 

这些体系结构相关选项在嵌入式的设计中会有较多的应用,读者需根据不同体系结构将对应的选项进行组合处理。在本书后面涉及具体实例时将会有针对性的讲解。