- 从实践中学嵌入式Linux C编程
- 华清远见嵌入式学院 曹宏安编著
- 3699字
- 2020-08-28 15:17:48
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则还包括循环展开和其他一些与处理器特性相关的优化工作。
虽然优化选项可以加快代码的运行速度,但对于调试而言将是一个很大的挑战。因为代码在经过优化之后,原先在源程序中声明和使用的变量很可能不再使用;控制流也可能会突然跳转到其他的地方;循环语句也有可能因为循环展开而变得到处都有,所有这些都将使调试工作异常艰难。
建议开发人员在调试程序的时候不使用任何优化选项。只有当程序完成调试,准备发布的时候再对其进行优化。