gcc编译流程及编译选项分析

2019-07-13 04:02发布

摘自《嵌入式linux应用程序开发详解》第三章 GNU CC(简称为 Gcc)是 GNU 项目中符合 ANSI C 标准的编译系统,能够编译用 C、C++和 Object C 等语言编写的程序。Gcc 不 仅功能强大,而且可以编译如 C、C++、Object C、Java、Fortran、Pascal、Modula-3 和 Ada 等多种语言,而且 Gcc 又是一个交 叉平台编译器,它能够在当前 CPU 平台上为多种不同体系结构的硬件平台开发软件, 因此尤其适合在嵌入式领域的开发编译。 Gcc 所支持后缀名解释 .c C 原始程序 .s/.S 汇编语言原始程序 .C/.cc/.cxx C++原始程序 .h 预处理文件(头文件) .m Objective-C 原始程序 .o 目标文件 .i 已经过预处理的 C 原始程序 .a/.so 编译后的库文件 .ii 已经过预处理的 C++原始程序 Gcc 的编译流程分为了 4 个步骤,分别为: • 预处理(Pre-Processing);
• 编译(Compiling);
• 汇编(Assembling);
• 链接(Linking)。 下面就具体来查看一下 Gcc 是如何完成 4 个步骤的。 首先,有以下 hello.c 源代码:
#include
int main()
{
printf(”Hello! This is our embedded world!/n”);
return 0;
}
(1)预处理阶段 在该阶段,编译器将上述代码中的 stdio.h 编译进来,并且用户可以使用 Gcc 的选项“-E”进行查看,该选项的作用是让 Gcc 在预处理结束后停止编译过程。 注意,Gcc 指令的一般格式为:Gcc [选项] 要编译的文件 [选项] [目标文件] 其中,目标文件可缺省,Gcc 默认生成可执行的文件,命为:编译文件.out
[root@localhost Gcc]# Gcc –E hello.c –o hello.i
在此处,选项“-o”是指目标文件,“.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 20050519 (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!
Gcc 编译选项分析 Gcc 有超过 100 个的可用选项,主要包括总体选项、告警和出错选项、优化选项和体系结构相关选项。以下对每一类中最常用的选项进行讲解。 (1)总体选项 Gcc 的总结选项如表 3.7 所示,很多在前面的示例中已经有所涉及。 Gcc 总体选项列表: -c 只是编译不链接,生成目标文件“.o” -S 只是编译不汇编,生成汇编代码 -E 只进行预编译,不做其他处理 -g 在可执行程序中包含标准调试信息 -o file 把输出文件输出到 file 里 -v 打印出编译器内部编译各过程的命令行信息和编译器的版本 -I dir 在头文件的搜索路径列表中添加 dir 目录 -L dir 在库文件的搜索路径列表中添加 dir 目录 -static 链接静态库 -llibrary 连接名为 library 的库文件 对于“-c”、“-E”、“-o”、“-S”选项在前一小节中已经讲解了其使用方法,在此主要讲解另外两个非常常用的库依赖选项“-I dir”和“-L dir”。 • “-I dir” 正如上表中所述, “-I dir”选项可以在头文件的搜索路径列表中添加 dir 目录。由于 Linux中头文件都默认放到了“/usr/include/”目 录下,因此,当用户希望添加放置在其他位置的头文件时,就可以通过“-I dir”选项来指定,这样,Gcc 就会到相应的位置查找对应的目 录。比如在“/root/workplace/Gcc”下有两个文件:
/*hello1.c*/
#include
int main()
{
printf(”Hello!!/n”);
return 0;
}
/*my.h*/
#include
这样,就可在 Gcc 命令行中加入“-I”选项:
[root@localhost Gcc] Gcc hello1.c –I /root/workplace/Gcc/ -o hello1
这样,Gcc 就能够执行出正确结果。 小知识:
在 include 语句中, “<>”表示在标准路径中搜索头文件,“” “ ”表示在本目录中搜索。故在上例中,可把 hello1.c 的“#include”改为“#include “my.h””,就不需要加上“-I”选项了。 • “-L dir” 选项“-L dir”的功能与“-I dir”类似,能够在库文件的搜索路径列表中添加 dir 目录。例如有程序 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
需要注意的是, “-I dir”和“-L dir”都只是指定了路径,而没有指定文件,因此不能在路径中包含文件名。 另外值得详细解释一下的是 “-l”选项, 它指示 Gcc 去连接库文件 libsunq.so。由于在 Linux下的库文件命名时有一个规定:必须以 l、i、b 3 个字母开头。因此在用-l 选项指定链接的库文件名时可以省去 l、i、b 3 个字母。也就是说 Gcc 在对“-lsunq”进行处理 时,会自动去链接名为 libsunq.so 的文件。 (2)告警和出错选项 Gcc 的告警和出错选项如下所示: -ansi 支持符合 ANSI 标准的 C 程序 -pedantic 允许发出 ANSI C 标准所列的全部警告信息 -pedantic-error 允许发出 ANSI C 标准所列的全部错误信息 -w 关闭所有告警 -Wall 允许发出 Gcc 提供的所有有用的报警信息 -werror 把所有的告警信息转化为错误信息,并在告警发生时终止编译过程 下面结合实例对这几个告警和出错选项进行简单的讲解。 如有以下程序段:
#include
void main()
{
long long tmp = 1;
printf(”This is a bad code!/n”);
return 0;
}
这是一个很糟糕的程序,读者可以考虑一下有哪些问题? • “-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”这个无效数据类型的错误。 • “-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”这个无效数据类型的错误。 • “-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 还可以利用选项对单独的常见错误分 别指定警告,有关具体选项的含义感兴趣的读者可以查看 Gcc 手册进行学习。 (3)优化选项 Gcc 可以对代码进行优化,它通过编译选项“-On”来控制优化代码的生成,其中 n 是一个代表优化级别的整数。对于不同版本的 Gcc 来讲,n 的取值范围及其对应的优化效果可能并不完全相同,比较典型的范围是从 0 变化到 2 或 3。不同的优化级别对应不同的优化 处理工作。如使用优化选项“-O”主要进行线程跳转(Thread Jump)和延迟退栈(Deferred Stack Pops)两种优化。使用优化选项“- O2”除了完成所有“-O1”级别的优化之外,同时还要进行一些额外的调整工作,如处理器指令调度等。选项“-O3”则还包括循环展开和 其他一些与处理器特性相关的优化工作。虽然优化选项可以加速代码的运行速度,但对于调试而言将是一个很大的挑战。因为代 码在经过优化之后,原先在源程序中声明和使用的变量很可能不再使用,控制流也可能会突然跳转到意外的地方,循环语句也有可能 因为循环展开而变得到处都有,所有这些对调试来讲都将是一场噩梦。所以笔者建议在调试的时候最好不使用任何优化选项,只有当 程序在最终发行的时候才考虑对其进行优化。 (4)体系结构相关选项 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 返回,节省调用函数的一条指令 这些体系结构相关选项在嵌入式的设计中会有较多的应用,读者需根据不同体系结构将对应的选项进行组合处理。