LIO-SAM 这个库做激光惯性 SLAM 的应该都不陌生,用因子图把激光雷达、IMU 和 GPS 先验压到一起做全局优化,效果确实能打,但编译环节也是出了名的磨人。这两天帮人排查一个报错,卡在最后链接那一步,终端里就一行:
/usr/bin/ld: 找不到 -lBoost::timer估计不少人第一次看到这个提示都懵了:Boost::timer 到底是啥?系统里明明装了 Boost,怎么还找不到?这个报错的迷惑性就在于,它看起来像"缺库",实际却是构建系统把 CMake 的 target 名字当成了普通库名,原封不动传给了链接器。这篇文章把这个错误的前因后果、排查路径和修复方法完整讲一遍,正在编译 LIO-SAM、或者在任何 ROS/CMake 工程里遇到-l系列报错的朋友,都可以直接对照着操作。
1. 先搞清楚报错在说什么,再动手改
1.1 链接器只认 libxxx.so,不认 Boost::timer
C/C++ 的构建分四步:预处理、编译、汇编、链接。你写的头文件、函数调用,到链接这一步才会真正和外部库产生关系。编译器把源码变成一堆.o目标文件之后,链接器(也就是报错里这个/usr/bin/ld)负责把这些.o和用到的库拼成最终的可执行文件或者动态库。这一步一旦出错,整个构建就卡死,前面编得再快也没用。
链接器找库靠的是一套非常朴素的规则:-l名字会去搜索路径下找lib名字.so或者lib名字.a。比如-lboost_timer,找的就是libboost_timer.so。搜索路径包括你用-L参数指定的目录、环境变量LIBRARY_PATH,以及系统默认目录,Ubuntu 上通常是/usr/lib/x86_64-linux-gnu这类带架构后缀的目录。找不到就报"找不到 -lxxx"。
现在回到报错本身。-lBoost::timer意味着链接器收到的参数是Boost::timer,于是它去找libBoost::timer.so。注意细节:大写B、中间有冒号。文件系统里根本不会存在这种名字的库,所以必然报找不到。翻译成人话就是:构建系统把一个本不该出现在链接命令里的字符串,硬塞给了链接器。真正要查的不是系统缺库,而是这个字符串是从哪儿冒出来的。
1.2 Boost::timer 本来是 CMake 的 target 名
Boost 的 timer 组件真实存在,库文件叫libboost_timer.so,但Boost::timer这种写法不是文件名,而是 CMake 里"导入目标"(imported target)的名字。find_package(Boost COMPONENTS timer)执行成功之后,CMake 会在内部生成一个名为Boost::timer的目标,你在target_link_libraries里写它,CMake 会把这个目标解析成实际的库文件路径,再传给链接器。这是一个封装得很好的抽象,你不需要关心库在哪个目录、叫什么名字。
问题就出在:如果find_package没有生成这个目标,而你的target_link_libraries里又写了Boost::timer,CMake 的默认处理方式是——不报错、不警告,把这个字符串当成普通库名,照样生成链接命令。于是链接器就收到了-lBoost::timer,后面的事大家都知道了。这种设计坑就坑在太"宽容",本来是你写错了,它不提醒你,还把错误一路送到最后一步才炸。
1.3 缺库和传错名,别混为一谈
很多教程一看到找不到 -lBoost::timer就让你apt install libboost-timer-dev,确实有人装完就好了,但这不代表问题根源就是缺库。我把两种情况放一起对比,你对着自己的报错看一眼就知道属于哪种。
| 报错形态 | 真实原因 | 典型场景 |
|---|---|---|
找不到 -lboost_timer | 系统确实没有这个库文件 | 没装 dev 包,或库路径不对 |
找不到 -lBoost::timer | CMake 目标未定义,字面量传给链接器 | CMakeLists 里 find_package 与链接写法不匹配 |
| 配置阶段出现 Could not find the following Boost libraries: boost_timer | 组件缺失,CMake 直接拒绝 | 缺libboost-timer-dev |
| 编译过程直接被 Killed | 内存不足,不是链接问题 | -j开太大,GTSAM/PCL 模板实例化吃内存 |
判断的方法很简单:看库名。小写的是真库,大概率是没装;带大写B和双冒号的是 target,优先查 CMakeLists。这个区分能帮你少走很多弯路,也决定了后面的修复方向完全不同。
2. LIO-SAM 的 Boost 链路,坑通常埋在哪
2.1 LIO-SAM 为什么和 Boost 绑在一起
LIO-SAM 全称是 Tightly-coupled Lidar-Inertial Odometry via Smoothing and Mapping,整套系统按功能拆成几个节点:imageProjection做点云预处理和去畸变,featureExtraction提取角点和平面点,mapOptmization负责因子图优化和地图维护,imuPreintegration做 IMU 预积分。节点之间靠 ROS 通信,核心优化部分依赖 GTSAM,点云处理依赖 PCL,图像部分依赖 OpenCV 和 cv_bridge。任何一个依赖在 CMakeLists 里声明了,编译期就绕不开。
Boost 在这里面扮演的角色比较"杂",它是一个大型 C++ 库集合,timer 组件主要用来做耗时统计。比如很多 fork 版本会在mapOptmization或点云处理模块里用boost::timer::cpu_timer去测各环节处理时间,方便调优。虽然 LIO-SAM 核心功能不用 timer 也能转,但既然 CMakeLists 里声明了这个依赖,链接阶段就必须正确解析它。
2.2 标准 CMakeLists 应该怎么写
LIO-SAM 的 CMakeLists.txt 里,和 Boost 相关的典型写法是这样:
find_package(Boost REQUIRED COMPONENTS timer) ... include_directories( include ${catkin_INCLUDE_DIRS} ${PCL_INCLUDE_DIRS} ${Boost_INCLUDE_DIRS} ${GTSAM_INCLUDE_DIRS} ) ... target_link_libraries(${PROJECT_NAME}_mapOptmization ${catkin_LIBRARIES} ${PCL_LIBRARIES} ${Boost_LIBRARIES} ${GTSAM_LIBRARIES} )这里有两个关键点。第一,find_package(Boost REQUIRED COMPONENTS timer)里的COMPONENTS timer是必须的,它除了检查库是否存在,还会在 CMake 里生成Boost::timer这个目标。第二,链接时用${Boost_LIBRARIES}是"老式变量"写法,CMake 会把它展开成实际的库路径;如果你更喜欢新式写法,也可以改成一个Boost::timer目标,但前提是前面的find_package确实执行成功。两种风格都能用,不要混着写,更不能在没执行find_package的情况下去链接一个 target 名。
2.3 三种常见的"人为制造"错误
根据我在各种交流群和论坛看到的求助帖,这个错八成是下面三种情况之一。第一种,find_package没写组件就直接用 target。比如有人把find_package(Boost REQUIRED)写在前面,后面链接时写了Boost::timer。Boost 基础头文件都在,这个find_package能过,但因为没有指定COMPONENTS timer,CMake 不会生成Boost::timer目标,链接时自然就变成字面量传参了。
第二种,手动改链接库列表改出问题。有些人觉得${Boost_LIBRARIES}路径太长、看着不顺眼,自作聪明改成target_link_libraries(... Boost::timer)。如果前面没写完整的find_package,或者因某种原因执行失败,这就是事故现场。
第三种,变量被覆盖。有些工程里会这样写:
set(Boost_LIBRARIES "Boost::timer") target_link_libraries(my_node ${Boost_LIBRARIES})或者是某个第三方模块在父作用域里改了变量,结果${Boost_LIBRARIES}展开出来就是Boost::timer,链接器同样一脸懵。遇到这种问题,不要只盯着target_link_libraries那一行,往上看变量是在哪儿被赋值的。
2.4 还有一种隐蔽来源:依赖库"传染"出来的
排查时还要留个心眼:这个传错的字符串不一定来自你自己的 CMakeLists,也可能是某个依赖库的 CMake config 文件里带出来的。比如 GTSAM 或某些自己编译的第三方库,它们的xxxConfig.cmake里如果通过INTERFACE_LINK_LIBRARIES导出了Boost::timer,你的工程在链接那个库的时候,这个字符串就会跟着传进链接命令。如果那个库编译时依赖的 Boost 组件和你当前环境不一致,就会出现这种莫名其妙的目标未定义。
怎么判断来源?看完整链接命令最靠谱。如果完整命令里只能看到-lBoost::timer,而自己的 CMakeLists 里压根没写这个词,那基本可以锁定是从依赖库的导出接口带过来的。处理方式很简单:在你的工程里补上find_package(Boost REQUIRED COMPONENTS timer),让目标先定义出来,后面链接时就能正确解析了。
3. 实操流程:从定位到修复
3.1 第一步:打开详细构建日志,找到案发现场
很多人遇到编译错误,只看终端最后一屏就到处问人。正确做法是先拿详细日志。LIO-SAM 如果用 catkin_tools 构建,命令是catkin build lio_sam -v;如果用的 catkin_make,则是catkin_make VERBOSE=1。加了详细输出之后,你会看到类似这样的完整链接命令:
[ 88%] Linking CXX executable /home/user/catkin_ws/devel/lib/lio_sam/lio_sam_mapOptmization /usr/bin/ld: 找不到 -lBoost::timer collect2: error: ld returned 1 exit status make[2]: *** [CMakeFiles/lio_sam_mapOptmization.dir/build.make:1235: ...] Error 1注意看Linking CXX executable这一行,它标注了是哪个可执行文件链接失败,对应的是哪个 ROS 节点。再结合你自己的 CMakeLists,基本能定位到是哪个add_executable、哪一行target_link_libraries出了问题。这一步不需要任何技巧,就是耐心看日志。
3.2 第二步:检查 Boost 组件是否装全
虽然我认为这个报错的核心是 target 问题,但也不排除你恰好没装全。先把底数摸清。Ubuntu 上查 Boost timer 库是否安装,用这两条命令:
dpkg -l | grep libboost ls /usr/lib/x86_64-linux-gnu/libboost_timer*如果ls结果为空,或者只有libboost_timer.so.1.71.0而没有不带版本号的开发符号链接,那就确实缺 dev 包,装上:
sudo apt update sudo apt install libboost-timer-dev不想一个一个装,直接sudo apt install libboost-all-dev也行,就是包比较大。装完后用ldconfig -p | grep boost_timer确认一下动态库能被系统找到。做完这一步,至少排除了"物理缺库"这个变量,后面的排查就可以专心放在 CMake 配置上。
3.3 第三步:修改 CMakeLists,让 target 真正存在
排除缺库之后,重点回到 CMakeLists。核心修复思路只有一句话:让链接器收到的库名是真实的库文件路径,而不是Boost::timer这个字面量。有两种改法。改法一,保持新式 target 风格,补上组件声明。在文件里找到 Boost 相关的find_package,确认写的是find_package(Boost REQUIRED COMPONENTS timer),然后在链接处用Boost::timer:
target_link_libraries(${PROJECT_NAME}_mapOptmization ${catkin_LIBRARIES} ${PCL_LIBRARIES} Boost::timer ${GTSAM_LIBRARIES} )改法二,整体退回老式变量,用${Boost_LIBRARIES}代替所有手写的Boost::timer:
target_link_libraries(${PROJECT_NAME}_mapOptmization ${catkin_LIBRARIES} ${PCL_LIBRARIES} ${Boost_LIBRARIES} ${GTSAM_LIBRARIES} )我个人更推荐第一种,target 风格是 CMake 官方建议的方向,能自动处理传递依赖和路径细节。无论选哪种,改完都记得全局搜一下 CMakeLists 里还有没有::写法残留,别改了一半。另外提醒一句,如果报错来自某个依赖库的导出接口,你不需要去改那个库,在自己的 CMakeLists 里补上find_package(Boost REQUIRED COMPONENTS timer)就行。
3.4 第四步:清理缓存,完整重编译
CMake 是有缓存的。只改 CMakeLists 不清理,有时候不会重新触发配置,或者配置了但链接命令没更新,导致你以为改了没用。建议直接清掉构建产物:
# catkin_tools catkin clean -y catkin build lio_sam -v # catkin_make rm -rf build devel catkin_make -j4注意-j别开太大,这项目吃内存,-j4或者把$(nproc)减半都行。编译通过后,再用ldd看一眼可执行文件,确认它最终链接的是/usr/lib/x86_64-linux-gnu/libboost_timer.so这个真实路径:
ldd devel/lib/lio_sam/lio_sam_mapOptmization | grep boost到这一步,报错就算彻底解决。整个流程先看日志、再查依赖、再改配置、最后验证,每一步都有明确目的,不会瞎折腾。
4. 顺手把 CMake 的 :: 规则彻底搞明白
4.1 target_link_libraries 到底怎么解析参数
这个报错之所以反复出现,是因为很多人没搞懂target_link_libraries的参数解析规则。其实逻辑很简单,CMake 拿到你写的每一项,会分三种情况处理。第一,看这个字符串是不是一个已存在的 target。什么叫已存在?就是你在当前作用域里通过add_library、add_executable创建的目标,或者通过find_package导入的目标。如果是,CMake 走 target 语义,解析出它的实际文件路径和附带的使用要求。
第二,如果这个字符串以-l开头,CMake 会原样把它当作链接参数传下去,不做任何解析。第三,如果这个字符串既不是 target、也不以-l开头,CMake 就把它当成普通库名,在前面补一个-l再传给链接器。而带::的字符串恰好卡在这个分支——它不是合法 target,又不是-l开头,于是变成-lBoost::timer。
所以,问题的本质从来不是 Boost 没有 timer,而是你的 CMake 作用域里没有Boost::timer这个 target。一切围绕"这个 target 到底存不存在"来排查,思路就会非常清晰。别被报错里的"找不到"带偏,它不是找不到库,是找不到目标。
4.2 Boost 的目标命名体系和老式变量对照
Boost 的 CMake 导入目标都是Boost::开头,后面跟组件名,比如Boost::headers、Boost::timer、Boost::chrono、Boost::filesystem。和它们对标的还有一套老式变量,整理成表看得更清楚:
| 新式 target | 老式变量 | 对应真实库 |
|---|---|---|
Boost::headers | Boost_INCLUDE_DIRS | 头文件目录,无库 |
Boost::timer | Boost_TIMER_LIBRARY,也含在Boost_LIBRARIES | libboost_timer.so |
Boost::chrono | Boost_CHRONO_LIBRARY,也含在Boost_LIBRARIES | libboost_chrono.so |
Boost::filesystem | Boost_FILESYSTEM_LIBRARY,也含在Boost_LIBRARIES | libboost_filesystem.so |
这里要注意大小写。CMake 的 target 名区分大小写,Boost::timer是官方定义的名字,如果你写成boost::timer,同样会变成-lboost::timer的字面量。别小看这个细节,我见过有人排查了半天,最后发现是手滑把大写B写成了小写。这种错误编译器不会提醒,链接器只会莫名其妙地报找不到。
4.3 时序:为什么 timer 会牵连 chrono 和 system
还有一个值得了解的知识点:Boost 的 timer 组件不是孤立的。libboost_timer.so内部依赖libboost_chrono.so和libboost_system.so,你可以用ldd验证:
ldd /usr/lib/x86_64-linux-gnu/libboost_timer.so这也是为什么libboost-timer-dev安装时会自动拉上 chrono、system 等 dev 包。如果你遇到的是链接时提示-lboost_timer或-lboost_chrono找不到,大概率是这几个依赖组件没装全,直接装libboost-all-dev最省心。
另外,如果将来想切静态链接,可以在find_package之前设置set(Boost_USE_STATIC_LIBS ON)和set(Boost_USE_MULTITHREADED ON)。这两个开关会影响 CMake 寻找libboost_timer.a还是libboost_timer.so。LIO-SAM 这种 ROS 工程一般不需要折腾静态库,了解有这回事就行,真遇到再查官方文档。
5. 高频问题速查与避坑清单
5.1 ld 找不到 -lxxx 的通用排查表
这类-l报错并不只是 Boost 的专利,任何 C++ 工程都可能遇到,Qt/QML 工程、纯 CMake 项目也不例外。排查思路是通用的:先看链接命令里出现的库名是"真实库名"还是"target 名"。我把常见情况整理成表。
| 报错关键词 | 大概率原因 | 处理 |
|---|---|---|
-lboost_timer(小写) | 缺libboost_timer.so | apt install libboost-timer-dev |
-lBoost::timer(含冒号) | CMake target 未定义 | 检查find_package(Boost COMPONENTS timer)和链接写法 |
-lboost_chrono/-lboost_system | 缺配套 Boost 组件 | 装libboost-chrono-dev等,或用libboost-all-dev |
-lfoo(任意其他库) | 缺库或-L路径不对 | 用dpkg -S或apt-file查库属于哪个包,再安装 |
链接命令里出现奇怪的:: | 某个依赖库导出了未定义 target | 在工程里补对应find_package或清理依赖 |
这套表放之四海皆准。Qt/QML 那边报出/usr/bin/ld: 找不到 -lxxx时,处理逻辑完全一样:先搞清楚这个xxx是文件名还是 target 名,然后决定是装包还是改工程配置。
5.2 LIO-SAM 编译期的其他高频坑
既然聊到 LIO-SAM 编译,顺手把另外几个高频问题也列一下,省得你修完这个又撞上下一个。GTSAM 版本不兼容是最常见的,LIO-SAM 官方推荐特定版本的 GTSAM,如果你从源码编译的是最新 GTSAM,可能在链接时出现一些和 Boost 无关的undefined reference,报错位置非常零散。建议严格按照官方 README 的版本依赖来装,别手滑装了个太新的。
OpenCV 和 cv_bridge 的冲突在 Noetic 上也很典型。Ubuntu 20.04 默认 OpenCV 4,ROS Noetic 的 cv_bridge 默认也是针对 OpenCV 4 编译的,但如果你以前手动装过别的 OpenCV 版本,或者系统里有多个 OpenCV 共存,就会出现两个版本的头文件和库文件混链接,报错千奇百怪。处理方式是检查/usr/local下有没有自定义安装的 OpenCV,必要时把它暂时移走。
PCL 版本不一致也是老问题。LIO-SAM CMakeLists 里写的是find_package(PCL 1.7 REQUIRED),在 Noetic 上实际装的是 PCL 1.10,这个版本号只是最低要求,通常能过,但如果你在 conda 或者其他环境里也装了 PCL,就可能头文件混乱。编译前用pcl-config --version确认当前默认 PCL 是谁。
还有一个偏物理的坑:编译内存不够。LIO-SAM 编译时 GTSAM、PCL 相关的模板实例化非常吃内存,-j8经常被 OOM 干掉,表现为编译进程被Killed或者直接卡死。建议-j4,确实不行就-j2,多等几分钟总比反复被杀死强。
5.3 几个保命经验,都是踩坑踩出来的
最后分享几条亲身实践下来的经验,不是文档里会写的。第一,遇到-l找不到,永远先开 verbose 看完整链接命令,再决定是修库还是修 CMakeLists。终端最后那几行只是表象,完整命令才能暴露Boost::timer是从哪一行、哪一个变量展开出来的。这一步能省你至少半小时。
第二,改完 CMakeLists 千万别忘了清缓存。CMake 的缓存机制经常让人误判修复无效,catkin clean或者直接删build/ devel/,然后从头编译。很多人"改了没效果",其实就是因为少了这一步。
第三,不要用创建软链接的方式去骗过链接器。网上确实有人给出这种操作:sudo ln -s libboost_timer.so libBoost::timer.so。这种办法能让当前编译"通过",但完全绕过了问题本身,而且含冒号的符号链接会污染你的系统,以后别的工程会踩更深的坑。链接器报错是好事,它在提醒你工程配置有毛病,把配置板正才是正道。
第四,装 Boost 组件用系统包管理器,别去网上手动下载源码包自己编。系统包管理器能保证多个 Boost 组件的版本一致,手动编译很容易搞出版本错配,到时候libboost_timer和libboost_filesystem版本对不上,链接阶段又是一堆莫名其妙的 undefined reference。
说实话,这种链接错误在 SLAM 这个圈子里太常见了,几乎每周都有人在群里问。但它的排查逻辑其实非常固定:链接命令里的库名长什么样,决定了问题的方向。把这个思路内化成习惯,以后再遇到/usr/bin/ld: 找不到 -lxxx,你大概扫一眼报错就知道该往哪个方向修了。