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个步骤,分别为:
预处理(Pre-Processing);
编译(Compiling);
汇编(Assembling);
链接(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”。
“-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应用程序会链接
支持的相关图形库等。
函数库有静态库和动态库两种,静态库是一系列的目标文件(.o 文件)的归档文件(文件名格
libname.a),如果在编译某个程序时链接静态库,则链接器将会搜索静态库,从中提取出它所需要的
文件并直接复制到该程序的可执行二进制文件(ELF格式文件)之中;动态库(文件名格式为libname
版本号.次版本号.发行号])在程序编译时并不会被链接到目标代码中,而是在程序运行时才被载入。
下面举一个简单的例子,讲解如何怎么创建和使用这两种函数库。
首先创建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文件可以制作一个函数库。下面分别讲解怎么生成静态库和动态库。
静态库的创建和使用。
创建静态库比较简单,使用归档工具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目录,而“-lna
选项指示编译时链接到库文件libname.a 或者libname.so。本实例中,程序pow_test.c需要使用当前目录