第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 运行时库路径的处理

动态库的运行时查找路径,是嵌入式开发中另一个容易出问题的地方。我建议用以下两种方式之一:

  1. 设置RPATH:在编译时嵌入库搜索路径
  2. 使用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 构建与部署流程

整个流程可以概括为以下步骤:

  1. 准备工具链:下载并解压交叉编译工具链
  2. 编译第三方库:使用ExternalProject_Add或手动编译ARM版本的库
  3. 配置项目:指定工具链文件和第三方库路径
  4. 编译:执行make生成目标文件
  5. 安装到rootfs:使用make install DESTDIR=...生成文件系统
  6. 打包:使用tar或mksquashfs制作文件系统镜像
  7. 烧录到设备:通过uboot或fastboot烧录

核心要点:交叉编译的本质就是「在A平台上生成B平台的可执行文件」。只要工具链正确、库路径正确、文件系统布局合理,剩下的就是自动化构建的事情了。我个人习惯写一个build.sh脚本,把上述步骤全部自动化,这样每次构建只需要执行一个命令。

嵌入式Linux交叉编译项目流程 交叉编译工具链 arm-linux-gnueabihf-gcc CMAKE_TOOLCHAIN_FILE 第三方库管理 ExternalProject_Add find_library + NO_DEFAULT_PATH 文件系统布局 install() + DESTDIR INSTALL_RPATH CMake构建过程 cmake -DCMAKE_TOOLCHAIN_FILE=... → make → make install DESTDIR=... ARM可执行文件 my_app (ARM架构) ARM动态库 libssl.so, libsqlite3.so 文件系统目录 rootfs/usr/bin, etc... 部署到嵌入式设备

这张图展示了整个流程的核心脉络。从工具链配置到第三方库管理,再到文件系统布局,最后生成可部署的产物。每一步都环环相扣,缺一不可。

最后提醒:交叉编译环境搭建好后,一定要先编译一个简单的hello world测试程序,放到板子上跑一下。确认工具链没问题了,再开始编译复杂的项目。这个习惯帮我省了无数次排查时间。


公众号:蓝海资料掘金营,微信deep3321