综合实战:从零搭建一个完整的交叉编译项目

好了,终于到了我们这门课的重头戏。前面讲了那么多工具链文件怎么写、CMakeLists.txt怎么配、构建脚本怎么组织,今天咱们就把这些东西串起来,真正从零开始搭一个完整的交叉编译项目。

说实话,我最早学交叉编译的时候,最头疼的就是「看别人的教程都能跑通,自己一动手就各种报错」。后来我发现,问题往往出在「只抄了代码,没理解结构」。所以今天这个实战,我会带着你一步步把每个文件都写出来,并且告诉你为什么这么写。

项目目标:为一个基于ARM Cortex-A7的嵌入式Linux目标板,构建一个包含可执行程序、静态库、动态库的完整项目,并实现交叉编译。

1. 项目结构设计

我个人习惯,一个交叉编译项目至少要有四个层次:

  • 顶层目录:放CMakeLists.txt、构建脚本、README
  • cmake/:放工具链文件和自定义CMake模块
  • src/:源代码
  • build/:构建产物(通常不提交到版本控制)

来看看我们今天的项目长什么样:

cross_compile_demo/
├── CMakeLists.txt              # 顶层构建文件
├── build.sh                    # 一键构建脚本
├── cmake/
│   ├── arm-linux-gnueabihf.cmake  # 工具链文件
│   └── FindMyUtils.cmake          # 自定义查找模块
├── src/
│   ├── CMakeLists.txt
│   ├── main.c
│   ├── utils/
│   │   ├── CMakeLists.txt
│   │   ├── utils.h
│   │   └── utils.c
│   └── math/
│       ├── CMakeLists.txt
│       ├── math.h
│       └── math.c
└── output/                     # 最终产物输出目录

嗯,这里要注意:output/目录是我故意加上的。为什么?因为很多新手把编译产物直接扔在源码目录里,结果交叉编译出来的ARM版可执行文件和本地x86版混在一起,分都分不清。我建议你养成习惯,单独指定一个输出目录。

2. 工具链文件:arm-linux-gnueabihf.cmake

工具链文件是整个交叉编译的「身份证」。它告诉CMake:你是谁?你从哪里来?要到哪里去?

# cmake/arm-linux-gnueabihf.cmake
# 用于ARM Cortex-A7目标板的交叉编译工具链

# 设置目标系统
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

# 指定交叉编译器路径
# 我这里假设工具链安装在 /opt/arm-gcc/ 下
set(TOOLCHAIN_PREFIX /opt/arm-gcc/bin/arm-linux-gnueabihf)

set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++)

# 指定sysroot——说白了就是目标板的根文件系统
set(CMAKE_SYSROOT /opt/arm-gcc/arm-linux-gnueabihf/sysroot)

# 查找目标系统上的程序
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})

# 搜索策略:只在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)

# 设置编译器标志
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=armv7-a -mfpu=neon -mfloat-abi=hard")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=armv7-a -mfpu=neon -mfloat-abi=hard")
我的经验:CMAKE_FIND_ROOT_PATH_MODE的四个选项,我建议你一开始就全部写清楚。我曾经因为漏写了MODE_PACKAGE,结果find_package一直从本机系统找库,编译出来的程序链接了x86的库,放到ARM板上直接段错误。

3. 顶层CMakeLists.txt

顶层CMakeLists.txt是整个项目的「总指挥」。它不负责具体编译,而是把各个子模块组织起来。

# CMakeLists.txt (顶层)
cmake_minimum_required(VERSION 3.16)
project(CrossCompileDemo VERSION 1.0.0 LANGUAGES C CXX)

# 设置C标准
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)

# 设置输出路径
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/output/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/output/lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/output/lib)

# 添加自定义模块路径
list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)

# 添加子目录
add_subdirectory(src)

你可能会问:为什么要把输出路径单独设到output/?说白了,就是为了「干净」。构建的时候所有生成的文件都在output/里,源码目录里只有.c.h,用git管理起来特别清爽。

4. 源码目录的CMakeLists.txt

接下来是src/CMakeLists.txt,它负责把各个模块拼起来:

# src/CMakeLists.txt
add_subdirectory(utils)
add_subdirectory(math)

# 主程序
add_executable(main main.c)

# 链接库
target_link_libraries(main
    PRIVATE
        utils
        math
)

然后是src/utils/CMakeLists.txt——我们做一个静态库:

# src/utils/CMakeLists.txt
add_library(utils STATIC
    utils.c
)

target_include_directories(utils
    PUBLIC
        ${CMAKE_CURRENT_SOURCE_DIR}
)

再来看src/math/CMakeLists.txt——这次做一个动态库:

# src/math/CMakeLists.txt
add_library(math SHARED
    math.c
)

target_include_directories(math
    PUBLIC
        ${CMAKE_CURRENT_SOURCE_DIR}
)

# 设置动态库版本
set_target_properties(math PROPERTIES
    VERSION 1.0.0
    SOVERSION 1
)
注意:静态库和动态库的链接方式不同。静态库在编译时直接嵌入到可执行文件中,动态库则在运行时加载。交叉编译时,如果你用了动态库,记得把.so文件也拷贝到目标板的/lib/usr/lib下。

