第19章 嵌入式Linux项目实战:交叉编译应用、链接第三方库、文件系统布局
嵌入式Linux开发,说白了就是「在PC上写代码,在板子上跑程序」。这中间的桥梁,就是交叉编译。我刚开始接触这块时,总觉得跟普通Linux开发差不多,结果第一次编译出来的程序,放到板子上直接报「无法执行」——嗯,那种尴尬我到现在还记得。
这一章,我们就来把交叉编译、第三方库链接、文件系统布局这三个核心问题彻底讲透。你想想看,这三个问题搞不定,你的嵌入式项目基本就卡在「能编译但跑不起来」的阶段。
19.1 交叉编译工具链的配置
交叉编译工具链,就是一套能在x86 PC上生成ARM(或其他架构)可执行文件的工具集。我个人习惯用Linaro提供的预编译工具链,省时省力。
核心概念:工具链的命名规则通常为「架构-厂商-操作系统-ABI」。比如 arm-linux-gnueabihf 表示ARM架构、Linux系统、使用硬浮点ABI。
在CMake中配置交叉编译,我们需要一个工具链文件。这是我项目中常用的配置:
# toolchain-arm-linux.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
# 指定交叉编译器路径
set(TOOLCHAIN_PATH /opt/gcc-arm-10.3-2021.07-x86_64-arm-linux-gnueabihf)
set(CMAKE_C_COMPILER ${TOOLCHAIN_PATH}/bin/arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PATH}/bin/arm-linux-gnueabihf-g++)
# 查找工具链中的其他工具
set(CMAKE_FIND_ROOT_PATH ${TOOLCHAIN_PATH}/arm-linux-gnueabihf/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_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
我的经验:CMAKE_FIND_ROOT_PATH_MODE 这三个参数非常关键。我曾经因为忘了设置,导致CMake找到了宿主机的libc库,编译出来的程序在板子上段错误不断。排查了整整一天才找到原因。
使用这个工具链文件时,只需要在构建时指定:
mkdir build && cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=../toolchain-arm-linux.cmake
make
19.2 链接第三方库的实战技巧
嵌入式项目中,第三方库的链接是个大坑。你想想看,PC上链接库只要 find_package 就完事了,但交叉编译时,你得确保找到的是ARM版本的库,而不是宿主机的x86库。
19.2.1 静态库 vs 动态库的选择
| 类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 静态库(.a) | 部署简单,无运行时依赖 | 可执行文件体积大 | 小型嵌入式设备、文件系统空间有限 |
| 动态库(.so) | 节省空间,方便更新 | 需要处理运行时链接路径 | 存储空间充足、需要模块化更新 |
我个人习惯:对于核心功能(比如加密库、协议栈),用静态库;对于UI、日志等非关键模块,用动态库。这样既保证了稳定性,又兼顾了灵活性。
19.2.2 使用ExternalProject_Add管理第三方库
当第三方库需要从源码交叉编译时,CMake的ExternalProject_Add模块非常好用。我在项目中用它来管理openssl和curl:
include(ExternalProject)
ExternalProject_Add(openssl
URL https://www.openssl.org/source/openssl-1.1.1k.tar.gz
CONFIGURE_COMMAND <SOURCE_DIR>/Configure
--prefix=${CMAKE_INSTALL_PREFIX}
--cross-compile-prefix=arm-linux-gnueabihf-
linux-armv4
BUILD_COMMAND make
INSTALL_COMMAND make install
)
ExternalProject_Add(libcurl
URL https://curl.se/download/curl-7.78.0.tar.gz
DEPENDS openssl
CONFIGURE_COMMAND <SOURCE_DIR>/configure
--host=arm-linux-gnueabihf
--with-ssl=${CMAKE_INSTALL_PREFIX}
--prefix=${CMAKE_INSTALL_PREFIX}
BUILD_COMMAND make
INSTALL_COMMAND make install
)
避坑指南:我曾经在配置openssl时,忘记指定 --cross-compile-prefix,结果编译出来的库是x86架构的。链接时没有任何报错,但运行时就崩溃。所以,交叉编译第三方库时,一定要确认生成的库文件确实是目标架构的。用 file 命令检查一下:file libcrypto.so.1.1。
19.2.3 使用find_library和find_path
对于已经预编译好的第三方库,我建议手动指定搜索路径,避免CMake的自动查找机制出错:
# 手动指定第三方库路径
set(THIRD_PARTY_PATH /opt/arm-libs)
# 查找头文件
find_path(OPENSSL_INCLUDE_DIR openssl/ssl.h
PATHS ${THIRD_PARTY_PATH}/include
NO_DEFAULT_PATH
)
# 查找库文件
find_library(OPENSSL_SSL_LIBRARY ssl
PATHS ${THIRD_PARTY_PATH}/lib
NO_DEFAULT_PATH
)
find_library(OPENSSL_CRYPTO_LIBRARY crypto
PATHS ${THIRD_PARTY_PATH}/lib
NO_DEFAULT_PATH
)
# 链接到目标
target_link_libraries(my_app
${OPENSSL_SSL_LIBRARY}
${OPENSSL_CRYPTO_LIBRARY}
)
target_include_directories(my_app PRIVATE
${OPENSSL_INCLUDE_DIR}
)
小技巧:加上 NO_DEFAULT_PATH 可以防止CMake去宿主机的标准路径下搜索。这样能确保找到的一定是ARM版本的库。
19.3 文件系统布局的设计
嵌入式Linux的文件系统布局,跟桌面Linux有很大区别。桌面系统有几百兆甚至几个G的空间,但嵌入式设备可能只有几十兆的flash。所以,布局要精打细算。
19.3.1 典型的嵌入式文件系统结构
这是我个人比较推荐的一种布局:
/
├── bin/ # 核心可执行文件(busybox、init等)
├── sbin/ # 系统管理工具
├── usr/
│ ├── bin/ # 用户应用程序
│ ├── lib/ # 动态库和内核模块
│ └── share/ # 只读数据文件
├── etc/ # 配置文件
├── lib/ # 核心库(libc、ld-linux等)
├── dev/ # 设备节点
├── proc/ # proc文件系统挂载点
├── sys/ # sysfs挂载点
├── tmp/ # 临时文件
├── var/ # 可变数据(日志、运行时状态)
└── mnt/ # 挂载点(U盘、SD卡等)
19.3.2 使用CMake安装规则
在CMake中,我们可以通过 install() 命令来定义文件系统的安装布局。这样,构建完成后只需要执行 make install DESTDIR=/path/to/rootfs 就能生成完整的文件系统:
# 安装可执行文件
install(TARGETS my_app
RUNTIME DESTINATION usr/bin
)
# 安装动态库
install(TARGETS my_lib
LIBRARY DESTINATION usr/lib
)
# 安装配置文件
install(FILES my_app.conf
DESTINATION etc
)
# 安装数据文件
install(DIRECTORY data/
DESTINATION usr/share/my_app
FILES_MATCHING PATTERN "*.json"
PATTERN ".git" EXCLUDE
)
关键点:使用 DESTDIR 而不是直接指定绝对路径。这样可以让CMake的安装路径与目标文件系统的路径解耦。我见过有人直接在 install() 里写 /usr/bin,结果每次测试都要手动清理,非常麻烦。
19.3.3 运行时库路径的处理
动态库的运行时查找路径,是嵌入式开发中另一个容易出问题的地方。我建议用以下两种方式之一:
- 设置RPATH:在编译时嵌入库搜索路径
- 使用ldconfig:在系统启动时配置库路径
在CMake中设置RPATH的方法:
# 设置运行时库搜索路径
set_target_properties(my_app PROPERTIES
INSTALL_RPATH "\$ORIGIN/../lib"
BUILD_WITH_INSTALL_RPATH TRUE
)
# 或者使用CMAKE_INSTALL_RPATH全局设置
set(CMAKE_INSTALL_RPATH "/usr/lib:/usr/local/lib")
我的建议:使用 $ORIGIN 相对路径的方式,这样可执行文件可以放在任意位置,只要保持与lib目录的相对关系即可。这在调试阶段特别方便,不用每次都修改ldconfig配置。
19.4 完整的实战示例
下面是一个完整的CMakeLists.txt,整合了交叉编译、第三方库链接和安装布局:
cmake_minimum_required(VERSION 3.16)
project(EmbeddedApp VERSION 1.0.0 LANGUAGES C CXX)
# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 第三方库路径(由外部传入)
set(THIRD_PARTY_PATH /opt/arm-libs CACHE PATH "Third party libraries path")
# 查找openssl
find_library(OPENSSL_SSL_LIBRARY ssl
PATHS ${THIRD_PARTY_PATH}/lib
NO_DEFAULT_PATH
)
find_library(OPENSSL_CRYPTO_LIBRARY crypto
PATHS ${THIRD_PARTY_PATH}/lib
NO_DEFAULT_PATH
)
find_path(OPENSSL_INCLUDE_DIR openssl/ssl.h
PATHS ${THIRD_PARTY_PATH}/include
NO_DEFAULT_PATH
)
# 查找sqlite3
find_library(SQLITE3_LIBRARY sqlite3
PATHS ${THIRD_PARTY_PATH}/lib
NO_DEFAULT_PATH
)
find_path(SQLITE3_INCLUDE_DIR sqlite3.h
PATHS ${THIRD_PARTY_PATH}/include
NO_DEFAULT_PATH
)
# 主应用
add_executable(my_app
src/main.cpp
src/network.cpp
src/database.cpp
)
target_include_directories(my_app PRIVATE
${OPENSSL_INCLUDE_DIR}
${SQLITE3_INCLUDE_DIR}
include
)
target_link_libraries(my_app
${OPENSSL_SSL_LIBRARY}
${OPENSSL_CRYPTO_LIBRARY}
${SQLITE3_LIBRARY}
pthread
)
# 设置运行时RPATH
set_target_properties(my_app PROPERTIES
INSTALL_RPATH "\$ORIGIN/../lib"
BUILD_WITH_INSTALL_RPATH TRUE
)
# 安装规则
install(TARGETS my_app
RUNTIME DESTINATION usr/bin
)
install(FILES config/app.conf
DESTINATION etc/my_app
)
install(DIRECTORY data/
DESTINATION usr/share/my_app
FILES_MATCHING PATTERN "*.json"
)
19.5 构建与部署流程
整个流程可以概括为以下步骤:
- 准备工具链:下载并解压交叉编译工具链
- 编译第三方库:使用ExternalProject_Add或手动编译ARM版本的库
- 配置项目:指定工具链文件和第三方库路径
- 编译:执行make生成目标文件
- 安装到rootfs:使用make install DESTDIR=...生成文件系统
- 打包:使用tar或mksquashfs制作文件系统镜像
- 烧录到设备:通过uboot或fastboot烧录
核心要点:交叉编译的本质就是「在A平台上生成B平台的可执行文件」。只要工具链正确、库路径正确、文件系统布局合理,剩下的就是自动化构建的事情了。我个人习惯写一个build.sh脚本,把上述步骤全部自动化,这样每次构建只需要执行一个命令。
这张图展示了整个流程的核心脉络。从工具链配置到第三方库管理,再到文件系统布局,最后生成可部署的产物。每一步都环环相扣,缺一不可。
最后提醒:交叉编译环境搭建好后,一定要先编译一个简单的hello world测试程序,放到板子上跑一下。确认工具链没问题了,再开始编译复杂的项目。这个习惯帮我省了无数次排查时间。
公众号:蓝海资料掘金营,微信deep3321