综合实战:从零搭建一个完整的交叉编译项目
好了,终于到了我们这门课的重头戏。前面讲了那么多工具链文件怎么写、CMakeLists.txt怎么配、构建脚本怎么组织,今天咱们就把这些东西串起来,真正从零开始搭一个完整的交叉编译项目。
说实话,我最早学交叉编译的时候,最头疼的就是「看别人的教程都能跑通,自己一动手就各种报错」。后来我发现,问题往往出在「只抄了代码,没理解结构」。所以今天这个实战,我会带着你一步步把每个文件都写出来,并且告诉你为什么这么写。
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")
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.h和utils.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.h和math.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. 知识体系总览
为了让你对整个流程有个全局认识,我画了一张图:
这张图把整个流程分成了四个阶段:输入(工具链、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」字样,说明交叉编译成功了。
build.sh里忘记加-DCMAKE_TOOLCHAIN_FILE参数了。cmake默认会使用本机编译器,所以一定要在配置阶段显式指定工具链文件。
9. 总结
这个实战项目虽然简单,但麻雀虽小五脏俱全。你掌握了这个结构,以后不管是做ARM Linux、RISC-V还是MIPS的交叉编译,换汤不换药——改工具链文件、改编译器前缀、改架构标志,其他部分基本可以复用。
我个人建议你把这个项目当成一个「模板」保存起来。以后接到新的交叉编译需求,直接复制一份,改改工具链路径和架构参数就能用。省时省力,还不会漏掉关键配置。
好了,今天的实战就到这里。代码都在上面了,建议你亲手敲一遍。遇到问题不要慌,对照着流程图一步步排查,总能找到原因。