一、GCC编译过程
在使用gcc编译程序时,编译过程可以细分为4个阶段:
- 预处理(Pre-Processing)
- 编译(Compiling)
- 汇编(Assembling)
- 链接(Linking)
二、使用GCC
查看GCC版本
gcc -v
各个编译阶段
这里引入一个例子
// hello.c
#include
int main (int argc,char **argv) {
printf("Hello Linux\n");
}
- 编译这个程序,只要在命令行下执行如下命令:
gcc hello.c -o hello
./hello
- 采用模块化设计中,编译多个C文件
gcc david.c xueer.c -o davidxueer
- (1)预编译
使用-E
参数可以让gcc在预处理结束后停止编译过程:
gcc -E hello.c -o hello.i
此时若查看hello.i文件中的内容,会发现stdio.h的内容确实都插到文件里去了,而且被预处理的宏定义也都作了相应的处理。
# 1 "hello.c"
# 1 ""
# 1 ""
# 31 ""
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "" 2
# 1 "hello.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 27 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 1 3 4
# 33 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 3 4
# 1 "/usr/include/features.h" 1 3 4
# 424 "/usr/include/features.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 1 3 4
...
- (2)编译
编译过程通过词法和语法分析,确认所有指令符合语法规则(否则报编译错),之后翻译成对应的中间码,在linux中被称为RTL(Register Transfer Language),通常是平台无关的,这个过程也被称为编译前端。编译后端对RTL树进行裁减,优化,得到在目标机上可执行的汇编代码。gcc采用as作为其汇编器,所以汇编码是AT&T格式的,而不是Intel格式,所以在用gcc编译嵌入式汇编时,也要采用AT&T格式。
使用-S
命令
gcc -S hello.i -o hello.S
gcc默认将.i文件看成是预处理后的C语言源代码,因此上述命令将自动跳过预处理步骤而开始执行编译过程。
也可以使用-x参数让gcc从指定的步骤开始编译为目标代码。
以下为编译后的输出文件hello.s的内容
.file "hello.c"
.text
.section .rodata
.LC0:
.string "hello linux"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
leaq .LC0(%rip), %rdi
call puts@PLT
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0"
.section .note.GNU-stack,"",@progbits
- (3)汇编
汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。汇编相对于编译过程比较简单,根据汇编指令和机器指令的对照表一一翻译即可。
使用-c
命令
gcc –c hello.c –o hello.o
- (4)链接
gcc hello.o -o hello
链接器ld将各个目标文件组装在一起,解决符号依赖,库依赖关系,并生成可执行文件。
ld –static crt1.o crti.o crtbeginT.ohello.o –start-group –lgcc –lgcc_eh –lc-end-group crtend.o crtn.o
(省略了文件的路径名)。
当然链接的时候还会用到静态链接库,和动态连接库。静态库和动态库都是.o目标文件的集合。
静态库是在链接过程中将相关代码提取出来加入可执行文件的库(即在链接的时候将函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中),ar只是将一些别的文件集合到一个文件中。可以打包,当然也可以解包。
ar -v -q test.a test.o
上面指令可以生成静态链接库test.a
动态库在链接时只创建一些符号表,而在运行的时候才将有关库的代码装入内存,映射到运行时相应进程的虚地址空间。如果出错,如找不到对应的.so文件,会在执行的时候报动态连接错(可用LD_LIBRARY_PATH指定路径)。用file test.so
可以看到test.so是shared object的ELF文件。
gcc -sharedtest.so test.o
上面指令可以生成动态连接库test.so
gcc警告提示功能
- 当gcc在编译不符合ANSI/ISO C语言标准的源代码时,如果加上了
-pedantic
选项,那么使用了扩展语法的地方将产生相应的警告信息:
gcc -pedantic bad.c -o bad
输出
bad.c: In function 'main':
bad.c:4: warning: ISO C89 does not support 'long long'
bad.c:3: warning: return type of 'main' is not 'int'
需要注意的是,-pedantic编译选项并不能保证被编译程序与ANSI/ISO C标准的完全兼容,它仅仅用来帮助Linux程序员离这个目标越来越近。换句话说,-pedantic选项能够帮助程序员发现一些不符合ANSI/ISO C标准的代码,但不是全部。事实上只有ANSI/ISO C语言标准中要求进行编译器诊断的那些问题才有可能被gcc发现并提出警告。
- 除了-pedantic之外,gcc还有一些其他编译选项也能够产生有用的警告信息。这些选项大多以-W开头,其中最有价值的当数
-Wall
了,使用它能够使gcc产生尽可能多的警告信息。例如:
gcc -Wall bad.c -o bad
输出
bad.c:3: warning: return type of 'main' is not 'int'
bad.c: In function 'main':
bad.c:4: warning: unused variable 'var'
bad.c:6:2: warning: no newline at end of file
gcc给出的警告信息虽然从严格意义上说不能算作是错误,但很可能成为错误的栖身之所。一个优秀的Linux程序员应该尽量避免产生警告信息,使自己的代码始终保持简洁、优美和健壮的特性。
- 在处理警告方面,另一个常用的编译选项是
-Werror
,它要求gcc将所有的警告当成错误进行处理,这在使用自动编译工具(如make等)时非常有用。如果编译时带上-Werror选项,那么gcc会在所有产生警告的地方停止编译,迫使程序员对自己的代码进行修改。只有当相应的警告信息消除时,才可能将编译过程继续朝前推进。
gcc -Werror bad.c -o bad
对Linux程序员来讲,gcc给出的警告信息是很有价值的,它们不仅可以帮助程序员写出更加健壮的程序,而且还是跟踪和调试程序的有力工具。建议在用gcc编译源代码时始终带上-Wall选项,并把它逐渐培养成为一种习惯,这对找出常见的隐式编程错误很有帮助。
gcc代码优化
编译时使用选项-O可以告诉gcc同时减小代码的长度和执行时间,其效果等价于-O1
。在这一级别上能够进行的优化类型虽然取决于目标处理器,但一般都会包括线程跳转(Thread Jump)和延迟退栈(Deferred Stack Pops)两种优化。
选项-O2
告诉gcc除了完成所有-O1级别的优化之外,同时还要进行一些额外的调整工作,如处理器指令调度等。
选项-O3
则除了完成所有-O2级别的优化之外,还包括循环展开和其他一些与处理器特性相关的优化工作。
通常来说,数字越大优化的等级越高,同时也就意味着程序的运行速度越快。许多Linux程序员都喜欢使用-O2选项,因为它在优化长度、编译时间和代码大小之间取得了一个比较理想的平衡点。
- 借助Linux提供的time命令,可以大致统计出该程序在运行时所需要的时间:
$ time ./test
由此可以测试优化后代码的运行性能
优化虽然能够给程序带来更好的执行性能,但在如下一些场合中应该避免优化代码。
● 程序开发的时候:优化等级越高,消耗在编译上的时间就越长,因此在开发的时候最好不要使用优化选项,只有到软件发行或开发结束的时候,才考虑对最终生成的代码进行优化。
● 资源受限的时候:一些优化选项会增加可执行代码的体积,如果程序在运行时能够申请到的内存资源非常紧张(如一些实时嵌入式设备),那就不要对代码进行优化,因为由这带来的负面影响可能会产生非常严重的后果。
● 跟踪调试的时候:在对代码进行优化的时候,某些代码可能会被删除或改写,或者为了取得更佳的性能而进行重组,从而使跟踪和调试变得异常困难。
加速
在将源代码变成可执行文件的过程中,需要经过许多中间步骤,包含预处理、编译、汇编和连接。这些过程实际上是由不同的程序负责完成的。大多数情况下gcc可以为Linux程序员完成所有的后台工作,自动调用相应程序进行处理。
这样做有一个很明显的缺点,就是gcc在处理每一个源文件时,最终都需要生成好几个临时文件才能完成相应的工作,从而无形中导致处理速度变慢。例如,gcc在处理一个源文件时,可能需要一个临时文件来保存预处理的输出,一个临时文件来保存编译器的输出,一个临时文件来保存汇编器的输出,而读写这些临时文件显然需要耗费一定的时间。当软件项目变得非常庞大的时候,花费在这上面的代价可能会变得很大。
解决的办法是,使用Linux提供的一种更加高效的通信方式——管道。它可以用来同时连接两个程序,其中一个程序的输出将直接作为另一个程序的输入,这样就可以避免使用临时文件,但编译时却需要消耗更多的内存。
注意:
在编译过程中使用管道是由gcc的-pipe选项决定的。下面的这条命令就是借助gcc的管道功能来提高编译速度的:
gcc -pipe david.c -o david
在编译小型工程时使用管道,编译时间上的差异可能还不是很明显,但在源代码非常多的大型工程中,差异将变得非常明显。
gcc常用选项
选 项 名 | 作 用 |
---|---|
-c | 通知gcc取消连接步骤,即编译源码并在最后生成目标文件 |
-Dmacro | 定义指定的宏,使它能够通过源码中的#ifdef进行检验 |
-E | 不经过编译预处理程序的输出而输送至标准输出 |
-g3 | 获得有关调试程序的详细信息,它不能与-o选项联合使用 |
-Idirectory | 在包含文件搜索路径的起点处添加指定目录 |
-llibrary | 提示连接程序在创建最终可执行文件时包含指定的库 |
-O -O2 -O3 | 将优化状态打开,该选项不能与-g选项联合使用。当出现多个优化时,以最后一个为准 |
-O0 | 关闭所有优化选项 |
-S | 要求编译程序生成来自源代码的汇编程序输出 |
-v | 启动所有警报 |
.h | 预处理文件(标头文件) |
-Wall | 在发生警报时取消编译操作,即将警报看作是错误 |
-w | 禁止所有的报警 |
-share | 此选项将尽量使用动态库,所以生成文件比较小,但是需要系统由动态库 |
-shared | 产生共享对象文件 |
-static | 使用静态链接。此选项将禁止使用动态库。所以编译出的文件一般都很大,不需要动态连接库 |
-finline-functions,-fnoinline-functions | 启用/关闭内联函数 |
-g | 在编译结果中加入调试信息 |
-ggdb | 加入GDB调试器能识别的格式 |
-fPIC | 使用地址无关代码模式进行编译 |
-fPIE | 使用地址无关代码模式编译可执行文件 |
-fomit-frame-pointer | 禁止使用EBP作为函数帧指针 |
-fno-builtin | 禁止GCC编译器内置函数 |
-fno-stack-protector | 关闭堆栈保护功能 |
-ffunction-sections | 将每个函数编译到独立的代码段 |
-fdata-sections | 将全局/静态变量编译到独立的数据段 |