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 时,主要是按照以下思路排查
所需的依赖库到底有没有,装在哪,我们可以用 ldd 等工具查询缺少哪些依赖库,也可以用 nm 命令查询某一个符号状态
确认依赖库存在,并且确认和编译时引用的是同一个
依赖库确认没问题后,那只可能是执行文件不知道这个库在哪。同编译时一样,执行文件也只会搜索几个标准路径。这个问题有如下几个方法解决
- 将你的依赖库通通放到 /usr/lib 下,这种方法最差,影响系统环境,但如果是在 docker 内运行则没这个问题
- 使用 LD_LIBRARY_PATH 环境变量,将依赖库路径加进去,用法和 PATH 一样
- 如果你在编译的时候就知道未来部署环境中这些依赖库的位置,那你可以使用 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 总结
各个编译系统对于依赖引用已经做了相当多的优化,但是仍然无法确保不出错,因此我们一定要了解相关知识,这样才能快速的解决问题