☰
Ubuntu下交叉编译armadillo到ARM平台:工具链、OpenBLAS与测试全攻略
2026/10/3 8:06:26 网站建设 项目流程

最近因为一个嵌入式项目,需要在ARM Cortex-A9的板子上跑矩阵运算,结果手头这台Ubuntu机器上装好的armadillo库派不上用场——主机编译出来的库是x86_64架构,直接拷贝过去铁定跑不起来。所以我把“Ubuntu中交叉编译armadillo库”这件事完整走了一遍,从工具链选型、交叉编译OpenBLAS、再到编译armadillo本体和验证测试程序,整个过程大概花了一下午,也踩了几个比较典型的坑。这篇就当一份实操备忘,给后面要在ARM目标板上用armadillo的同学做个参考,少走点弯路。

armadillo在C++线性代数库里的定位比较特殊,它是模板库,接口风格接近MATLAB,写起来非常顺手。但有一点要注意,它并不是纯粹的header-only:虽然核心模板都在头文件里,但矩阵乘法、特征值分解、SVD这些运算默认会去链接BLAS和LAPACK的实现,速度和纯模板实现完全是两回事。所以交叉编译的关键,其实不是armadillo本身,而是BLAS/LAPACK如何交叉编译出来、如何让armadillo找到它们。适合人群主要是这三类:在Ubuntu主机上做ARM交叉开发、需要在嵌入式Linux环境里跑矩阵运算的工程师,刚接触交叉编译想搞清楚“工具链、依赖、目标架构”怎么串联起来的新手,以及被各种链接报错折磨得想摔键盘的倒霉蛋。

1. 交叉编译armadillo库的整体思路

1.1 armadillo库的“编译”到底编译了什么

先把这个库拆开看。armadillo的源码主要分成两部分:include/armadillo_bits下面的大量模板头文件,以及src/目录下的少量实现文件(主要和wrapper有关)。模板部分在你编译用户程序的时候会被实例化,真正需要单独编译出来的东西是一个libarmadillo.so或者libarmadillo.a,它的作用是把底层BLAS/LAPACK函数封装成armadillo内部的wrapper符号。

也就是说,armadillo本身编译一次之后,头文件基本就固定了,后续你写用户代码,只要include头文件并链接libarmadillo即可。但在编译armadillo的时候,CMake会去检测系统里的BLAS和LAPACK,如果找到了,就会在生成的config.hpp里定义ARMA_USE_BLAS和ARMA_USE_LAPACK这两个宏,然后用户程序里所有的线性代数运算都会走这些外部库。如果没找到,它也不会报错,而是退化成纯C++实现的小规模矩阵运算,矩阵一大性能就非常难看。

这里用个生活化的类比:armadillo本身像是一个菜谱,模板部分是把菜谱印在脑子里,真正做菜用的是锅和火——锅就是BLAS/LAPACK。你光有菜谱没锅,菜也能做出来,但只能做小份凉菜,量大就抓瞎。交叉编译就是要在目标平台上先把“锅灶”准备好,然后确保armadillo这个“菜谱”知道去哪找锅。

这就引出了交叉编译的第一个关键点:你必须在目标平台的sysroot或者一个自定义前缀目录里准备好ARM版本的BLAS/LAPACK,然后让armadillo的CMake去找它们。否则,就算armadillo编译成功了,它也可能是“裸奔模式”,到板子上跑起来性能不对,而且高层的函数可能直接抛异常。

1.2 交叉编译的核心难点在哪里

交叉编译本身不难,难的是“让CMake找到正确的ARM版本依赖”。常见的坑是:主机上明明装了libopenblas-dev和liblapack-dev,然后config.hpp里也检测到了,编译也过了,结果链接用户程序时却报出一堆relocation错误,因为链接器拿到的是x86_64的.so,和目标机的ARM代码对不上。这种问题最烦人,因为在配置阶段一切正常,偏偏到最后一步才炸锅。

