GCC编译器入门

读了一本介绍GCC的书《An Introduction to GCC》,这里记录一下关于GCC的功能和用法。

1. 概念

GCC(GNU C Compiler)早期是专为C语言而开发的编译器自由软件,后来加入了C++语言的支持,再后来增加了大量其他语言的支持,包括Fortran,Object-C,Java等,因此GCC这个词的涵义升级为GNU Compiler Collection。

GCC诞生于1987年,今天已有30岁高龄。

2. gcc.exe的用法

gcc.exe是GCC套件中的C语言编译器,它能够编译、调试和链接源代码并输出可执行文件。

2.1 基础用法

最简单的用法为:

gcc main.c

在命令行提示符窗口中输入这条命令,将编译main.c代码并生成一个可执行文件a.exe。

这里假设main.c中的内容是:

#include <stdio.h>
int main(void)
{
    printf("Hello World!\n");
    return 0;
}

那么在命令行中输入a.exe即可运行并打印出“Hello World!”字样。

2.2 显示警告

-Wall(Warning all)参数可以显示出编译过程中遇到的警告信息。

将上述编译命令改成

gcc -Wall main.c

此时如果源代码中有不合理的代码,则会在编译过程显示警告信息。这些警告信息包括:嵌套块注释,printf等函数的格式参数不匹配,未被使用的变量,隐式声明,返回值不匹配。

推荐所有的编译过程都使用-Wall参数。

2.3 指定输出文件

默认的输出文件名为a.exe,使用-o(output)指令可以输出指定输出文件的文件名:

gcc -Wall main.c -o main.exe

这样就能够生成main.exe而不是a.exe

2.4 编译多文件项目

假如项目中存在多个源文件,比如main.c中调用了hello.c的头文件hello.h,使用了它的函数SayHello()。

那么在命令行中执行:

gcc -Wall main.c hello.c -o main.exe

就能够正确的编译这个项目。

2.5 分步编译

-c(compile)参数用于将源代码文件编译成.o对象文件,然后再将所有的对象文件链接成可执行文件。

gcc -Wall -c main.c hello.c

输出main.o和hello.o两个对象文件。对象文件是二进制文件,不能直接使用文本编辑器读取。

命令行中main.c和hello.c的先后顺序不可颠倒。

利用这些对象文件,可以生成最终的可执行文件:

gcc main.o hello.o -o main.exe

2.6 Makefile

当项目中文件比较多,而这时更改了hello.c,希望只重新编译该文件,而不是重新编译整个项目,使用2.5中的命令行将会异常繁琐。

Makefile使用脚本的方式,将2.5中的编译过程列举出来并依次执行。

2.5中的两行命令可以用Makefile表达:

CC=gcc
CFLAGS=-Wall
main:main.o hello.o

clean:
    del /f main.exe main.o hello.o

第一行CC(C Compiler)表示C编译器类型,第二行CFLAGS表示参数开关,第三行表示输出文件是main.exe,依赖文件为main.o和hello.o,这两个文件会自动从对应的.c文件中编译生成。最后的clean是一个make.exe的命令函数。注意,最后一行del前需要是一个Tab,而不是空格。

将文件保存在当前目录下,文件名为Makefile,没有扩展名,在命令行中执行:

make

即运行make命令,不带任何参数,将会自动读取当前目录下的Makefile并执行内容。执行完毕将生成main.exe文件。

还可以执行Makefile中设定的clean函数:

make clean

则自动执行删除main.exe, main.o和hello.o文件的操作。

现代的IDE都会使用Makefile来编译项目文件。

2.7 链接外部库

假如使用的是MinGW版本的GCC,自带的库文件通常放在:\MinGW32\lib或\MinGW32\lib32目录下。

观察其中的库名称,会发现所有的库文件名都以lib作为前缀,扩展名为.a。

假如源代码中使用了math.h库,这个数学库需要调用标准库libm.a文件,则需要在命令行中指明:

gcc -Wall main.c -lm -o main.exe

这里的-lm要拆开理解,-l(Library)表示链接外部库,m表示文件libm.a去除了lib前缀和.a扩展名的剩余部分。假如库名字为libABC.a,调用时候就是-lABC。

如果不显式调用库文件,则会造成编译失败。

同时,要注意-lm参数是放在main.c的后面,表示与该文件的调用关系,顺序不能颠倒,位置不能出错。

2.8 库和头文件路径

假如库或者头文件放在其他目录,有两种办法可以通知编译器:

  • 将那些目录加入到环境变量
  • 中命令行中指定那些目录

命令行中指定头文件目录参数是-I(Include_Path),库目录参数是-L(Library_Path),注意都是大写。

假如main.c调用的头文件中父目录的include目录中,库文件在父目录的lib目录中,那么命令行可以写成:

gcc -Wall -I/../include -L/../lib -lm main.c

2.9 静态库和动态链接库

静态库的后缀是.a,编译时会将其完整的复制到最终的可执行文件中,即静态库最终会成为可执行文件的一部分。

动态链接库后缀是.so(这里仅考虑GNU编译平台,不考虑Windows的DLL),仅在执行前将库文件中需要的那部分代码加载到系统内存中,执行结束则释放,是一个动态的链接过程。这部分内存代码,可以共享给多个不同程序使用,所以也叫共享库。

