虽然平时经常用gcc来开发,但从没有细想过其内部怎么执行,其具体的流程又是如何。由于主力开发语言为C/C++,加上最近看C专家编程有感,决定写此博客,来加深自己的记忆。
1 编译流程
通常GCC的编译流程为:
- 预编译-预处理源文件,生成.i文件:去掉注释、宏替换、增加行号信息等步骤;
- 编译-编译预处理后的.i文件,生成.s汇编代码:代码语法分析、代码优化和汇总符号等步骤
- 汇编-将汇编代码编译为目标文件;
- 连接-连接目标文件和附加的目标文件,最终生成可执行文件。
其流程如下图所示:
示例代码main.c
如下所示:
#include <stdio.h>
int main()
{
int a = 1, b = 2;
printf("The result of a + b is: %d\n", a + b);
return 0;
}
通常我们的做法是:
gcc main.c -o main
gcc -Wall main.c -o main # add warning info.
1.1 预编译
gcc -E main.c -o main.i
只激活预处理,生成的.i文件,即在源.c文件前面会插入头文件的内容,由于文件较大,可以只查看最后20行,可以通过下面指令查看,其内容自行查看即能理解。
# view all
vim main.i # by vim
code main.i # by vscode
# view the last 20 row
tail -n 20 main.i
1.2 编译生成汇编代码
gcc -S main.c -o main.s
gcc -S main.i -o main.s
只激活预处理和编译,将文件编译成为汇编代码。可以对源.c文件或者预处理后的文件进行编译操作。
1.3 汇编
gcc -c main.s -o main.o
对于前面编译生成的汇编代码,将其进行编译生成目标文件。
1.4 连接
gcc main.o -o main
执行./main
,即会弹出:
$ ./main
The result of a + b is: 3
gcc的连接器通常为gas,其将程序目标文件和其他附加的目标文件连接起来,最后生成可执行文件。
1.5 连接多个文件
如果我们编写的文件,包含多个.c和头文件时,怎么编译呢?这里以三个文件为例,参考如下的代码:
gcc test1.c test2.c test3.c main.c -o test
其等价于以下内容:
gcc main.c -o main.o
gcc -c test1.c -o test1.o
gcc -c test2.c -o test2.o
gcc -c test3.c -o test3.o
gcc test1.o test2.o test3.o main.o -o test
注意:头文件需要包含三个.c文件的相关申明,对于依赖最好写Makefile文件
1.6 错误检查
通常我们会使用-Wall 打开警告信息,但还有其他的一些命令,如下:
gcc -Wall main.c -o main
gcc -pedantic main.c -o main
gcc -Werror main.c -o main
gcc -Wextra main.c -o main
将测试代码中的打印输出,改为打印c
,来看一下错误检查信息。-Wall
选项,不仅给出了错误信息,也给出了详细的警告信息。
$ gcc -Werror main.c -o main
main.c: In function 'main':
main.c:7:44: error: 'c' undeclared (first use in this function)
printf("The result of a + b is: %d\n", c);
^
main.c:7:44: note: each undeclared identifier is reported only once for each function it appears in
$ gcc -Wall main.c -o main
main.c: In function 'main':
main.c:7:44: error: 'c' undeclared (first use in this function)
printf("The result of a + b is: %d\n", c);
^
main.c:7:44: note: each undeclared identifier is reported only once for each function it appears in
main.c:5:16: warning: unused variable 'b' [-Wunused-variable]
int a = 1, b = 2;
^
main.c:5:9: warning: unused variable 'a' [-Wunused-variable]
int a = 1, b = 2;
^
$ gcc -Wextra main.c -o main
main.c: In function 'main':
main.c:7:44: error: 'c' undeclared (first use in this function)
printf("The result of a + b is: %d\n", c);
^
main.c:7:44: note: each undeclared identifier is reported only once for each function it appears in
$ gcc -pedantic main.c -o main
main.c: In function 'main':
main.c:7:44: error: 'c' undeclared (first use in this function)
printf("The result of a + b is: %d\n", c);
^
main.c:7:44: note: each undeclared identifier is reported only once for each function it appears in
-Wall
选项,返回GCC所有的警告信息;
-pedantic
选项,能够帮助程序员发现一些不符合 ANSI/ISO C标准的代码;
-Werror
选项,GCC会在所有产生警告的地方停止编译,迫使程序员对自己的代码进行修改;
-Wextra
选项,对所有合法但值得怀疑的表达式发出警告。
1.7 语言标准
gcc -std=c11 main.c -o main
gcc -std=c99 main.c -o main
顾名思义,选择c11还是c99标准。
1.8 优化选项
gcc -O0 main,c -o mian
gcc -O1 main.c -o main
gcc -O2 main.c -o main
gcc -O3 main.c -o main
gcc -Os main.c -o main
-O0
:不优化,缺省值;
-O1
:尝试优化编译时间和可执行文件大小;
-O2
:尝试几乎全部的优化功能,但不会进行“空间换时间”的优化方法。;
-O3
:再打开一些优化选项:-finline-functions
,-funswitch-loops
和-fgcse-after-reload
;
-Os
:对生成文件大小进行优化。打开-O2
开的全部选项,除了会那些增加文件大小的。
2 外部库的连接
通常在开发时,不可能所有函数都是自己编写,不可避免的会使用第三方提供的库函数等。这些库函数通常由一组互相关联的可重用原则编写,而我们熟知的库函数如:.dll, .lib, .so
等。库具有模块化、重用、可维护的特性。
库又可以分为静态库与动态库:
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。静态库比较占用磁盘空间,而且程序不可以共享静态库。运行时也是比较占内存的,因为每个程序都包含了一份静态库。
- 动态库(.so):程序在运行的时候才去链接共享库的代码,多个程序共享使用库的代码,这样就减少了程序的体积。
C专家编程中关于动态链接和静态链接有这样的描述:
- 如果函数库的一份拷贝是可执行文件的物理组成部分,那么我们称之为静态链接;
- 如果可执行文件只是包含了文件名,让载入器在运行时能够寻找程序所需要的函数库,那么称之为动态链接。
静态链接的模块被链接编辑并载入以便运行。动态链接的模块被链接编辑后再载入,在运行时进行连接以便运行。
Linux下,一般头文件或库文件的位置在:
- /usr/include及其子目录底下的include文件夹
- /usr/local/include及其子目录底下的include文件夹
- /usr/lib
- /usr/local/lib
- /lib
静态库和动态库的生成和使用,参考如下两篇博客:
linux下 GCC编译链接静态库&动态库
Linux GCC编译使用动态、静态链接库