GCC 基本使用教程 - part 1
posted on 10 Nov 2020 under category tutorial
GCC (GNU Compiler Collection) 除了编译 C/C++, GCC 本身还有着这些特性:
gcc 把源文件转换为可执行文件, 基本要经历四个阶段, 每个阶段都会用到特定的工具.
预处理器进行预处理 (cpp): 对源文件中的宏定义进行扩展, 把源文件中引用的头文件中的代码包含进去
这个阶段中, 使用的预处理器是 cpp. 由于 cpp 默认不保存结果, 所以我们必须使用重定向, 或者传入 -save-temps
选项以保存结果, 比如:
$ cpp hello.c > hello.i
经过 cpp 预处理后的 C 源码, 一般以后缀 .i
命名. 而 C++ 则一般以 .ii
为后缀.
编译 (gcc -S): 把源码转换为汇编代码
使用 -S
选项, 可以让 gcc 只产生汇编代码 (文件名后缀 .s
) 而不产生目标文件 (文件名后缀 .o
).
$ gcc -S hello.i
运行上面结果, 会生成 hello.s
文件, 其中包含了转换后的汇编代码.
汇编 (as -o): 把汇编代码转换为机器指令
这个阶段使用的汇编器是 as. 比如运行
$ as hello.s -o hello.o
将会产生目标文件 hello.o
. 目标文件以 .o
为后缀.
注意: 这个阶段产生的目标文件中, 对于外部函数和变量的调用地址是没有定义的. 下一步的链接器负责填充这些丢失的地址.
链接 (ld / gcc): 生成最终的可执行文件
这个阶段使用的链接器是 ld, 调用 gcc 时它会在幕后调用 ld, 比如:
$ gcc hello.o
最终会产生默认命名为 a.out
的可执行文件.
gcc -Wall main.c -o hello
-Wall
选项见编译警告)-o hello
指定产生的可执行文件名称. 默认为 a.out这里我们假设 main.c 用到了 (依赖于) hello_fn.c.
被依赖的源文件置于后面:
gcc -Wall main.c hello_fn.c -o newhello
首先使用 -c
选项单独编译每个文件. -c
选项告诉 gcc 只生成目标文件, 而不生成可执行文件:
$ gcc -Wall -c main.c
$ gcc -Wall -c hello_fn.c
执行后会产生两个目标文件: main.o 和 hello_fn.o
然后链接刚刚产生的多个目标文件:
$ gcc main.o hello.o -o hello
gcc 会使用 ld 链接器进行链接, 生成 -o
指定的可执行文件 hello.
注意这里 不用 再重复指定 -Wall
选项, 而且被依赖的文件在后面.
如果你不了解静态库和共享库的概念, 可以读一下我的另一篇文章 (@todo). 这篇文章里我们把焦点放到 gcc 的使用上.
我们假设 calc.c 需要链接用于数学计算的 math 静态库 (静态库文件以 .a
为后缀), 一般 math 这样的系统库相关的文件会放到这些目录中:
C 标准库一般位于 /usr/lib/libc.a
为了将程序链接到 libm.a, 我们可以指定静态库的完整路径:
$ gcc -Wall calc.c /usr/lib/libm.a -o calc
也可以通过 -l
选项指定:
$ gcc -Wall calc.c -lm -o calc
这里的 -lm
指示 gcc 寻找并链接 libm.a
. 注意 gcc 自动补上了 lib
, 而且这里指定的顺序也很重要: 被依赖的库位于右边.
上文提到, gcc 需要找到指定的库文件然后链接, 那么它怎么知道库文件在哪儿呢?
gcc 会默认按照如下顺序搜索被指定的库文件, 我们把这些目录叫做库路径:
gcc 也需要知道库对应的头文件的位置. 头文件的默认搜索顺序如下, 我们把这些目录叫做包含路径:
这里假设我们的程序 dbmain.c 依赖于 gdbm 库. 并且它被我们安装到了非标准路径 /opt/gdbm-1.8.3/ 下面, 目录结构如下:
$ tree /opt/gdbm-1.8.3/
/opt/gdbm-1.8.3/
|-- lib/ # 库目录
| |-- libgdbm.a # 静态库
| |-- libgdbm.so # 动态库
|-- include/ # 包含目录
| |-- gdbm.h # 头文件
如何让 gcc 找到它呢? 有两种方法:
我们可以使用 -I
选项指定额外的包含路径, 使用 -L
指定额外的库路径:
gcc -Wall -I/opt/gdbm-1.8.3/include dbmain.c -L/opt/gdbm-1.8.3/lib -lgdbm
或者通过设置环境变量来告知 gcc 额外的搜索路径. 使用 LIBRARY_PATH
指定额外要搜索的库路径. 使用 C_INCLUDE_PATH
或 CPLUS_INCLUDE_PATH
分别定义 c 或 c++ 额外的包含路径:
$ C_INCLUDE_PATH=/opt/gdbm-1.8.3/include
$ export C_INCLUDE_PATH
$ LIBRARY_PATH=/opt/gdbm-1.8.3/lib
$ export LIBRARY_PATH
$ gcc -Wall dbmain.c -lgdbm
设置环境变量时, 可以使用
:
分隔符, 同时指定多个路径.
使用选项和环境变量这两种方式也可以混用. 在这种情况下, gcc 按照以下顺序搜索:
在日常使用中, 我们一般通过使用选项的方式来指定.
按照上文所讲链接库文件之后, 虽然能编译通过了, 但是尝试 运行 可执行文件时, 依然会报 “找不到文件” 错误:
$ ./a.out
./a.out: error while loading shared libraries:
libgdbm.so.3: cannot open shared object file:
No such file or directory
这是因为, 对于使用 -l
选项链接的外部库, gcc 实际上会先尝试搜索并链接可用的共享库. 而对于链接了共享库的程序, 当它运行时, 动态链接器 ld 必须找到这个共享库, 把它加载到内存中.
所以这里又涉及一个动态链接器 ld 需要搜索的加载路径, 默认会搜索以下加载路径 (同库路径):
而这两个目录中都没有要找的 libgdbm.so
, 所以报错了.
为了解决这个问题, 我们可以在编译时使用 -static
选项强制 gcc 链接静态库. 这样就不会需要运行时的动态链接了:
gcc -Wall -static -I/opt/gdbm-1.8.3/include/ -L/opt/gdbm-1.8.3/lib/ dbmain.c -lgdbm
也可以通过 LD_LIBRARY_PATH
变量指定额外的加载路径让 ld 找到对应的共享库文件.
还可以在 ld 的配置文件中定义额外的加载路径.
gcc 默认使用 GNU C 标准来编译, GNU C 在 ANSI/ISO 标准上增加了诸如嵌套函数和可变长数组等特性.
但是我们也可以通过以下选项更改编译时的标准:
-ansi
: 禁用 GNU 扩展 (asm, inline, typeof, unix, vax…)-ansi -pedantic
: 禁用所有 GNU 扩展, 即使那些兼容 ANSI 标准的特性-std=c89
: 指定为 c89, iso9899:199409, c99, gnu89, gnu99 等-Wall
包含了以下警告:
-Wcomment
: 嵌套函数-Wformat
: 不正确的使用格式字符串 (比如在 printf()
中)-Wunused
: 没有用到的变量-Wimplicit
: 未声明的函数-Wreturn-type
: 没有定义返回值类型也没有指定返回类型为 void 的函数以下没有包含在 -Wall
中:
-W
: 常见编程错误, 如函数没有返回值, 有符号和无符号比较等. 一般和 -Wall
一块儿使用-Wconversion
: 隐式类型转换-Wshadow
: 在一个作用域中重复声明变量-Wcast-qual
: 指针被强转后丢掉了类型限定符-Wwrite-strings
: 尝试覆写字符串常量-Wtraditional
: ANSI 标准和传统 C 标准 (K&R) 解释不同注意: 所有这些警告都不会终止编译, 使用 -Werror
可以转换为错误, 终止编译.
下一篇介绍如何使用预处理器, 如何为调试或性能进行编译, 以及相关的工具简介等.