ARTICLE DETAIL

资讯详情

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

Ubuntu交叉编译armadillo到aarch64:从工具链到CMake完整实战

Ubuntu交叉编译armadillo到aarch64:从工具链到CMake完整实战 开头说实话我第一次接到Ubuntu中交叉编译armadillo库这个需求时内心是有点懵的——armadillo不是个header-only的模板库吗直接拷贝头文件不就行了为什么要走交叉编译这么麻烦的流程后来真正动手做嵌入式ARM平台的项目才发现这事远没有拷贝头文件那么简单。armadillo自身确实是模板库但它在处理矩阵分解、特征值求解这类高性能运算时依赖的是BLAS和LAPACK这类底层数值库。你一旦需要armadillo在嵌入式板卡上跑出高性能就得把这些依赖库也一并交叉编译出来然后解决工具链、sysroot、链接参数、运行时路径等一系列问题。这篇文章就基于我在Ubuntu 20.04上为aarch64ARM64平台交叉编译armadillo的完整过程来写。适合需要把C矩阵运算项目部署到ARM Linux设备上的开发者不管是做机器人、嵌入式视觉还是边缘计算都值得参考。我会把为什么这样做的逻辑、每一步的具体操作、以及我在实际过程中踩过的坑都讲清楚保证你能照着复现。1. 为什么交叉编译armadillo这件事比想象中麻烦1.1 先理解armadillo的真实依赖结构armadillo是一个C模板库接口层面确实只需要包含头文件。但仔细看它的源码就能发现它有两层架构上层是模板化的用户接口下层是可选的BLAS/LAPACK后端。这层可选可不是真正可选的。当你的代码里出现求逆、特征值分解、SVD、QR分解这类操作时armadillo会调用底层BLAS/LAPACK的C接口比如dgemm_、dgesvd_。如果编译时没有链接这些库最常见的结果就是链接阶段报一堆undefined reference to dgemm_之类的错误或者程序能跑起来但性能差得离谱——armadillo的模板层内置了一套退化实现它做矩阵乘法的方式你可能不太想用。所以交叉编译armadillo本质上要做的是三件事第一把armadillo头文件放到目标平台的include路径下第二为目标平台准备好BLAS/LAPACK实现第三让你的项目能以正确的交叉编译方式找到这些库并链接成功。1.2 直接apt安装为什么不行很多人在Ubuntu上第一次装armadillo直接sudo apt install libarmadillo-dev就完事了确实方便。但一旦涉及交叉编译这个套路立刻失效。apt默认安装的是为当前宿主机架构x86_64编译好的.deb包里面的头文件虽然能直接用但自带的CMake配置文件和预编译库都是宿主机的。你拿aarch64的交叉编译器去链接宿主机的libarmadillo.so链接器第一时间就报错说架构不匹配librariy的ELF class根本不对。更麻烦的是嵌入式目标平台往往没有apt可用或者系统版本极老、包仓库早就停止维护。你没办法在板子上直接apt install libopenblas-dev。这种时候从宿主机交叉编译再拷贝过去几乎是唯一可靠的路。1.3 哪些项目才需要这么干如果你的项目只用了armadillo的基础模板运算比如自己实现的简单迭代算法那确实可以只拷贝头文件完全不依赖BLAS/LAPACK。但这种场景其实很少因为大部分值得用armadillo的项目看中的就是它背后成熟的BLAS/LAPACK数值能力。有几种典型场景我建议老老实实完整交叉编译嵌入式视觉项目涉及大量矩阵运算、协方差矩阵求逆、特征值分解卡尔曼滤波、EKF、状态估计类算法落地到ARM板卡需要在板子上做实时控制、滤波、信号处理的C程序。我这次的项目就是一套需要做批量矩阵运算和最小二乘拟合的嵌入式信号处理程序跑在aarch64的开发板上操作系统是厂家给的Linux 5.x内核。这种情况完全没法绕开交叉编译。2. 动手之前先把依赖关系和工具链选型理顺2.1 三层依赖每一层都得交叉编译armadillo对底层的依赖可以拆成三层来看层级作用是否必须交叉编译armadillo头文件提供C模板接口需要纯头文件可直接拷贝但建议用CMake安装管理BLAS实现矩阵乘、矩阵向量乘等基础运算必须否则性能不可接受LAPACK实现特征值、SVD、QR、线性方程组求解必须否则求逆和特征分解无法使用BLAS和LAPACK的范畴经常被人混淆。简单说BLAS管的是gemm、gemv这类基础线性代数运算LAPACK在BLAS之上提供更高层的数值分解功能。armadillo两者都要用。在ARM嵌入式平台上最常用的BLAS/LAPACK实现是OpenBLAS。它对ARM架构做了大量针对性优化比如基于NEON指令集的向量化、针对不同ARM核的微架构调优Cortex-A53/A72/A76都有专门的优化等级。对比一下通用的netlib BLAS在ARM上性能差距可以有数倍。既然都已经折腾交叉编译了没理由不用OpenBLAS。2.2 交叉编译工具链怎么选优先级和理由工具链是整个交叉编译流程的地基选不好后面全是问题。我强烈建议根据你的目标平台架构来定主流就是两种aarch64架构ARM64使用aarch64-linux-gnu-g32位ARM硬浮点armhf使用arm-linux-gnueabihf-g。在Ubuntu 20.04下安装命令分别是sudo apt install gcc-aarch64-linux-gnu g-aarch64-linux-gnu sudo apt install gcc-arm-linux-gnueabihf g-arm-linux-gnueabihf我这次目标平台是aarch64所以用的是前者。但有一个隐藏坑需要提前说明Ubuntu官方源里这套交叉编译器版本可能跟你板子上的glibc版本不匹配。比如Ubuntu 20.04自带的gcc交叉工具链用的是glibc 2.31如果你的板子系统较老、glibc版本低于编译器要求的版本编出来的程序在板子上会报version GLIBC_2.29 not found这类错误。遇到这种情况有两条路一是找开发板厂商官方SDK里携带的工具链通常跟板子系统严格匹配二是用Linaro提供的工具链。先跑一下aarch64-linux-gnu-gcc --version再在板子上查ldd --version两者对不上就先停下来解决问题别急着开始编译。2.3 sysroot是什么没有它寸步难行所谓sysroot你可以理解成目标平台根文件系统的一份精简副本里面包含目标平台的glibc库文件、链接器脚本、系统头文件。交叉编译器拿到sysroot之后才知道目标平台的C标准库长什么样链接时也才能正确处理动态库依赖。这里就容易出现一个非常常见的错误交叉编译时编译器默认可能尝试用宿主机的/usr/include和/usr/lib结果编出来的程序要么链接了x86_64的库要么头文件版本混乱导致各种奇怪的宏错误。获取sysroot最好的方式是从开发板的rootfs或者SDK里直接拷贝一份。部分板卡厂商的SDK会直接提供一个完整的rootfs目录里面有完整的lib、usr/include、usr/lib等目录结构。如果没有现成的可以在板子上执行这样的命令把关键目录打包拷回宿主机# 在目标板上 tar czf /tmp/sysroot.tar.gz /lib /usr/include /usr/lib然后拷到宿主机解压放到统一目录比如/opt/aarch64-sysroot。至少要有libc.so.6、ld-linux-aarch64.so.1、libm.so.6这些基本库在find /opt/aarch64-sysroot -name libc.so.6 -o -name ld-linux-aarch64.so.1有一个能正常工作的sysroot后面的所有步骤才有了立足点。3. 交叉编译OpenBLAS整个流程的地基3.1 为什么先编OpenBLAS而不是直接编armadillo从纯流程上讲armadillo的CMake在配置阶段会主动探测BLAS/LAPACK库。如果探测不到它会退回到自己的内置TinyBLAS实现或者干脆禁用掉相关功能。为了确保armadillo配置出来能用上高性能后端我选择先把OpenBLAS编译安装到sysroot里这样armadillo的CMake探测阶段就能直接找到它。3.2 OpenBLAS交叉编译的完整命令与参数注解先拉源码git clone https://github.com/xianyi/OpenBLAS.git cd OpenBLAS实际上交叉编译OpenBLAS比想象中简单它自带对交叉编译的良好支持。关键在编译参数make CCaarch64-linux-gnu-gcc FCaarch64-linux-gnu-gfortran HOSTCCgcc TARGETARMV8 -j$(nproc) make PREFIX/opt/aarch64-sysroot/usr/local install这里几个参数需要逐个说清楚CCaarch64-linux-gnu-gcc指定C编译器为交叉编译器FCaarch64-linux-gnu-gfortran指定Fortran编译器。OpenBLAS里部分代码用Fortran编写实例代码里LAPACK的某些部分也需要Fortran编译器。如果没装Fortran交叉编译器先执行sudo apt install gfortran-aarch64-linux-gnuHOSTCCgcc这个参数最容易忽略。OpenBLAS在构建过程中需要在宿主机上生成一些辅助工具比如代码生成器这些工具必须用宿主机的gcc编译否则会编译出aarch64的可执行文件然后在x86_64上运行直接报cannot execute binary fileTARGETARMV8指定目标CPU微架构。这里是强制指定为ARMv8通用架构。如果你明确知道板子CPU型号比如Cortex-A53可以改成TARGETCORTEXA53性能还能再提升一点。不指定的话OpenBLAS尝试运行时自动探测交叉编译环境下探测脚本跑不了大概率会失败。编译完成后确认一下产物架构file /opt/aarch64-sysroot/usr/local/lib/libopenblas.a输出里必须有ARM aarch64的字样看到这个就说明编译架构对了。3.3 链接器脚本的问题比想象中隐蔽OpenBLAS编译完成后我遇到过一个比较隐蔽的问题它在lib目录下生成了一些.so链接文件指向实际的版本号后缀。这些链接文件在交叉编译的sysroot里需要保持有效拷贝到目标板时也不能漏。另外当你的板子系统较老动态库的.so.0符号链接缺失时程序运行时会报告找不到libopenblas.so.0。排查时先用aarch64-linux-gnu-readelf -d 你的程序查看依赖列表再去sysroot里检查对应的库文件是否存在、符号链接是否完整基本能定位问题。4. 配置CMake工具链文件交叉编译armadillo的正式步骤4.1 环境变量方案的问题如果你在网上搜索交叉编译的简易方法可能会看到有人建议直接设CC、CXX环境变量再跑cmakeexport CCaarch64-linux-gnu-gcc export CXXaarch64-linux-gnu-g cmake ..这种方式在简单项目上确实能跑通但做交叉编译时隐患很大。最典型的问题在于CMake对库和头文件的探测逻辑不会因为CXX被改了就去sysroot里找依赖它仍然倾向于搜索宿主机的/usr/lib、/usr/include。一旦armadillo探测到宿主机的BLAS库后面编译出来的东西就乱套了。所以正规起见必须使用CMake工具链文件。4.2 我使用的工具链文件全文在armadillo源码根目录下创建toolchain-aarch64.cmake内容如下set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR aarch64) set(CMAKE_SYSROOT /opt/aarch64-sysroot) set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc) set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g) set(CMAKE_FIND_ROOT_PATH /opt/aarch64-sysroot) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_CROSSCOMPILING_EMULATOR qemu-aarch64)文件里几个关键点值得展开CMAKE_SYSROOT让编译器把sysroot作为根目录来寻找头文件和库CMAKE_FIND_ROOT_PATH告诉CMake的find_package和find_library命令去哪里搜索依赖CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER程序搜索不走sysroot。这个很关键因为CMake在探测过程中需要执行一些辅助程序这些程序必须是宿主机版本的去sysroot里找一个aarch64的可执行文件执行会直接失败CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY只允许在sysroot里搜索库。这样就不会误抓到宿主机里的liblapack.soCMAKE_CROSSCOMPILING_EMULATOR qemu-aarch64如果宿主机装了QEMUCMake可以在配置阶段用qemu-aarch64跨架构运行目标平台的测试程序。这一步很有用但也可选前提是sysroot里的动态库能配合QEMU正常工作通常还需要设置QEMU_LD_PREFIX环境变量指向sysroot。如果没装QEMU可以先把这一行注释掉不影响主要的交叉编译。4.3 armadillo的CMake配置命令与关键选项下载armadillo源码并解压后进入源码目录执行wget https://sourceforge.net/projects/arma/files/armadillo-12.6.6.tar.xz tar xf armadillo-12.6.6.tar.xz cd armadillo-12.6.6然后创建build目录避免直接在源码目录下留一堆临时文件mkdir build cd build cmake .. \ -DCMAKE_TOOLCHAIN_FILE../toolchain-aarch64.cmake \ -DCMAKE_INSTALL_PREFIX/opt/aarch64-sysroot/usr/local \ -DDETECT_HDF5OFF这里必须解释两个选项DETECT_HDF5OFF是因为armadillo的CMake配置阶段会探测HDF5库这个库在大多数嵌入式场景下用不到而且它的跨平台探测经常会出问题。干脆关掉省得它干扰整个配置流程。配置过程中最需要关注的输出是类似这样的两行-- Found BLAS: /opt/aarch64-sysroot/usr/local/lib/libopenblas.a -- Found LAPACK: /opt/aarch64-sysroot/usr/local/lib/libopenblas.a注意看路径两个必须指向sysroot里的OpenBLAS。如果这两行显示的是/usr/lib/x86_64-linux-gnu/libblas.so说明工具链文件的FIND_ROOT_PATH没有起效立刻停下来检查路径配置。此时继续往下编译armadillo安装出来的配置是错的后面联调你的项目时还会踩坑。确认无误后执行编译和安装make -j$(nproc) make install来看看安装后的产物find /opt/aarch64-sysroot/usr/local/include/armadillo* -maxdepth 0 ls /opt/aarch64-sysroot/usr/local/lib/libarmadillo*armadillo的安装产物主要是一个armadillo头文件目录、一个armadillo_bits目录以及若干libarmadillo.so相关的库文件。它本质上是把模板实现和一层薄封装打包成库编译项目时仍然需要头文件路径和库链接信息。4.4 验证armadillo是否真的找到了OpenBLAS一个我在实际项目中养成的好习惯是安装完成后马上检查生成的libarmadillo.so链接了哪些库aarch64-linux-gnu-readelf -d /opt/aarch64-sysroot/usr/local/lib/libarmadillo.so | grep NEEDED正常情况下能看到libopenblas.so在NEEDED列表中。如果只看到libm.so、libgcc_s.so这些基础库而完全没有OpenBLAS很可能armadillo没探测到LAPACK退化使用了内部实现。这种问题不会让编译失败但你的矩阵乘法性能会惨不忍睹。5. 编写测试程序验证全套环节跑通5.1 测试程序应该覆盖哪些功能交叉编译完成不等于万事大吉。我建议写一个覆盖多类矩阵运算的测试程序用file、readelf检查架构有条件的话直接在目标板或QEMU里跑一遍。测试代码可以这样写把基础乘法、求逆、特征值分解都覆盖到#include armadillo #include iostream int main() { arma::mat A arma::randuarma::mat(6, 6); // 矩阵乘法走BLAS的dgemm arma::mat B A * A.t(); // 求逆走LAPACK arma::mat C arma::inv(B); std::cout inv(0,0) C(0, 0) std::endl; // 对称矩阵特征值分解 arma::vec eigval; arma::mat eigvec; arma::eig_sym(eigval, eigvec, B); std::cout eigval eigval.t(); // 求解线性方程组 arma::vec x arma::solve(A, arma::onesarma::vec(6)); std::cout solve ok, x(0) x(0) std::endl; return 0; }这几个操作分别覆盖了BLAS层乘法和LAPACK层求逆、特征分解、线性方程组。如果链接的后端有问题很容易在其中一个调用上暴露出来。5.2 手动编译测试程序理解链接参数测试程序单独建目录不放在armadillo源码目录里避免CMake探测路径出问题。编译命令aarch64-linux-gnu-g test_arma.cpp \ -I/opt/aarch64-sysroot/usr/local/include \ -L/opt/aarch64-sysroot/usr/local/lib \ -larmadillo -lopenblas \ -o test_arma执行后先看架构file test_arma输出必须包含ELF 64-bit LSB executable, ARM aarch64这样的内容。再检查动态库依赖aarch64-linux-gnu-readelf -d test_arma | grep NEEDED注意-larmadillo和-lopenblas的顺序。GNU链接器在静态链接场景下对库的顺序非常敏感被依赖的库要放在依赖它的库后面。虽然这里armadillo主要提供动态库但为了规范起见保持这个顺序先-larmadillo再-lopenblas能避免不少莫名其妙的undefined reference问题。5.3 在QEMU里先跑一遍不用板子也能验证宿主机装了QEMU用户态模拟器的话就算板子不在手边也能直接运行aarch64程序sudo apt install qemu-user qemu-aarch64 -L /opt/aarch64-sysroot ./test_arma-L指定sysroot路径QEMU通过它加载aarch64版本的动态链接器和共享库。如果程序能正常跑出特征值结果说明整条工具链、sysroot、OpenBLAS、armadillo的交叉编译全部正确。如果QEMU运行时报错invalid ELF header或者FATAL: kernel too old多半是sysroot里的库版本和QEMU模拟的内核版本不匹配优先检查sysroot的glibc版本再检查QEMU版本是否太旧。5.4 上板运行时的真实排错过程板子上的真实运行和QEMU还是有区别。把test_arma连同需要的动态库拷贝到开发板比如放到/opt/test_arma和/opt/lib下然后在板子上执行export LD_LIBRARY_PATH/opt/lib:$LD_LIBRARY_PATH ./test_arma常见的板端运行错误无非三类排查方法如下表报错信息根因处理方法error while loading shared libraries: libarmadillo.so.11: cannot open shared object file动态库路径不对export LD_LIBRARY_PATH指向库所在目录或用-rpath链接选项error while loading shared libraries: libopenblas.so.0: cannot open shared object file拷贝库时遗漏了OpenBLAS检查readelf -d输出的所有NEEDED库全量拷贝Illegal instruction (core dumped)OpenBLAS的TARGET指定了板子不支持的指令集用/proc/cpuinfo查实际CPU型号重新编译OpenBLAS降低TARGET级别其中第三种情况最坑。我第一次在开发板上跑OpenBLAS时测试程序一执行就直接Illegal instruction第一反应还以为是编译器的问题。后来逐条排查才发现OpenBLAS默认启用的线程本地存储机制在特定内核配置下偶发异常加上部分ARM核的原子指令支持差异导致非法指令。把OPENBLAS_NUM_THREADS1环境变量加上或者重编OpenBLAS时加入USE_OPENMP0参数问题就解决了。针对性能敏感的程序在上板运行时建议统一用环境变量控制OpenBLAS线程数export OPENBLAS_NUM_THREADS4 export OPENBLAS_VERBOSE0嵌入式板子的CPU核心数通常有限默认的自动线程检测有时会开太多线程反而拖慢性能。6. 交叉编译过程中绕不开的几个坑6.1 缓存污染和宿主机库干扰交叉编译时CMakeCache和宿主机库干扰是两个最隐蔽的问题。CMake的探测结果会缓存起来如果你在同一个build目录里先做了宿主机构建或者试过不同工具链再切到交叉编译缓存里可能残留着宿主机路径。哪怕工具链文件写对了缓存仍然把路径指向/usr/lib/x86_64-linux-gnu/libopenblas.so。处理方式很粗暴也很有效每次切换工具链前直接删掉build目录重新创建。别心存侥幸CMakeCache的坑不值得花时间研究。另外包含路径也要小心。在交叉编译项目里-I/usr/include、-L/usr/lib这类宿主机路径不要出现在编译参数里。一旦混入头文件冲突和架构不匹配的问题会成串出现而且报错信息非常难定位。所有依赖的头文件和库统一走sysroot路径。6.2 C ABI版本不一致的隐性报错宿主机编译器和交叉编译器版本不一致会引发一类特别隐蔽的链接错误。比如宿主机gcc是11.x交叉编译器是9.xC标准库的std::string、std::list等类内部布局不同链接时会出现符号对不上或程序运行到字符串处理就崩溃。这种问题排查起来极其痛苦根因就是宿主机编译器和交叉编译器的_GLIBCXX_USE_CXX11_ABI宏定义不一致。我在项目里吃过一次亏宿主机装了一堆新版本库交叉编译的sysroot里是旧版本两边混合用了最后所有代码统一用交叉编译工具链来编问题才消失。所以交叉编译要有洁癖宿主机上编译的C库绝不直接拿给交叉编译器链接sysroot里所有的库必须全部由交叉工具链编译生成。混用一次可能没问题但出现问题的时候先怀疑这个。6.3 链接器找不到共享库或静态库时的排查思路链接阶段报cannot find -lopenblas或者cannot find -larmadillo时按这个顺序排查ls /opt/aarch64-sysroot/usr/local/lib/libopenblas*确认库文件确实存在检查-L参数是否指向了正确路径特别注意/usr/local/lib在sysroot里对应的完整路径是/opt/aarch64-sysroot/usr/local/lib检查库文件后缀是否有.so或.a检查CMAKE_FIND_ROOT_PATH是否把sysroot的/usr/local也算进去了。我在流程中把OpenBLAS和armadillo都安装到了/opt/aarch64-sysroot/usr/local下就是为了让CMake的find_library在搜索/usr/local时直接命中sysroot里的库避免手动加一堆-L参数。6.4 用CMake管理的项目应该怎么引入这套环境如果你的实际项目是用CMake管理的强烈建议在项目中复用上面写的工具链文件。在项目根目录的CMakeLists.txt中通过find_package(armadillo REQUIRED)来引入然后让用户在配置阶段显式指定交叉工具链cmake -DCMAKE_TOOLCHAIN_FILEpath/to/toolchain-aarch64.cmake ..项目里的CMakeLists.txt可以这样写find_package(armadillo REQUIRED) add_executable(my_app main.cpp) target_link_libraries(my_app PRIVATE armadillo)armadillo自带的CMake配置在find_package时会读取安装时生成的ArmadilloConfig.cmake文件这个文件里记录了它编译时链接的BLAS/LAPACK库。因为我们安装时配置正确它会把libopenblas自动带出来不需要在CMakeLists里手动写-lopenblas。当然前提是ArmadilloConfig.cmake文件必须位于sysroot内并且find_package能通过CMAKE_PREFIX_PATH或默认路径找到它。我安装到/opt/aarch64-sysroot/usr/local后在CMake配置中添加-DCMAKE_PREFIX_PATH/opt/aarch64-sysroot/usr/local这样find_package(armadillo)就能稳当地找到sysroot里的安装版本不会去翻宿主机目录。7. 一点收尾的个人经验整套流程跑通之后你会发现交叉编译armadillo其实没有想象中可怕核心就是三件事一个跟目标平台匹配的工具链、一个完整的sysroot、一套严格控制查找路径的CMake配置。把这三点理顺后面不管编OpenBLAS还是armadillo以及你项目里其他需要交叉编译的第三方库都是同一个思路。我个人还有一个习惯就是在拿到新板子的第一天就把板子的CPU型号、glibc版本、根文件系统路径等信息记下来连同板厂SDK里的工具链版本一起写进项目的README。后面每次做交叉编译先对一遍这些基线信息能省下大量排查问题的时间。另外如果真的只是为了在板子上跑一个不算复杂的armadillo程序上面这套流程可能显得有点重。但现在嵌入式开发的项目规模和复杂度都在涨一套可持续复用的交叉编译环境从长远看一定是值得投入的。至少我在这套环境上已经把好几个数值计算模块都编译进去了后面再加新依赖库整个流程基本是复制粘贴的节奏。
返回列表