1. 前言

和其他现代语言不同,C/C++ 的编译系统是散装的,由众多工具组成。比如包管理器有 Conan,Vcpkg 等等,构建系统有 Cmake, Ninja, autotools, scons 以及原始的 Makefile. 也有想要一统天下的 Bazel

说句题外话,我深入用过 Conan + Cmake 和 Bazel 两种方案,并在公司负责这两种编译系统的开发和维护。相比来说,我认为前者方案适合纯 C/C++/cuda 项目,非常方便好用,官方提供的库菜单很多,对交叉编译支持完备。后者则更适合大型、复杂项目,以及多语言,多个模块项目,Bazel 目标依赖系统做的很好,上游目标改动后会触发下游目标编译,这点是 Conan 和 Cmake 系统无法做到的。 但是 Bazel 对 C/C++ 的交叉编译没有支持,Cuda 规则没有官方提供,编译器规则开发经过多次迭代,可能前几年使用的在新版中已经无法使用,相对维护成本高很多。

回到正题,无论采用那种构建系统,最后总都归到编译器,主流的有 GCC 和 Clang 两种,我只用过 GCC,因此本文主要记录一些使用中觉得比较重要的点,供自己记录参考

2. 依赖问题

依赖问题应该是编译中大家遇到最多的问题。

简单来说,编译过程分为 编译链接 两步,如果你写的代码没问题,那我们在进行编译时主要会遇到以下三种错误

  • 编译的时候报找不到头文件
  • 编译的时候报找不到依赖库
  • 编译过了运行时候报找不到对应符号

下面我们分别讨论如何解决上述问题

2.1 找不到头文件

这个问题最容易解决,编译器默认只会查询 /usr/include, /usr/local/include 等几个标准路径下的头文件,将这些路径和代码里引用的相对路径拼起来就是头文件位置。

而对于放在非标准路径下的头文件,我们可以通过 -I 参数告诉编译器,如下

gcc -I{include_dir} -o my_program my_program.c

还有一种方法是使用 isystem

gcc -isystem {include_dir} -o my_program my_program.c

这两种方式都可以引入头文件搜索路径,两者区别如下

  • 使用 -I 添加用户头文件搜索路径,编译器会报告这些头文件中的所有警告。
  • 使用 -isystem 添加系统头文件搜索路径,编译器会抑制这些头文件中的警告。
  • -I 添加的目录优先级高于 -isystem,但低于默认的用户头文件目录。

因此如果你引用的是自己写的头文件,请用 -I,如果引用的是放在非标准位置的系统头文件,请用 -isystem,这一点在交叉编译是很有用

头文件会和源文件拼起来后,被编译到 .o 文件里,因此不用担心运行时问题

2.2 编译时找不到依赖库

C/C++ 的依赖库分为动态和静态库两种,.so 和 .a 两种。

和头文件引用一样,GCC 默认只会搜索几个标准路径下的头文件,同样 GCC 中可以使用 -L 参数增加搜索路径,如下

gcc -L{lib_dir} -o my_program my_program.c -lfoo

看起来很简单,但是这只是开始,因为你依赖的库也可能依赖一个放在非标准位置的库!!!

如果按上述编译会报错找不到 lib_bar.so/a 依赖的库。这时我们可以用 -rpath-link 解决这个问题,如下

gcc -L{lib_dir1} -Wl,-rpath-link,{lib_dir2} -o my_program my_program.c -lfoo

假设 libfoo.so 依赖 libbar.so, 则其中 lib_dir1 是 libfoo.so 所在的路径,lib_dir2 是 libbar.so 所在路径。

-Wl 的意思是后面的 Option 是告诉链接器的,因为这些搜索的过程是在链接过程中,所以需要告诉它

当然,如果 libfoo.so 和 libbar.so 在同一路径下,我们也可以如下去写

gcc -Wl,-L{lib_dir} -o my_program my_program.c -lfoo

解决完编译依赖问题后,如果你全部依赖的是静态库,虽然编出来的文件体积可能会较大,但是就不用担心运行时的问题了。因为所有需要的符号已经被集成进可执行文件中。而如果使用的是动态库,则仍然需要考虑运行时问题。

2.3 运行时找不到所需符号

当运行时报错 undefined symbol 时,主要是按照以下思路排查

  1. 所需的依赖库到底有没有,装在哪,我们可以用 ldd 等工具查询缺少哪些依赖库,也可以用 nm 命令查询某一个符号状态

  2. 确认依赖库存在,并且确认和编译时引用的是同一个

  3. 依赖库确认没问题后,那只可能是执行文件不知道这个库在哪。同编译时一样,执行文件也只会搜索几个标准路径。这个问题有如下几个方法解决

    1. 将你的依赖库通通放到 /usr/lib 下,这种方法最差,影响系统环境,但如果是在 docker 内运行则没这个问题
    2. 使用 LD_LIBRARY_PATH 环境变量,将依赖库路径加进去,用法和 PATH 一样
    3. 如果你在编译的时候就知道未来部署环境中这些依赖库的位置,那你可以使用 rpath 参数告诉执行文件以后去哪寻找依赖库
    # 运行时,my_program 会去 runtime_lib_dir 寻找 libfoo.so
    gcc -Wl,-L{build_lib_dir} -Wl,-rpath,{runtime_lib_dir} -o my_program my_program.c -lfoo
    

2.4 总结

各个编译系统对于依赖引用已经做了相当多的优化,但是仍然无法确保不出错,因此我们一定要了解相关知识,这样才能快速的解决问题