[C/C++]程序编译和交叉编译
从源码到可执行文件,C/C++ 的编译过程涉及多个阶段和工具链。本文梳理编译流程、GCC/G++ 的使用、CMake 构建系统,以及交叉编译的配置方式。
1. C/C++ 编译流程概览
1.1 工具链组成
C/C++ 编译涉及以下工具,合称工具链(Toolchain):
| 组件 | 作用 |
|---|---|
| 预处理器(cpp) | 处理 #include、#define、条件编译指令 |
| 编译器(cc1 / cc1plus) | 将预处理后的代码翻译为汇编代码 |
| 汇编器(as) | 将汇编代码转换为机器码(目标文件) |
| 链接器(ld) | 将目标文件和库链接为可执行文件 |
除此之外,GNU Binutils 还提供 ar(归档)、objdump(反汇编)、readelf(分析 ELF)、strip(剥离符号)等辅助工具。
1.2 四阶段编译流程
以 gcc 为例,从源码到可执行文件经过四个阶段:
1 | 源文件 (.c/.cpp) |
分步命令演示:
1 | 1. 预处理:展开所有宏和头文件 |
日常使用中不需要分步执行,一条 gcc main.c -o main 即可完成全部流程,但理解每个阶段有助于定位编译问题。
1.3 中间产物总结
| 阶段 | 输入 | 输出 | 产物说明 |
|---|---|---|---|
| 预处理 | .c/.cpp |
.i/.ii |
纯 C/C++ 文本,无预处理指令 |
| 编译 | .i/.ii |
.s |
目标平台的汇编代码 |
| 汇编 | .s |
.o/.obj |
可重定位目标文件(ELF/COFF) |
| 链接 | .o + 库 |
无后缀/.exe |
最终可执行文件 |
2. GCC 与 G++ 的区别及安装
2.1 核心差异
GCC(GNU Compiler Collection)是一个编译器集合。gcc 和 g++ 都是它的前端驱动程序,本质上调用同一套后端,但有以下关键区别:
| 维度 | gcc | g++ |
|---|---|---|
| 语言判断 | 根据后缀 .c→C,.cpp→C++ |
一律按 C++ 处理 |
| 默认链接库 | 不自动链接 libstdc++ |
自动链接 libstdc++ |
| 异常/RTTI | C 模式下不启用 | 默认启用 |
| 适用场景 | 编译 C 代码、内核模块 | 编译 C++ 代码、混合工程 |
实际影响:用 gcc 编译 C++ 代码时,必须手动加 -lstdc++,否则链接阶段会报 undefined reference 错误。因此编译 C++ 代码直接用 g++ 最可靠;编译 C 代码用 gcc 足够。
2.2 Ubuntu 安装
安装默认版本(系统源提供的稳定版):
1 | sudo apt update |
build-essential 是一个元包,会安装 gcc、g++、make、libc-dev 等基础开发工具。
安装指定版本:
1 | 查看可用版本 |
多版本切换:使用 update-alternatives 管理:
1 | 注册 gcc 版本 |
版本与 C++ 标准对照:
| GCC 版本 | C++ 标准支持 |
|---|---|
| GCC 4.8+ | C++11 完整支持 |
| GCC 5.0+ | C++14 |
| GCC 7.0+ | C++17 |
| GCC 10.0+ | C++20 大部分 |
| GCC 13.0+ | C++23 大部分 |
3. 使用 GCC/G++ 命令行进行本地编译
3.1 单文件编译
1 | 最简形式 |
常用编译选项速查:
| 选项 | 含义 |
|---|---|
-std=c++17 |
指定 C++ 标准(c++11/c++14/c++17/c++20) |
-O0 / -O2 / -O3 |
优化级别:调试 / 推荐 / 激进 |
-g |
生成调试符号 |
-Wall -Wextra |
启用常见警告 |
-Werror |
警告视为错误 |
-DNAME=VALUE |
定义预处理宏 |
-I/path/to/include |
添加头文件搜索路径 |
-L/path/to/lib |
添加库文件搜索路径 |
-lname |
链接 libname.so 或 libname.a |
-fPIC |
生成位置无关代码(用于动态库) |
3.2 多文件编译
假设工程结构:
1 | project/ |
方法一:一步编译
1 | g++ main.cpp math.cpp -o main |
适合文件少的快速验证。每次改动一个文件也会全量编译,不适合工程化。
方法二:分离编译再链接
1 | 分别编译为目标文件 |
每次只重编译修改过的文件,链接阶段不变。这正是 Makefile / CMake 等构建系统的核心思想。
3.3 静态库与动态库
静态库(.a):编译时链接进可执行文件,体积大但无运行时依赖。
1 | 创建 |
动态库(.so):运行时动态加载,体积小,可被多个程序共享。
1 | 创建(注意 -fPIC) |
-Wl,-rpath 将动态库的搜索路径硬编码到可执行文件中,是生产环境的首选做法。
4. 使用 CMake 进行本地编译
4.1 为什么需要构建系统
当工程规模增长,手动编写 Makefile 或敲命令行变得低效且不可移植。CMake 是一款跨平台的构建系统生成器:它读取 CMakeLists.txt,生成目标平台的构建文件(Linux 的 Makefile、Windows 的 VS 工程等)。
4.2 最小 CMakeLists.txt
1 | cmake_minimum_required(VERSION 3.16) |
构建流程(推荐 out-of-source 构建,不污染源码目录):
1 | 在源码目录外生成构建文件 |
常用 cmake 命令选项:
1 | 指定编译类型 |
4.3 多文件工程
1 | cmake_minimum_required(VERSION 3.16) |
STATIC编译为静态库,SHARED编译为动态库PUBLIC/PRIVATE/INTERFACE控制头文件路径和依赖的传递范围
4.4 find_package 查找机制
find_package 是 CMake 引入外部依赖的核心指令。以 OpenSSL 为例:
1 | find_package(OpenSSL REQUIRED) |
CMake 在以下位置查找配置文件:
CMAKE_PREFIX_PATH环境变量- 系统默认路径(
/usr、/usr/local等) - 包自身的
*-config.cmake或Find*.cmake模块
未放默认路径的库可通过 -DCMAKE_PREFIX_PATH 或 -D<Package>_DIR 指定。
5. 交叉编译的概念
5.1 为什么需要交叉编译
目标平台的 CPU 架构或操作系统与开发机不同,且目标平台往往没有足够资源运行编译器:
- ARM 嵌入式设备:树莓派、Jetson、手机 SoC——内存和算力远不如 x86_64 PC
- 物联网设备:MIPS / RISC-V 等非主流架构
- 内核/驱动开发:目标系统还没有完整用户态环境
在性能更强的 x86_64 主机上编译 ARM 程序,就是典型的交叉编译场景。
5.2 三个核心概念
| 概念 | 含义 | 示例 |
|---|---|---|
| Build(构建平台) | 执行编译的机器 | x86_64 Ubuntu 开发机 |
| Host(主机平台) | 运行编译产物的机器 | aarch64 嵌入式板卡 |
| Target(目标平台) | 编译器本身面向的平台 | 通常与 Host 相同(编译器场景除外) |
常见的交叉编译场景中,Build 和 Host 不同,Target 与 Host 相同。
5.3 工具链命名规范
交叉编译工具链通常遵循 GNU 三元组/四元组命名:
1 | arch-vendor-kernel-system |
| 字段 | 含义 | 示例 |
|---|---|---|
| arch | CPU 架构 | aarch64、arm、x86_64 |
| vendor | 厂商 | linaro、poky、unknown |
| kernel | 内核/系统 | linux、elf |
| system | ABI/库 | gnu、gnueabi、gnueabihf、musl |
示例:
aarch64-linux-gnu-— 64 位 ARM,GNU libcarm-linux-gnueabihf-— 32 位 ARM,GNU libc,硬浮点 ABIx86_64-linux-gnu-— 64 位 x86,GNU libc(即本地工具链)
5.4 工具链组成
一套完整的交叉编译工具链包含:
| 组件 | 文件名示例 |
|---|---|
| 交叉编译器(C) | aarch64-linux-gnu-gcc |
| 交叉编译器(C++) | aarch64-linux-gnu-g++ |
| 交叉汇编器 | aarch64-linux-gnu-as |
| 交叉链接器 | aarch64-linux-gnu-ld |
| 其他 Binutils | aarch64-linux-gnu-objdump、aarch64-linux-gnu-strip 等 |
| Sysroot | 目标平台的根文件系统(头文件 + 动态库) |
6. 直接调用交叉编译工具链进行编译
6.1 命令行编译
交叉编译工具链安装后,直接指定编译器即可。以 Linaro aarch64 工具链为例:
1 | 下载 Linaro 工具链 |
通过环境变量指定:
1 | export CC=aarch64-linux-gnu-gcc |
6.2 Sysroot 与链接
交叉编译链接动态库时,编译器需要知道目标平台的库在哪里。通过 --sysroot 指定:
1 | aarch64-linux-gnu-g++ \ |
Sysroot 即目标平台的根文件系统镜像,其目录结构为:
1 | /path/to/aarch64-rootfs/ |
6.3 静态链接 vs 动态链接
| 方式 | 优点 | 缺点 |
|---|---|---|
静态链接(-static) |
无运行时依赖,拷贝即运行 | 体积大,无法共享公共库 |
| 动态链接 | 体积小,随系统更新 | 需要目标平台有匹配的 .so |
交叉编译时推荐静态链接以简化部署,避免目标平台库版本不匹配。在编译器命令中加 -static 即可。
1 | aarch64-linux-gnu-g++ -static main.cpp -o main_aarch64 |
7. 使用 CMake 进行交叉编译配置
7.1 两种配置方式
CMake 支持两种方式指定交叉编译工具链:
| 方式 | 适用场景 |
|---|---|
命令行传参:-DCMAKE_C_COMPILER=... |
快速验证、一次性构建 |
Toolchain File:-DCMAKE_TOOLCHAIN_FILE=... |
工程化管理、可复用 |
命令行传参适合临时测试,Toolchain File 是正式工程的标准做法。
7.2 命令行传参方式
1 | cmake -B build_aarch64 \ |
CMAKE_SYSTEM_NAME和CMAKE_SYSTEM_PROCESSOR告知 CMake 目标平台身份CMAKE_FIND_ROOT_PATH限制find_package/find_library的搜索范围,避免混入主机库
7.3 Toolchain File 方式(推荐)
创建 aarch64-linux-gnu.toolchain.cmake:
1 | # 目标系统信息 |
NEVER:始终在主机路径中查找(如构建时需要的代码生成器)ONLY:仅在 sysroot 中查找(如目标平台的库和头文件)
使用 Toolchain File 构建:
1 | cmake -B build_aarch64 \ |
7.4 常见问题
1. find_package 找不到库
交叉编译时 CMake 默认在主机路径中搜索。解决方案:
- 正确设置
CMAKE_FIND_ROOT_PATH和CMAKE_FIND_ROOT_PATH_MODE_* - 或通过
CMAKE_PREFIX_PATH指向目标平台的安装路径
2. pkg-config 路径问题
交叉编译时需要用目标平台的 pkg-config。对于 aarch64 平台:
1 | export PKG_CONFIG_PATH=/path/to/aarch64-sysroot/usr/lib/aarch64-linux-gnu/pkgconfig |
也可以在 toolchain file 中通过 set(ENV{PKG_CONFIG_PATH} ...) 指定。
3. CMake Try Compile 阶段报错
CMake 在配置阶段会先尝试编译一个测试程序来检测编译器能力。测试程序按目标平台的架构编译,但如果在 x86_64 主机上直接运行,会报 Exec format error。这属于正常警告,不影响编译产物。若需避免,可设置:
1 | set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) |
此时 CMake 仅检查编译而不尝试运行。