GCC/G++ 是用来编译链接 C/C++ 程序的,长话短说即编译器。这里不介绍它的历史,感兴趣的朋友自行查阅。
介绍 GCC/G++ 之前,先简单说明下 C/C++ 的编译链接生成可执行文件的过程。
1、源文件预处理:通过 c 预处理(cpp.exe)处理源文件中的宏等
cpp hello.c > hello.i
2、编译:gcc/g++ 将经过预处理后的代码编译成汇编程序,-S
选项表示生成汇编程序文件(.s)
gcc -S hello.i
3、使用 as.exe 将汇编程序生成目标文件(.o)
as -o hello.o hello.s
4、最后使用连接器(ld.exe)链接目标文件(.o),生成可执行文件(.out/.exe)
ld -o hello.out hello.o ...libraries...
以 GCC 为例,介绍基本使用方法。新建一个 hello.c
,写入以下内容:
#include <stdio.h>
int main() {
printf("Hello, World");
return 0;
}
1、编译并链接 c 文件
$ gcc hello.c
$ ls
a.out hello.c
默认生成 a.out
(linux 上生成的可执行文件为 a.out, windows 为 a.exe)。可以使用 -o
选项指定生成文件的名称。
$ gcc -o hello hello.c
$ ls
hello hello.c
2、编译和链接过程分开
实际项目中某个文件发生了变化,我们希望单独编译,然后将其编译后的目标文件(.o)与其他目标文件或者库文件链接。可以使用 -c
选项将源文件编译成目标文件(.o):
$ gcc -c hello.c
$ ls
hello.c hello.o
将目标文件链接成可执行文件,执行的命令与编译并链接相同,当源文件是目标文件时执行的是链接过程,源文件为 C 文件时执行的编译和链接过程。
$ gcc -o hello hello.o
$ ls
hello hello.c hello.o
3、多文件编译链接
gcc 命令支持多个源文件输入,如果你想把 source1.c 和 source2.c 一起编译:
$ gcc -o a.out source1.c source2.c
$ ls
a.out source1.c source2.c
更多的时候,你可能想分开编译:
$ gcc -c source1.c
$ gcc -c source2.c
$ gcc -o a.out source1.out source2.out
4、编译时打印所有警告信息
使用 -Wall
选项可以打印所有警告信息,所以使用 gcc 时,一般都会开启该选项:
$ gcc -Wall -o hello hello.c
5、调试
使用 -g
选项,可以使文件包含调试信息,以提供给 gdb 调试
$ gcc -Wall -g -o hello hello.c
gdb 调试教程:用 GDB 调试程序
6、编译时打印完整的信息
-v
可以打印完整的编译过程信息:
$ gcc -v -o hello hello.c
通常我们会把一些公用的方法、变量、类,编译成一个单独的库——library,供业务方调用。库本质上是一个包含一个或者多个目标文件(object file)的文件,供链接阶段使用。
库分为两种:静态库(static library) 和共享库(shared library),共享库也被叫做动态库(dynamic)。它们在行为上有些不同?
.a
, windows 中后缀名为 .lib
。在程序编译过程中的链接阶段,与其他目标文件(.o)一起链接形成可执行文件(.out)。所以静态库只在编译阶段起作用,运行时不需要静态库了。.so
(shared object), mac 中后缀名为 .dylib
, windows 中后缀名为 .dll
,在程序编译的链接阶段和运行阶段都起作用。在链接阶段,链接器会验证程序执行所需要的符号(变量、方法等)已经被链接到程序中或者存在于某个共享库中。但是共享库中的目标文件并不会被链接到最终生成的可执行文件中。而在运行阶段,程序启动时,系统中有一个程序——**动态加载器(dynamic loader)**会检查哪些动态库被链接到了程序中,并将这些动态库加载到内存中和程序一起执行。所以共享库在程序运行时必须存在。静态库相当于在编译时,它里面包含的代码被拷贝到了可执行文件中,而共享库的代码是在运行时被加载到内存中的,所以如果用静态库编译,最终生成的可执行文件会比使用共享库编译得到的可执行文件大。
使用 ar 工具创建静态库,ar 具有以下功能。
linux 中可以使用 man ar
查看 ar
命令的用法。
1、创建静态库
可以用下面的命令创建一个静态库:
$ ar -rc libutil.a util_file.o util_net.o util_math.o
上述命令将 util_file.o
util_net.o
util_math.o
三个文件打包成静态库 libutil.a
,如果 libutil.a
文件已经存在,那么会将 util_file.o
util_net.o
util_math.o
三个文件添加到静态库中。c
选项表示如果文件不存在则创建库文件,r
选项表示如果某个要打包的目标文件已经存在于库中,则替换掉对应的目标文件。
也可以用 gcc 的 -static
生成静态库:
$ gcc *.o -static -o libname.a
静态文件名称一般以 lib
前缀开头,.a
后缀结尾,因为链接的时候编译器会根据这个特点查找库文件(下面会说明)。
2、使用静态库
当我们创建静态库后,我们想在程序中使用它。可以通过下面的命令将库文件于目标文件一起链接成可执行文件:
gcc {{目标文件1 目标文件2 ...}} -L<库文件目录> -l<库文件> -o <输出可执行文件名称>
例如,下面的例子将目标文件 main.o
与库 libutil.a
链接成可执行文件 prog。
gcc main.o -L. -lutil -o prog
需要注意的是:
-L
选项用于指示库文件目录,.
表示当前目录,所以要链接的库就在当前目录下。-l
选项用于表示要链接的库文件的名称,要注意我们去掉了库文件名称的前缀 lib
和 后缀 .a
,所以库文件名称以 lib
开头,以 .a
结尾。在执行 gcc main.o -L. -lutil -o prog
时,假设 main.o
用到了 libutil.a
中的一个函数符号,例如 add
方法,链接器的执行过程如下:
main.o
时,发现一个未解析的符号 add
,记住这个未解析的符号libutil.a
时找到了前面未解析的符号,因此提取相关代码prog
如果将静态库放到目标文件前面,例如执行 gcc -L. -lutil main.o -o prog
,那么链接器的执行过程如下:
libutil.a
时由于前面没有任何未解析的符号,所以不会提取库中的任何代码main.o
时,发现未解析的符号 exp创建共享库(动态库)的过程和静态库其实没什么区别,都是将一组目标文件编译插入到一个库文件中。但是再链接动态库时,并不是将需要的二进制代码都“拷贝”到可执行文件中,二十仅“拷贝”一些重定位和符号信息,有了这些信息,在程序运行时会完成真正的链接过程。
1、创建共享库 gcc 的 -shared
参数可以创建共享库:
# 生成目标文件
$ gcc -fPIC -c *.c
# 将目标文件编译成共享库
$ gcc –shared –o *.so *.o
2、链接共享库
链接共享库与链接动态库命令一致,由于 gcc 默认采用动态链接,如果存在同名的动态库和静态库,gcc 会链接动态库,可以使用 -static
指定链接静态库。
$ gcc main.o -L. -lutil -o prog
上面的命令会链接 libutil.so
文件。
通常程序运行时,系统动态加载(dynamic loader)起会在系统指定的一些目录下寻找共享库(例如 /lib, /usr/x11/lib 等等)。当我们创建了一个共享库,我们可以使用 LD_LIBRARY_PATH
环境变量告诉动态加载器在指定目录下寻找。
LD_LIBRARY_PATH=/full/path/to/library/directory:${LD_LIBRARY_PATH}
export LD_LIBRARY_PATH
可以使用 ldd 命令查看可执行文件所依赖的共享库:
$ ldd prog
(完)