所以整体方案其实很清晰,就是三步:

  • 在Ubuntu主机上装ARM交叉编译工具链。
  • 用这套工具链先交叉编译OpenBLAS(同时提供BLAS和LAPACK接口),安装到一个指定前缀目录,比如/opt/arm/armadillo-deps。
  • 再用CMAKE_TOOLCHAIN_FILE指定工具链,并让CMake只在这个前缀目录里搜索依赖,最后编译、安装armadillo本体。

这三个步骤的顺序不能换,尤其不能跳步。有人会想,是不是可以直接下载OpenBLAS的预编译ARM版本?确实也有,但版本和工具链的匹配度是个问题,而且很多预编译包是基于特定glibc版本构造的,放到板子上不一定能跑。自己用目标工具链编一遍是最稳妥的方案,后面排查问题也容易。

在动手之前,还有一件事值得做:确认目标板的架构和浮点特性。有些板子是32位ARMv7,有些是64位ARMv8,还有浮点单元是否支持硬浮点,这些都决定了工具链的选型。如果拿32位工具链去编64位目标,或者硬浮点/软浮点不匹配,后面一堆莫名其妙的问题就来了。

2. 环境准备与依赖库交叉编译

2.1 工具链选型与安装

先确认目标板的架构。我这块板子是32位ARM Cortex-A9,支持硬件浮点,Ubuntu主机上直接安装arm-linux-gnueabihf这套工具链:

sudo apt update sudo apt install -y gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf gfortran-arm-linux-gnueabihf

这里特别注意,gfortran-arm-linux-gnueabihf这个包特别容易漏装。OpenBLAS在编译时会调用Fortran编译器去处理LAPACK相关代码,不装Fortran的话可以设NOFORTRAN=1绕过,但那样LAPACK的高层功能会有缺失,armadillo的一些函数会受到限制,所以能用Fortran还是尽量装全。

如果是64位ARM目标板,就把前缀换成aarch64-linux-gnu,对应安装gcc-aarch64-linux-gnu、g++-aarch64-linux-gnu、gfortran-aarch64-linux-gnu。工具链装好后可以用下面命令确认版本:

arm-linux-gnueabihf-g++ --version

另外建议确认一下当前CMake版本。armadillo 12.x对CMake的最低要求是3.8以上,Ubuntu 20.04自带的CMake 3.16完全够用;如果版本太低,建议用pip安装新版或者下载官方二进制,避免出现奇怪的配置问题。顺带说一句,如果你是用WSL里的Ubuntu做这个事,路径问题要多留个心眼,WSL的文件系统和Windows共享,有时候CMake在跨文件系统拷贝时行为会有点奇怪,最好把所有工作目录放在Linux侧。

如果还不确定板子的架构,可以在板子上执行uname -m,armv7l表示32位ARM,aarch64表示64位。这个命令值得在项目一开始就做,能省掉后续很多猜测。

2.2 交叉编译OpenBLAS作为线性代数后端

OpenBLAS同时实现了BLAS和LAPACK接口,对于armadillo来说一个库就够了,这比分别编译BLAS和LAPACK要省事很多。我源码编译的是v0.3.24,这个版本相对稳定,社区反馈也多,遇到问题容易搜到答案:

git clone https://github.com/xianyi/OpenBLAS.git cd OpenBLAS git checkout v0.3.24

编译命令里重点是这几个变量:

make CROSS_COMPILE=arm-linux-gnueabihf- \ CC=arm-linux-gnueabihf-gcc \ FC=arm-linux-gnueabihf-gfortran \ HOSTCC=gcc \ TARGET=ARMV7 \ NOFORTRAN=0 \ USE_THREAD=1 \ PREFIX=/opt/arm/armadillo-deps

逐个解释一下参数的意义。CROSS_COMPILE是交叉编译器的通用前缀,OpenBLAS的Makefile会根据这个变量拼接出交叉编译的ar、as、ld等工具;CC和FC指定C和Fortran编译器;HOSTCC这个参数很关键,必须还是主机上的gcc,因为OpenBLAS在编译过程中会生成一些辅助工具并在本机运行,如果用交叉编译器直接运行会报段错误;TARGET=ARMV7告诉OpenBLAS生成针对ARMv7指令集的优化代码,如果你的板子具体型号明确,比如Cortex-A9,也可以直接写TARGET=CORTEXA9,优化效果更好;USE_THREAD=1开启OpenMP多线程支持,板子上多核的话性能提升明显,但也要确认目标系统里有libgomp运行时,没有的话就改成0。