动态链接库可以让程序体积减小,如果更新库本身,只要接口维持不变,则不会影响应用程序。

如果平台针对一个功能同时提供静态库和动态链接库,那么编译器会优先使用动态链接库。

动态链接库的使用方法与静态链接库相同。(此处存疑,因为书中介绍不甚清楚。)

2.10 指定标准

C语言有C89,C99,有ANSI纯净版和GNU扩展版,各个版本略有不同。

-std(standard)参数可以指定C语言标准版本:

gcc -Wall -std=gnu99 main.c

2.11 宏开关

假设代码中使用了宏开关:

#ifdef ON
{//xxxxxx}
#end

-D 参数可以可以指定这个ON宏:

gcc -Wall -DON main.c

这等同于在代码中使用:

#define ON

2.12 宏参数

假设代码中使用了宏参数:

#if OPEN==1
{//xxxxxx}
#endif

仍然在命令行中使用-D参数:

gcc -Wall -DOPEN=1 main.c

这等同于执行代码:

#define OPEN (1)

如果希望OPEN等于空,那么可以使用双引号进行包裹空字符:

-DOPEN=""

3. gdb.exe的用法

gdb是C语言调试工具。在生成了main.exe之后,执行:

gdb main.exe

这样就打开了gdb的子环境。在这个子环境中,q表示退出,在任何时候输入q字母并回车,就退出gdb环境。

  • 在gdb中输入file main,表示读取main.exe程序中的符号列表。
  • 运行run命令,将开始执行main.exe。
  • 假如main中有一些全局变量,则可以使用print xxx来打印出来。
  • 使用break 函数名来设置断点,然后step表示单步执行。

跟常用的IDE上的调试功能一致。

gdb的使用值得使用一本书来介绍,这里点到为止。

4. 代码优化

4.1 基本手段

代码层面的优化包括:

  • 表达式消除
  • 函数内联化
  • 循环展开
  • 指令调度

比如在代码中用到的加减乘除运算,在编译器会直接得到结果再进行编译,这样就减少了可执行程序的运算步骤。

一个”小“函数,如果频繁的被调用,则将其内联到具体的调用者体内。

所谓循环展开是指将某些赋值循环展开成扁平的赋值操作。因为一个循环,必然存在检测退出机制,而大多检测操作是无用的操作,所以展开后能够减少指令数。

指令调度则是重新排列指令顺序,以加快执行速度。

4.2 优化选项

上述的一些优化措施,已经集成到了gcc内部,以-o(Optimize)参数形式提供:

  • -o0
  • -o1
  • -o2
  • -o3
  • -os

-o0表示没有任何优化,等同于不使用该参数。适合调试时候使用。

-o1清除代码层面上的冗余,未做深入优化。这个是最常用的优化级别。

-o2使用了指令调度优化,它没有增加最终文件的大小,却提高了速度,所以适合做程序发布时候的优化项。

-o3使用了较多技术,增加了最终程序的大小,带来了速度上的提升。这种优化项有时候可能不稳定。

-os专为为减小最终程序大小而做的优化项。

书中举了一个例子(链接),在函数中做大量、复杂的数学运算。如果不做优化,程序执行时间为13秒,不同的优化选项能带来不同的提升,-o3选项下,程序执行时间减小到5.4秒,改善比较可观。

5. 汇编文件

使用cpp.exe可以将源代码文件进行预处理,生成.i文件。

cpp main.c > main.i

这个main.i文件可以进一步转成汇编文件:

gcc -Wall -S main.i

在当前目录下将生成main.s文件,使用编辑器打开可以看到汇编代码。

进一步可以使用as.exe将汇编文件打包成对象文件:

as main.s -o main.o

然后使用链接器ld.exe生成可执行文件,但是ld命令的参数比较复杂,通常直接利用gcc把这些事情给做了。

6. 其他功能

6.1 输出符号列表

nm.exe可以输出可执行文件的符号表:

nm main.exe > main.map

输出的map文件包含了各个符号的存储地址和类型。

6.2 执行时间

gprof.exe可以记录各个函数的调用次数、执行时间等。

gprof main.exe

这个main.exe需要预先使用-pg指令进行处理:

gcc -Wall -pg main.c -o main.exe

可以参考这个示例(链接)进行操作。

6.3 代码执行次数

gcov用于生成测试报告,记录源代码中每一行被执行的次数,如果代码行未被执行,也会被统计出来,所以叫做覆盖性测试。

先生成特殊处理的可执行文件:

gcc -Wall -fprofile-arcs -ftest-coverage main.c

执行main.exe,生成必要的中间文件:

main.exe

然后再使用gcov.exe读取main.c:

gcov main.c

此时观察源代码所在目录,会有一个main.c.gcov的文件,打开它就能够看到各个代码行被执行了多少次。

7. 小结

至此,对gcc的各功能有了概念性的了解,以后再遇到编译器选项、符号表、优化等问题,可以再做深入探究。

这本书是免费书,阅读地址:http://www.network-theory.co.uk/docs/gccintro/index.html

比较喜欢这种100页的书,一两天就读完了,好开心。

(完)