日期:2023年2月17日标签:C/C++

GCC/G++ #

GCC/G++ 是用来编译链接 C/C++ 程序的,长话短说即编译器。这里不介绍它的历史,感兴趣的朋友自行查阅。

C/C++ 的编译过程 #

介绍 GCC/G++ 之前,先简单说明下 C/C++ 的编译链接生成可执行文件的过程。

gcc 编译过程

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/G++ 的使用 #

以 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)。它们在行为上有些不同?

  • 静态库是目标文件(.o)的集合,linux 和 mac 上后缀名为 .a, windows 中后缀名为 .lib。在程序编译过程中的链接阶段,与其他目标文件(.o)一起链接形成可执行文件(.out)。所以静态库只在编译阶段起作用,运行时不需要静态库了。
  • 共享库也是目标文件的集合,linux 中后缀名为 .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 方法,链接器的执行过程如下:

  1. 链接器从左往右扫描目标文件和静态库
  2. 扫描到 main.o 时,发现一个未解析的符号 add,记住这个未解析的符号
  3. 扫描 libutil.a 时找到了前面未解析的符号,因此提取相关代码
  4. 最终没有任何未解析的符号,编译链接完成,生成可执行文件 prog

如果将静态库放到目标文件前面,例如执行 gcc -L. -lutil main.o -o prog,那么链接器的执行过程如下:

  1. 链接器从左往右依次扫描静态库和目标文件
  2. 扫描 libutil.a 时由于前面没有任何未解析的符号,所以不会提取库中的任何代码
  3. 扫描 main.o 时,发现未解析的符号 exp
  4. 扫描结束,存在未解析的符号,因此编译链接报错

共享库的创建和链接 #

创建共享库(动态库)的过程和静态库其实没什么区别,都是将一组目标文件编译插入到一个库文件中。但是再链接动态库时,并不是将需要的二进制代码都“拷贝”到可执行文件中,二十仅“拷贝”一些重定位和符号信息,有了这些信息,在程序运行时会完成真正的链接过程。

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

参考 #

(完)

目录