编译完成后执行安装:

make install

安装完成后检查一下/opt/arm/armadillo-deps/lib下面有没有libopenblas.so、libopenblas.a,同时用file看一下文件类型:

file /opt/arm/armadillo-deps/lib/libopenblas.so

如果输出里显示ARM,那就对了。

这里有个很容易踩的坑:有些版本的OpenBLAS安装后,libopenblas.so是指向带版本号文件的软链接,但也可能只有带版本号的.so.0文件,没有不带版本号的libopenblas.so。armadillo的wrapper在链接时查找的是libopenblas,如果链接目录里没有这个名称,CMake就会认为OpenBLAS不存在。我在实际编译armadillo时就遇到过CMake检测OpenBLAS又检测不到的情况,排查半天才发现是软链接的问题。处理方式很简单:

ln -sf /opt/arm/armadillo-deps/lib/libopenblas.so.0 /opt/arm/armadillo-deps/lib/libopenblas.so

这个细节不贵,但很值得记下来。

3. armadillo库的交叉编译实操

3.1 编写CMake工具链文件

armadillo用CMake构建,交叉编译第一步是写一个工具链文件。它的作用很简单:把所有和“目标平台”相关的配置集中起来,让CMake知道编译器是谁、目标架构是什么、应该去哪里找依赖,从而避免去主机目录里找头文件和库文件。

我在项目目录下新建了arm-linux-toolchain.cmake,内容如下:

set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR arm) set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc) set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++) set(CMAKE_Fortran_COMPILER arm-linux-gnueabihf-gfortran) set(CMAKE_FIND_ROOT_PATH /opt/arm/armadillo-deps) 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_FIND_ROOT_PATH_MODE_PACKAGE ONLY) set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

CMAKE_SYSTEM_NAME必须设置成Linux,否则CMake默认是在编译本机代码,后面可能整出一堆问题。CMAKE_FIND_ROOT_PATH是这套配置里最核心的变量,它指定一个“虚拟根目录”,配合下面三个MODE变量,CMake在查找头文件、库文件、包配置时就会只在这个目录范围内找,而不会挨个去/usr/include、/usr/lib这些主机目录翻。

CMAKE_TRY_COMPILE_TARGET_TYPE设置成STATIC_LIBRARY是为了跳过CMake在配置阶段编译并运行测试程序的步骤。因为目标平台是ARM,交叉编译出来的测试程序在x86主机上根本跑不了,不设置这一条,配置阶段就会直接失败报错,而且报错信息还不直观。这一行是交叉编译项目里通用的“保命配置”,很多第三方库的交叉编译都会用到。

3.2 编译安装armadillo本体

armadillo源码可以直接从GitHub下载稳定tag,我不建议用master分支,因为master上可能有新增功能带来的不确定性,稳定版本更靠谱:

git clone https://github.com/conradsnicta/armadillo-code.git cd armadillo-code git checkout 12.6.7

然后创建build目录,开始配置:

mkdir build && cd build cmake .. \ -DCMAKE_TOOLCHAIN_FILE=../arm-linux-toolchain.cmake \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/opt/arm/armadillo-deps \ -DBUILD_SHARED_LIBS=ON \ -DDETECT_HDF5=OFF

如果配置成功,CMake会输出检测结果,注意看这两行:

-- Detected OpenBLAS: YES -- Detected LAPACK: YES

只有看到两个YES,才说明config.hpp里会定义ARMA_USE_BLAS和ARMA_USE_LAPACK。如果这里显示NO,先不要继续make,回到第2章检查OpenBLAS的安装和工具链文件配置。这里最容易犯的错是主机上装了BLAS/LAPACK的开发包,于是CMake“聪明”地找到了主机库,给出YES,然后等到链接用户程序时爆出一堆架构错误。所以配置的时候务必确认CMAKE_FIND_ROOT_PATH生效,别让CMake跑偏。

