ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Ubuntu编译WonderTrader避坑:boost与.so动态库路径全解析

Ubuntu编译WonderTrader避坑:boost与.so动态库路径全解析 简介压缩包内提供了一套在 Ubuntu 22.04、GCC 11.4.0 环境下完成编译的 WonderTrader 依赖库面向需要基于 C 量化交易框架搭建本地开发环境的开发者。包体中共有 2000 个文件绝大多数为头文件其中 hpp 格式 1856 个、h 格式 144 个整体压缩后大小约 14.5MB内容上涵盖 Boost 等第三方库的序列化、文档解析、日志格式、编码转换等接口声明与模板实现可被源码直接引用。资源已针对 WonderTrader 的 CMake 工程做了路径适配实际使用时先导出 MyDependsGcc 环境变量使其指向 /deps/wtcpp即可替代原工程默认的 /home/mydeps 路径省去手工修改构建脚本或逐个编译依赖项的繁琐步骤。目前已有 238 人学习下载适合熟悉 Linux 构建流程、希望快速集成 WonderTrader 并减少依赖编译时间的中高级 C 开发者作为基础运行环境使用。整体组织清晰便于按需取用依赖关系已提前固化可降低环境搭建门槛。1. WonderTrader 依赖库Ubuntu 上真正卡人的 boost 与 so 路径WonderTrader 是 C 写的开源量化交易框架代码结构干净但第一次在 Ubuntu 上从 git 拉下来编译十有八九不是被业务逻辑难住而是被 boost 依赖和一堆 .so 动态库反复摩擦。说白了这个项目对第三方依赖的编排比较重boost、TinyXML、数据库客户端一个不少构建脚本写得很灵活代价是运行期的依赖搜索路径非常敏感。这篇文章按我实际拆过的依赖库来讲在 Ubuntu 上先用 git 拉 WonderTrader 源码装对版本的 boost编译出 .so 产物最后把可执行文件复制到别的目录还能正常跑完整个流程。适合刚接触 WonderTrader、准备在 Linux 下自己动手编译的量化开发也适合被动态库路径问题反复折腾的 C 后端工程师。2. WonderTrader 依赖库图谱boost 动态库与 header-only 组件的边界2.1 依赖清单boost 是主角但不止 boostWonderTrader 的 src 目录按功能拆成 wtcore、wttrade、wtcompute 等模块共同点是都依赖一组第三方库。我拿到一份源码包先不看业务代码而是去翻 deps 目录和 CMakeLists把依赖分成三类第一类是 boost负责文件系统、线程、日期时间、智能指针这些基础设施第二类是 TinyXML负责解析项目里的 XML 配置文件第三类是数据库客户端MySQL 和 SQLite 的 C/C 接口负责历史数据回放和落盘。这三类在 Ubuntu 上的安装方式差别很大boost 有系统包也可以源码编译TinyXML 大概率内嵌源码MySQL 客户端则需要单独装 dev 包并设置额外的 include 和 lib 路径不能一概而论。boost 里面还要再分一层。像 smart_ptr、function、bind、variant 这些组件是 header-only 的实现在头文件里编译时不需要链接单独的系统库而 boost_system、boost_filesystem、boost_thread、boost_date_time 这几个会编译成独立的动态库运行期必须能在系统路径里找到。很多人只装了 libboost-dev编译期头文件都有程序也能编过一到运行就报 libboost_filesystem.so.1.x.x not found就是这个原因。所以我装依赖的时候直接上 libboost-all-dev省得后面缺一个组件补一个组件反复在 configure 和运行时之间来回折腾。TinyXML 这边倒是省心多数分支源码里自带一份CMake 构建时直接编译进目标可执行文件不会在外面找依赖。数据库客户端才是第二个容易踩雷的地方如果只需要回放本地 CSV 或者 SQLite 文件依赖很轻一旦要连 MySQL官方 dev 包安装后头文件在 /usr/include/mysql库文件在 /usr/lib/x86_64-linux-gnu/libmysqlclient.soCMake 里 find_package 经常扫不到需要手动把路径传进去。我一般会先在命令行确认库文件真实存在再决定 CMake 里数据库相关的开关怎么设绝不靠猜。2.2 为什么依赖库必须拆成动态库符号表与加载顺序C 的二进制产物对依赖库版本高度敏感这一点在 WonderTrader 这种多模块框架里格外明显。把依赖拆成动态库而不是全部静态编死好处是热点修复时只需要替换一个 .so不用重编所有上游可执行文件这在量化框架频繁迭代策略引擎和风控模块的节奏下非常实用坏处是程序运行起来后必须有一套明确的动态库搜索顺序任何一个环节脱节程序都会在进 main 函数之前直接退出。Linux 加载器在找依赖时按近似顺序搜索可执行文件里记录的 RPATH/RUNPATH然后是环境变量 LD_LIBRARY_PATH接着是 /etc/ld.so.cache 缓存的系统路径最后才是 /lib、/usr/lib 这类默认目录。把完全相同的可执行文件复制到另一台机器只要这台机器上这些搜索位置里的对应库不存在或者版本对不上启动就会报 error while loading shared libraries。这个报错信息很有迷惑性因为它没有告诉你文件在哪、搜了哪些路径只说“加载失败”第一次遇到时很容易对着目录里的 .so 文件发呆。RPATH 和 RUNPATH 是另一个隐蔽点。编译时如果 CMake 设置了 INSTALL_RPATH可执行文件里就会写死一串绝对路径指向编译机器上的 /home/xxx/wondertrader/build/lib。这个路径在编译机上当然没问题但把整个 build 目录拷到别的机器或者只把可执行文件拷走路径就失效了。很多人想当然觉得“把依赖的 .so 和可执行文件放同一个目录总该行了吧”实际上 Linux 加载器不会默认搜索可执行文件所在目录这不是 Windows 的 DLL 搜索逻辑。没有 $ORIGIN 声明之前加载器根本不会看旁边。这个特性直接解释了为什么网上那么多人问“qt 程序考到其他目录且考了依赖库为什么还是不能运行”——答案就在加载顺序里。2.3 静态库与动态库的取舍不要为了部署省事硬切静态构建期还会遇到一个选择题要不要把 boost 或者 mysqlclient 改成静态库编进产物这样部署时少带几个 .so。我在第 2.2 节说的依赖搜索问题让不少人第一反应就是“干脆全部静态链接”。这个思路在纯命令行工具上可行但在 WonderTrader 这种多模块框架上要慎重。项目本身对 boost 的依赖既有头文件又有动态库你把其中一个库换成静态版本boost::system 的 error_category 符号可能同时出现在主程序里和某个 .so 里运行时容易出现 typeinfo 匹配失败、异常无法正常抛出这类非常难查的诡异问题。数据库客户端同理。libmysqlclient.so 如果不想让目标机器装 MySQL 客户端可以改成 mysqlclient.a 静态链接但这个库还依赖 zlib、ssl、crypto 一串系统库静态之后并不会让部署变简单反而把依赖隐藏得更深。真出问题时ldd 看不出来strace 也难定位。所以我对依赖库的原则是能用动态就用动态用动态就老老实实把 rpath 和 LD_LIBRARY_PATH 理顺不要为了看起来干净去切静态库。这份“干净”会在后面某个深夜以一条完全看不懂的段错误补偿回来。3. Ubuntu 构建环境git 拉源码与 boost 版本对齐的三个关卡3.1 一条命令装齐编译链和依赖包Ubuntu 上编译 WonderTrader 我按顺序执行下面这组命令先装编译工具链再装第三方依赖sudo apt update sudo apt install -y git build-essential cmake sudo apt install -y libboost-all-dev libtinyxml-dev libmysqlclient-dev sudo apt install -y libssl-dev zlib1g-dev第一行的 build-essential 提供 gcc、g、makecmake 是构建系统本体git 是等会儿拉源码用的。第二行一次性把 boost 全家桶装好libtinyxml-dev 和 libmysqlclient-dev 分别对应配置解析和 MySQL 接口。第三行是 mysqlclient 的间接依赖少了它后面链接 mysqlclient 时可能报一堆 openssl 符号未定义。有人建议只装 libboost-system-dev、libboost-filesystem-dev 这些按需组件但依赖缺口会反复出现我第一次装的时候就是因为省事只装了几个组件后面 configure 阶段缺头文件、链接阶段缺库文件来回折腾了两个小时最后老实装全家桶。装完后验证一下 boost 到底装了哪些库文件这一步能省掉后面很多排查时间dpkg -l | grep libboost | head -20 ls /usr/lib/x86_64-linux-gnu/libboost_*.so | head -20第一条看包管理层面装了哪些组件第二条看真实的动态库文件名。Boost 动态库在 Ubuntu 上的命名是 libboost_system.so.1.71.0再带一个不带版本号的软链 libboost_system.so。编译期链接器找的是不带版本号的软链运行期加载器找的是带完整版本号的真实文件所以两条命令都要执行。软链如果断了编译能过运行必挂这是典型的“两段式翻车”。3.2 git clone 与子模块拉不全会卡在 CMake 阶段WonderTrader 源码托管在 GitHub标准拉取方式是git clone https://github.com/wondertrader/wondertrader.git cd wondertrader git submodule update --init --recursive第三行是关键。仓库里有一部分第三方代码通过 git submodule 引用只做 clone 不更新子模块目录里会是空的或者只有占位文件cmake 配置阶段会直接报找不到目录。我第一次栽在这里clone 完直接 mkdir build 就开始编结果报一堆 include 文件缺失还以为是网络问题后来才反应过来是子模块没拉全。git submodule update --init --recursive 里的 --recursive 参数保证嵌套子模块也被拉下来这个参数不能省。拉完后检查一下目录结构重点看 src、deps 和根目录的 CMakeLists.txt 是否存在。src 下是 wtcore、wttrade、wtcompute 这些核心模块deps 下是内嵌的第三方库。如果 deps 是空的说明子模块那一步出问题了回到上面重跑一遍。这里我建议用 git status 看一眼工作区是否干净干净再进构建避免本地改动干扰排错。3.3 版本验证头文件版本和动态库版本为什么对不上这是 Ubuntu 上最隐蔽的 boost 问题。Boost 头文件路径是 /usr/include/boost库文件路径是 /usr/lib/x86_64-linux-gnu两者都来自同一个 apt 包时版本自然一致。但如果你为了追求新特性手动从源码编译了 Boost 1.82并用 BOOST_ROOT 或 CMAKE_PREFIX_PATH 指到了新头文件目录而动态库还是系统的 1.71就会出现“编译期一切都好链接期一堆 undefined reference”的怪象。在编译前把版本打印出来验证grep -n BOOST_LIB_VERSION /usr/include/boost/version.hpp ls /usr/lib/x86_64-linux-gnu/libboost_system.so.1.*第一行输出的是头文件声明的 boost 版本宏格式一般是 1_71 或 1_74。第二行看的是系统里真实存在的 boost_system 库版本号文件名带完整版本号。两者如果不一致链接时就会出现找不到符号或者符号签名不匹配的报错。解决方式也很朴素全部走 apt 的 libboost-all-dev不要混用源码编译的头文件和系统库除非你对版本有硬性要求。如果确实要指定 boost 版本正确的做法是源码编译时把 include 和 lib 都放到同一个前缀目录下比如 /opt/boost-1_82然后让 CMake 的 BOOST_ROOT 同时指向那里。4. 编译与部署CMake 开关、.so 产物与 $ORIGIN 路径4.1 编译命令和常用开关参数源码就绪后我一般用 out-of-source 方式构建编译目录不放在源码目录里避免污染 git 工作区mkdir -p build cd build cmake .. -DCMAKE_BUILD_TYPERelease -DBUILD_WTPYOFF -DBUILD_SAMPLESOFF make -j$(nproc)-DCMAKE_BUILD_TYPERelease 决定优化级别Debug 版带的断言和日志太多回放性能差距明显。-DBUILD_WTPYOFF 是关闭 Python 绑定模块 wtpy如果不需要用 Python 写策略就关掉能省掉一大截编译时间。-DBUILD_SAMPLESOFF 同理不编示例程序。make -j$(nproc) 里的 nproc 是 CPU 核心数写成 -j8 也可以但虚拟机里内存小的话建议 -j2否则容易 OOM。整个项目全量编译在普通桌面级 CPU 上大概十几到三十分钟算正常范围。如果中途报错不要直接重跑 make。先看错误是编译期还是链接期编译期报错多跟缺头文件有关系链接期报错多跟库路径和符号有关系两个排查方向完全不同。CMake 输出里最值得关注的是 Boost 检测行看到类似 Found Boost: /usr/lib/x86_64-linux-gnu/cmake/Boost-1.74.0 就说明路径没问题如果出现 Could NOT find Boost要检查 -DBOOST_ROOT 有没有指对位置。系统包安装的 boost 一般不需要手动指定最容易出问题的是你自己源码编译的 boost这时需要在 cmake 命令里显式写 -DBOOST_ROOT/opt/boost-1_82。4.2 构建产物与依赖关系谁依赖哪些 .so构建完成后在 build 目录下能看到两类产物一类是可执行文件比如 WtBtRunner、WtDtPorter另一类是动态库名字类似 libWtCore.so、libWtTrade.so具体命名因分支略有差异。部署前先跑一次 ldd 看依赖关系这是最直观的“这份程序拷贝出去到底要带哪些文件”的依据ldd WtDtPorter输出里会列出所有依赖的共享库包括 libWtCore.so、libboost_filesystem.so.1.74.0、libstdc.so.6 这一串。带绝对路径的是系统库只要目标机器有对应版本就行显示 not found 的是部署时需要重点处理的。注意ldd 输出的依赖列表跟可执行文件同一个目录里放了多少个 .so 没有关系它只认可执行文件内部的依赖记录和搜索路径这一点刚接触的人很容易误会。接下来是部署的取舍。把整个 build 目录拷过去在单机开发时最省事但到了多机部署就出问题——build 目录里某些路径是编译时用绝对路径写死的。更可控的做法是把可执行文件和服务端核心库挑出来放进一个干净的目录结构再用环境变量或 patchelf 解决搜索路径。下面这段就是部署时最常见的目录组织方式。4.3 部署到新目录为什么“依赖库就在旁边”还是不行几乎所有第一次接触 Linux 动态库的人都会踩同一个认知差以为把 .so 和可执行文件放在同一个目录程序启动时就会自动去旁边找。实际上 glibc 的加载器不会默认去可执行文件目录搜索只有显式声明 rpath 里的 $ORIGIN 才会那么做。$ORIGIN 是加载器提供的特殊变量运行时被展开成可执行文件真实所在目录。我常用这样的部署结构deploy/ ├── bin/ │ ├── WtDtPorter │ └── WtBtRunner └── lib/ ├── libWtCore.so ├── libWtTrade.so └── libboost_filesystem.so.1.74.0要让 bin 里的程序找到 lib 下的依赖最简单的办法是运行前设置环境变量export LD_LIBRARY_PATH$LD_LIBRARY_PATH:/absolute/path/to/deploy/lib ./bin/WtDtPorter这个办法在开发和单机临时部署时够用但有个问题环境变量是进程级别的supervisor 或 systemd 管理服务时要把变量写进配置文件一不留神就漏。而且这里写的是绝对路径部署目录一换就要改。更正规的做法是用 patchelf 把 $ORIGIN 写进可执行文件让程序自带搜索路径后面专门有一节讲。5. 避坑依赖库复制过去仍运行失败的四个真相5.1 现象一依赖库明明复制了启动仍然报 cannot open shared object file现象把 WtDtPorter 连同依赖的 .so 全部复制到另一台 Ubuntu 机器放在同一个目录执行时依然报 error while loading shared libraries: libWtCore.so: cannot open shared object file: No such file or directory。原因加载器没有把“可执行文件所在目录”纳入默认搜索范围。这个现象在 Qt 程序上同样常见很多人把 qt 程序考到其他目录且考了依赖库仍然运行不了就是因为 Linux 的搜索顺序里没有当前目录这一项。Windows 的 DLL 搜索会先找 exe 所在目录Linux 不会除非用 rpath 或 LD_LIBRARY_PATH 显式指定。解决先用 ldd 确认到底缺哪一个库再用环境变量或 patchelf 把路径指过去。临时验证用 export LD_LIBRARY_PATH... 要长期生效就写进服务的启动脚本。判断原则是报错信息里如果带绝对路径说明可执行文件里写死了 rpath如果只报 not found说明根本没有搜到对应库路径优先检查环境变量和 ldconfig 缓存。5.2 现象二ldd 显示 not found但库文件明明就在系统路径里现象ldd WtDtPorter 显示 libboost_filesystem.so.1.74.0 not found但 ls /usr/lib/x86_64-linux-gnu/libboost_filesystem.so.1.74.0 能看到文件确实存在。原因这个库是后装的或者换过版本ldconfig 的缓存没有刷新。加载器搜索系统路径时读的是 /etc/ld.so.cache 这个二进制缓存不会每次启动都去遍历 /usr/lib。文件在但缓存里没有索引等于不存在。另一个隐蔽原因是软链接断了libboost_filesystem.so 是软链真实文件是带版本号的软链断掉后 ldd 也会报 not found。解决先 ls -l 检查软链是否完整再执行 sudo ldconfig 刷新缓存最后重新 ldd 确认。如果库放在自定义目录 /opt/libs需要在 /etc/ld.so.conf.d/ 下新建一个 .conf 文件写入该目录再跑 ldconfig。这个坑跟版本无关纯粹是缓存机制但排查时最容易让人怀疑人生我后来只要看到 not found 就条件反射先跑 ldconfig。5.3 现象三boost 版本升级后链接期符号找不到现象把系统 boost 从 1.71 升到 1.74 之后重新编译 WonderTrader链接阶段报 undefined reference to boost::filesystem::path::lexically_normal() 这类错误但头文件明明找得到。原因header-only 部分和编译出的库文件版本不一致。boost 1.71 的头文件与 1.74 的 libboost_filesystem.so 混用时头文件里声明的新符号在旧库里不存在链接器自然报 undefined reference。升级 boost 时如果只升级了 libboost-dev 而遗留了旧版本库或者你是源码编译的 boost头文件和库没有对齐都会复现。解决把 boost 相关包彻底清干净再重装。执行 sudo apt remove --purge libboost* 再 sudo apt install libboost-all-dev确保头文件和库都来自同一个版本。如果确实要源码编译指定版本把头文件和库都放在同一个自定义前缀下用 BOOST_ROOT 一起指过去千万别一半系统一半源码这种混搭状态非常难排查。5.4 现象四库文件存在但报 GLIBCXX_3.4.30 not found现象在一台 Ubuntu 22.04 上编好的 WonderTrader 程序拷贝到 Ubuntu 18.04 的机器上运行报 libstdc.so.6: version GLIBCXX_3.4.30 not found。原因编译器版本比目标机器高。GCC 11 编译出的二进制依赖新版 libstdc 的符号表而 18.04 自带的 libstdc.so.6 较旧缺少这个符号入口。这不是 WonderTrader 特有的问题所有 C 程序跨机器部署都会遇到只是这个目录里塞了一堆 .so报错时机更迷惑人。解决三个方向一是目标机器升级 GCC 运行时sudo apt install gcc-11 或直接更新 libstdc6前提是系统发行版允许二是把编译机器的 GCC 降到与目标机器一致重新编译三是用 Docker 拉一个目标版本的基础镜像做编译基线从源头保证 ABI 一致。我现在一律走第三条最可控编译环境完全可复现依赖库版本也锁得住。6. .so 体检ldd 输出解读与 patchelf 修复 rpath6.1 ldd 输出里每一列怎么读ldd 的输出看起来简单但每一层含义不同。第一段是依赖库名第二段是加载器实际解析到的路径或者 not found。排查时把所有 not found 行整理成清单逐个判断是系统库缺失还是自定义库路径没指到。系统库缺失走 apt 装自定义库路径问题走 LD_LIBRARY_PATH 或 patchelf。最常用的命令是把问题库名一次性列出来ldd WtDtPorter | grep not found注意 ldd 有个局限它只直接读取 ELF 头里的依赖项不递归展开。如果 libWtCore.so 自己又依赖了某个缺失的库ldd WtDtPorter 不一定能报出来这时要单独对每个 .so 跑一次 ldd。我在排查启动失败时习惯先对可执行文件跑一遍再对目录里的每个 .so 跑一遍才能把真正的缺口找全。6.2 patchelf 修改 rpath一次修好部署路径开发机环境没问题、部署机就挂的场景最省心的修法是把 rpath 写进可执行文件让程序自己知道去哪里找库。patchelf 在 Ubuntu 上通过 apt install patchelf 安装修改命令如下patchelf --set-rpath $ORIGIN:$ORIGIN/../lib WtDtPorter$ORIGIN 是运行时变量加载器会把 $ORIGIN 展开成可执行文件所在目录。$ORIGIN 表示依赖库都在可执行文件的同级目录$ORIGIN/../lib 表示在上二级目录的 lib 目录找。上面这条命令把两个位置都写入搜索路径部署时用前面那种 bin 和 lib 分层的结构就能直接命中。修改完成后重新跑 ldd确认原来的 not found 都变成了实际路径再做一次真实启动测试因为 ldd 通过不代表运行时一定没问题有些符号冲突要进程起来才会暴露。从那以后我每部署一台新机器都强制走一遍流程ldd 看依赖、grep not found、patchelf 定 rpath最后真实启动一次才收工。这套习惯救过我好几次尤其是隔了几个月再回头碰 WonderTrader 的依赖库问题照着这个顺序十分钟就能定位。希望帮到你。本文还有配套的精品资源点击获取
返回列表