5. 源代码示例

为了完整性,我给出简单的源码。首先是utils.hutils.c

// src/utils/utils.h
#ifndef UTILS_H
#define UTILS_H

void print_platform_info(void);

#endif

// src/utils/utils.c
#include <stdio.h>
#include "utils.h"

void print_platform_info(void) {
    #ifdef __arm__
        printf("Running on ARM architecture\n");
    #elif defined(__x86_64__)
        printf("Running on x86_64 architecture\n");
    #else
        printf("Running on unknown architecture\n");
    #endif
}

然后是math.hmath.c

// src/math/math.h
#ifndef MATH_H
#define MATH_H

int add(int a, int b);
int multiply(int a, int b);

#endif

// src/math/math.c
#include "math.h"

int add(int a, int b) {
    return a + b;
}

int multiply(int a, int b) {
    return a * b;
}

最后是main.c

// src/main.c
#include <stdio.h>
#include "utils.h"
#include "math.h"

int main() {
    printf("Cross Compile Demo v1.0\n");
    
    print_platform_info();
    
    int a = 10, b = 20;
    printf("add(%d, %d) = %d\n", a, b, add(a, b));
    printf("multiply(%d, %d) = %d\n", a, b, multiply(a, b));
    
    return 0;
}

6. 构建脚本:build.sh

有了工具链文件和CMakeLists.txt,还差最后一步——构建脚本。我习惯写一个build.sh,把cmake配置、编译、安装全部自动化:

#!/bin/bash
# build.sh - 一键交叉编译脚本

set -e  # 出错就停,别硬跑

# 项目根目录
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
BUILD_DIR="${PROJECT_DIR}/build"
OUTPUT_DIR="${PROJECT_DIR}/output"

# 清理旧构建
echo "Cleaning old build..."
rm -rf "${BUILD_DIR}" "${OUTPUT_DIR}"
mkdir -p "${BUILD_DIR}" "${OUTPUT_DIR}"

# 配置:指定工具链文件
echo "Configuring with CMake..."
cmake -S "${PROJECT_DIR}" \
      -B "${BUILD_DIR}" \
      -DCMAKE_TOOLCHAIN_FILE="${PROJECT_DIR}/cmake/arm-linux-gnueabihf.cmake" \
      -DCMAKE_BUILD_TYPE=Release \
      -DCMAKE_INSTALL_PREFIX="${OUTPUT_DIR}"

# 编译
echo "Building..."
cmake --build "${BUILD_DIR}" --parallel $(nproc)

# 安装到output目录
echo "Installing..."
cmake --install "${BUILD_DIR}"

echo "Done! Output files are in: ${OUTPUT_DIR}"
一个小技巧:我在脚本里用了set -e。这样如果cmake配置失败或者编译报错,脚本会立刻退出,不会继续往下执行。你想想看,如果编译失败了还继续执行install,装了一堆半成品到output里,排查问题的时候多闹心。

7. 知识体系总览

为了让你对整个流程有个全局认识,我画了一张图:

交叉编译项目完整流程 工具链文件 arm-linux-gnueabihf.cmake CMakeLists.txt 顶层 + 子目录 源代码 main.c / utils.c / math.c 构建脚本 build.sh cmake配置 → 编译 → 安装 CMake 配置阶段 加载工具链 → 检测编译器 → 生成构建系统 输出产物 output/bin/main | output/lib/libutils.a | output/lib/libmath.so

这张图把整个流程分成了四个阶段:输入(工具链、CMakeLists、源码)→ 构建脚本驱动 → CMake配置编译 → 输出产物。你照着这个流程走,基本不会出错。

8. 使用方式

一切准备就绪,怎么用呢?很简单:

# 1. 给构建脚本加执行权限
chmod +x build.sh

# 2. 运行构建脚本
./build.sh

# 3. 查看输出
ls -la output/bin/
ls -la output/lib/

如果一切顺利,你会看到output/bin/main是一个ARM架构的可执行文件。可以用file命令验证:

$ file output/bin/main
output/bin/main: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, ...

看到「ARM」字样,说明交叉编译成功了。

避坑指南:我曾经遇到过一个问题——工具链路径写对了,cmake配置也通过了,但编译出来的可执行文件还是x86的。排查了半天,发现是build.sh里忘记加-DCMAKE_TOOLCHAIN_FILE参数了。cmake默认会使用本机编译器,所以一定要在配置阶段显式指定工具链文件。

9. 总结

这个实战项目虽然简单,但麻雀虽小五脏俱全。你掌握了这个结构,以后不管是做ARM Linux、RISC-V还是MIPS的交叉编译,换汤不换药——改工具链文件、改编译器前缀、改架构标志,其他部分基本可以复用。

我个人建议你把这个项目当成一个「模板」保存起来。以后接到新的交叉编译需求,直接复制一份,改改工具链路径和架构参数就能用。省时省力,还不会漏掉关键配置。

好了,今天的实战就到这里。代码都在上面了,建议你亲手敲一遍。遇到问题不要慌,对照着流程图一步步排查,总能找到原因。


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