配置通过后,编译和安装:

make -j$(nproc) make install

安装完成后,建议打开安装目录下的include/armadillo_bits/config.hpp,确认这两行没有被注释:

#define ARMA_USE_BLAS #define ARMA_USE_LAPACK

这一步非常重要,因为“能编译通过”和“真正调用了BLAS/LAPACK”是两码事。很多交叉编译的坑都埋在这里,初次上手时一定要亲手确认。如果这两行被注释掉了,说明CMake没有找到ARM版本的库,后面所有的性能优化都是空谈。

3.3 交叉编译测试程序并在目标板上验证

编译完库,最后必须写一个简单的测试程序交叉编译,拷到板子上跑一遍,否则不能算完。这一步也是整个流程里最有成就感的环节。

测试程序test.cpp我写得尽量贴近实际使用场景:

#include <armadillo> #include <iostream> int main() { arma::mat A = arma::randu<arma::mat>(8, 8); arma::mat B = arma::randu<arma::mat>(8, 8); arma::mat C = A * B; arma::vec eigval; arma::mat eigvec; arma::eig_sym(eigval, eigvec, C + C.t()); std::cout << "eigval:" << eigval.t() << std::endl; return 0; }

这个程序同时覆盖了矩阵乘法、矩阵转置、对称特征值分解,如果链接阶段或运行时有问题,比较容易暴露出来。

交叉编译命令:

arm-linux-gnueabihf-g++ test.cpp \ -I/opt/arm/armadillo-deps/include \ -L/opt/arm/armadillo-deps/lib \ -o test_arm \ -larmadillo -lopenblas -lgfortran

编译完成后用file命令确认架构:

file test_arm

输出里应该包含ARM字样,而不是x86-64。之后把test_arm、libarmadillo.so、libopenblas.so一起拷贝到目标板,运行前设置一下动态库路径:

export LD_LIBRARY_PATH=/path/to/lib:$LD_LIBRARY_PATH ./test_arm

如果正常输出了特征值,说明整个链路已经通了。

如果目标板的存储空间比较小,或者你想彻底规避动态库运行时依赖问题,也可以考虑把armadillo和OpenBLAS都编译成静态库,然后静态链接用户程序。但注意,静态链接也不是零成本,OpenBLAS和Fortran运行库的静态链接触发的问题比动态库还多,尤其是libgfortran的依赖关系比较复杂,非必要不建议上来就静态。

4. 常见问题与排查技巧实录

4.1 链接阶段报relocation错误:多半是主机库混进来了

这个错误是交叉编译里最常见的:

relocation R_ARM_RELATIVE against `a local symbol' can not be used when making a shared object; recompile with -fPIC

出现这种情况,99%是因为CMake在找依赖库时找到了主机x86_64的libopenblas.so,然后把它的路径写进了链接参数。解决办法就是严格设置CMAKE_FIND_ROOT_PATH和MODE变量,只让工具链在/opt/arm/armadillo-deps里找库。另外还可以配置CMake的时候加-DCMAKE_VERBOSE_MAKEFILE=ON来打印完整链接命令,看-lopenblas展开后的实际路径是哪个,能很快确认链接了哪个目录下的库。这个排查思路不仅适用于armadillo,任何交叉编译项目里都通用。

4.2 CMake配置阶段报“无法编译测试程序”

armadillo的CMake在检测BLAS/LAPACK时会尝试编译一个小程序并运行它来判断链接是否生效。交叉编译环境下,这个测试程序是ARM架构的,在Ubuntu主机上无法运行,于是CMake就会报错。解决方法就是在工具链文件里加这一行:

set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

这一行的本质是告诉CMake:不用真的生成可执行文件去运行验证,只要链接这一步能过,就算检测通过。加了以后配置阶段就能顺利走完。经验之谈,这个设置不只是armadillo需要,几乎所有支持CMake的第三方库在交叉编译时都可能踩到这个坑。

4.3 动态库依赖问题:拷到板子上跑不起来的两种原因

测试程序编译好了,拷到目标板上运行,最常见的就是:

error while loading shared libraries: libarmadillo.so: cannot open shared object file: No such file or directory

这个简单,把相应的.so也拷过去,设置LD_LIBRARY_PATH就行。

还有一种情况是提示找不到libgfortran.so.5、libgomp.so.1这类运行时库,这是因为OpenBLAS和armadillo链接的程序依赖了交叉编译工具链自带的运行库。解决办法有两个:

  • 把目标板完整的sysroot同步到主机,编译时加--sysroot参数,让程序在运行时从根分区直接找库。
  • 直接把交叉编译器目录下的libgfortran.so.5、libgomp.so.1拷到板子的/lib或/usr/lib下。第二种方法更直接,但要注意拷贝时带上真实的符号链接,以及版本必须匹配板子上glibc和gcc的版本,这部分我建议在开始项目前就先和板子固件确认好,不要等跑不起来再去临时找。

4.4 常见问题速查表

整理一个速查表,方便遇到问题时快速定位:

问题现象可能原因解决办法
CMake检测不到OpenBLAS没有libopenblas.so无版本号链接手动建软链接,或显式指定OPENBLAS_LIBRARY
配置阶段报编译测试程序失败交叉编译可执行文件无法在本机运行工具链文件加CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY
链接报relocation错误CMake找到了主机x86_64的库检查CMAKE_FIND_ROOT_PATH和MODE配置
板子上运行提示找不到.so动态库路径没设置或没拷全设置LD_LIBRARY_PATH,补齐.so文件
运行直接段错误OpenBLAS的TARGET参数与板子架构不匹配重新用TARGET=ARMV7或具体型号编译OpenBLAS
armadillo矩阵运算特别慢config.hpp里ARMA_USE_BLAS被注释确认检测结果为YES,重新安装依赖后重编

4.5 实测中的几个小细节

最后分享几个我自己实操下来觉得值得提前注意的细节。

第一,armadillo的CMake默认会检测HDF5,发现之后会把它也加进依赖。交叉编译环境下HDF5本身又是另一个麻烦,除非你的项目真的需要,否则建议直接加-DDETECT_HDF5=OFF,省得引入一堆不必要的编译链。

第二,OpenBLAS编译参数里的TARGET不要随便填,填错了编译出的代码可能在板子上执行非法指令。如果不想纠结具体型号,TARGET=ARMV7是32位ARM Cortex-A系列比较通用的选择。有些教程为了省事直接用TARGET=ARM,但优化效果差很多,性能敏感的项目不建议这么干。

第三,软链接问题很容易被忽视,编译完OpenBLAS先检查一下lib目录下有没有不带版本号的.so文件,没有就手动加一个,省得后面CMake检测时找不到。这个坑我见过不止一次,包括我自己在内都在这上面浪费过时间。

另外一个经验是:交叉编译armadillo的整个过程,日志是很有价值的排查依据。建议所有编译步骤都开启完整输出(OpenBLAS的make可以加-j$(nproc)但保留终端输出,CMake可以加CMAKE_VERBOSE_MAKEFILE=ON),把报错信息完整保存下来再搜索,比自己瞎猜快得多。

我在实际过程中还发现,armadillo不同版本对CMake的检测输出有差异,如果安装的是比较新的12.x版本,检测OpenBLAS的逻辑更细,有时候会同时检查BLA_VENDOR和OpenBLAS的库名。遇到检测不到但又确实装好了的情况,可以在cmake命令里显式指定:

cmake .. -DOPENBLAS_FOUND=ON -DOPENBLAS_INCLUDE_DIR=/opt/arm/armadillo-deps/include -DOPENBLAS_LIBRARY=/opt/arm/armadillo-deps/lib/libopenblas.so

总而言之,交叉编译armadillo并不是一个有很高技术壁垒的事,但每一步的“确定你在链接哪个架构的库”都值得认真核对。多花两分钟用file命令检查生成物,后面就能少折腾一个小时。这套流程不止适用于armadillo,换成其他依赖BLAS/LAPACK的库,比如OpenCV或者Eigen,思路也是完全一样的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询