[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
2
3
4
5
6
7
8
9
10
11
12
13
源文件 (.c/.cpp)
│ 预处理 (gcc -E) ── 展开宏、包含头文件

预处理文件 (.i/.ii)
│ 编译 (gcc -S) ── 生成汇编代码

汇编文件 (.s)
│ 汇编 (gcc -c) ── 翻译为机器码

目标文件 (.o)
│ 链接 (gcc/ld) ── 链接库文件、解析符号

可执行文件 (ELF/PE)

分步命令演示

1
2
3
4
5
6
7
8
9
10
11
# 1. 预处理:展开所有宏和头文件
gcc -E main.c -o main.i

# 2. 编译:生成汇编代码
gcc -S main.i -o main.s

# 3. 汇编:生成目标文件
gcc -c main.s -o main.o

# 4. 链接:生成可执行文件
gcc main.o -o main

日常使用中不需要分步执行,一条 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)是一个编译器集合。gccg++ 都是它的前端驱动程序,本质上调用同一套后端,但有以下关键区别:

维度 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
2
sudo apt update
sudo apt install build-essential gcc g++

build-essential 是一个元包,会安装 gccg++makelibc-dev 等基础开发工具。

安装指定版本

1
2
3
4
5
# 查看可用版本
apt search "^gcc-[0-9]+$" | grep gcc

# 安装 GCC 13 和 G++ 13
sudo apt install gcc-13 g++-13

多版本切换:使用 update-alternatives 管理:

1
2
3
4
5
6
7
8
9
10
11
# 注册 gcc 版本
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 110
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 130

# 注册 g++ 版本
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-11 110
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 130

# 交互式选择
sudo update-alternatives --config gcc
sudo update-alternatives --config g++

版本与 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
2
3
4
5
# 最简形式
g++ main.cpp -o main

# 指定标准和优化
g++ -std=c++17 -O2 -g -Wall -Wextra main.cpp -o main

常用编译选项速查

选项 含义
-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.solibname.a
-fPIC 生成位置无关代码(用于动态库)

3.2 多文件编译

假设工程结构:

1
2
3
4
project/
├── main.cpp
├── math.cpp
└── math.h

方法一:一步编译

1
g++ main.cpp math.cpp -o main

适合文件少的快速验证。每次改动一个文件也会全量编译,不适合工程化。

方法二:分离编译再链接

1
2
3
4
5
6
# 分别编译为目标文件
g++ -c main.cpp -o main.o
g++ -c math.cpp -o math.o

# 链接
g++ main.o math.o -o main

每次只重编译修改过的文件,链接阶段不变。这正是 Makefile / CMake 等构建系统的核心思想。

3.3 静态库与动态库

静态库(.a):编译时链接进可执行文件,体积大但无运行时依赖。

1
2
3
4
5
6
# 创建
g++ -c math.cpp -o math.o
ar rcs libmath.a math.o

# 使用
g++ main.cpp -L. -lmath -o main

动态库(.so):运行时动态加载,体积小,可被多个程序共享。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 创建(注意 -fPIC)
g++ -c -fPIC math.cpp -o math.o
g++ -shared math.o -o libmath.so

# 使用
g++ main.cpp -L. -lmath -o main

# 运行时需要能找到 .so
# 方法 1:设置 LD_LIBRARY_PATH
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH

# 方法 2:编译时设置 RPATH
g++ main.cpp -L. -lmath -Wl,-rpath='$ORIGIN' -o main

-Wl,-rpath 将动态库的搜索路径硬编码到可执行文件中,是生产环境的首选做法。

4. 使用 CMake 进行本地编译

4.1 为什么需要构建系统

当工程规模增长,手动编写 Makefile 或敲命令行变得低效且不可移植。CMake 是一款跨平台的构建系统生成器:它读取 CMakeLists.txt,生成目标平台的构建文件(Linux 的 Makefile、Windows 的 VS 工程等)。

4.2 最小 CMakeLists.txt

1
2
3
4
5
6
7
cmake_minimum_required(VERSION 3.16)
project(HelloWorld VERSION 1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(hello main.cpp)

构建流程(推荐 out-of-source 构建,不污染源码目录):

1
2
3
4
5
6
7
8
# 在源码目录外生成构建文件
cmake -B build

# 编译
cmake --build build

# 运行
./build/hello

常用 cmake 命令选项

1
2
3
4
5
6
7
8
# 指定编译类型
cmake -B build -DCMAKE_BUILD_TYPE=Release

# 指定安装路径
cmake -B build -DCMAKE_INSTALL_PREFIX=/usr/local/myapp

# 指定编译器
cmake -B build -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++

4.3 多文件工程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cmake_minimum_required(VERSION 3.16)
project(MathApp VERSION 1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

# 创建库
add_library(math STATIC math.cpp)

# 指定头文件搜索路径(库的使用者自动继承)
target_include_directories(math PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

# 创建可执行文件
add_executable(main main.cpp)

# 链接库
target_link_libraries(main PRIVATE math)
  • STATIC 编译为静态库,SHARED 编译为动态库
  • PUBLIC/PRIVATE/INTERFACE 控制头文件路径和依赖的传递范围

4.4 find_package 查找机制

find_package 是 CMake 引入外部依赖的核心指令。以 OpenSSL 为例:

1
2
find_package(OpenSSL REQUIRED)
target_link_libraries(myapp PRIVATE OpenSSL::SSL OpenSSL::Crypto)

CMake 在以下位置查找配置文件:

  1. CMAKE_PREFIX_PATH 环境变量
  2. 系统默认路径(/usr/usr/local 等)
  3. 包自身的 *-config.cmakeFind*.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 架构 aarch64armx86_64
vendor 厂商 linaropokyunknown
kernel 内核/系统 linuxelf
system ABI/库 gnugnueabignueabihfmusl

示例:

  • aarch64-linux-gnu- — 64 位 ARM,GNU libc
  • arm-linux-gnueabihf- — 32 位 ARM,GNU libc,硬浮点 ABI
  • x86_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-objdumpaarch64-linux-gnu-strip
Sysroot 目标平台的根文件系统(头文件 + 动态库)

6. 直接调用交叉编译工具链进行编译

6.1 命令行编译

交叉编译工具链安装后,直接指定编译器即可。以 Linaro aarch64 工具链为例:

1
2
3
4
5
6
7
8
9
# 下载 Linaro 工具链
wget https://releases.linaro.org/components/toolchain/binaries/latest-7/aarch64-linux-gnu/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz
tar xf gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz

# 设置 PATH
export PATH=$HOME/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin:$PATH

# 编译
aarch64-linux-gnu-g++ -std=c++17 main.cpp -o main_aarch64

通过环境变量指定

1
2
3
4
export CC=aarch64-linux-gnu-gcc
export CXX=aarch64-linux-gnu-g++

$CXX -std=c++17 main.cpp -o main_aarch64

6.2 Sysroot 与链接

交叉编译链接动态库时,编译器需要知道目标平台的库在哪里。通过 --sysroot 指定:

1
2
3
4
aarch64-linux-gnu-g++ \
--sysroot=/path/to/aarch64-rootfs \
main.cpp \
-o main_aarch64

Sysroot 即目标平台的根文件系统镜像,其目录结构为:

1
2
3
4
5
6
/path/to/aarch64-rootfs/
├── usr/
│ ├── include/ # 目标平台头文件
│ └── lib/ # 目标平台动态/静态库
├── lib/ # 系统库
└── ...

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
2
3
4
5
6
cmake -B build_aarch64 \
-DCMAKE_SYSTEM_NAME=Linux \
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
-DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
-DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ \
-DCMAKE_FIND_ROOT_PATH=/path/to/aarch64-sysroot
  • CMAKE_SYSTEM_NAMECMAKE_SYSTEM_PROCESSOR 告知 CMake 目标平台身份
  • CMAKE_FIND_ROOT_PATH 限制 find_package / find_library 的搜索范围,避免混入主机库

7.3 Toolchain File 方式(推荐)

创建 aarch64-linux-gnu.toolchain.cmake

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 目标系统信息
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)

# 交叉编译器路径
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)

# Sysroot(目标平台根文件系统)
set(CMAKE_SYSROOT /path/to/aarch64-sysroot)

# find_package / find_library 仅在 sysroot 中查找
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) # 构建工具用本地
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) # 库文件只查 sysroot
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) # 头文件只查 sysroot
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) # find_package 只查 sysroot
  • NEVER:始终在主机路径中查找(如构建时需要的代码生成器)
  • ONLY:仅在 sysroot 中查找(如目标平台的库和头文件)

使用 Toolchain File 构建

1
2
3
4
5
cmake -B build_aarch64 \
-DCMAKE_TOOLCHAIN_FILE=./aarch64-linux-gnu.toolchain.cmake \
-DCMAKE_BUILD_TYPE=Release

cmake --build build_aarch64

7.4 常见问题

1. find_package 找不到库

交叉编译时 CMake 默认在主机路径中搜索。解决方案:

  • 正确设置 CMAKE_FIND_ROOT_PATHCMAKE_FIND_ROOT_PATH_MODE_*
  • 或通过 CMAKE_PREFIX_PATH 指向目标平台的安装路径

2. pkg-config 路径问题

交叉编译时需要用目标平台的 pkg-config。对于 aarch64 平台:

1
2
export PKG_CONFIG_PATH=/path/to/aarch64-sysroot/usr/lib/aarch64-linux-gnu/pkgconfig
export PKG_CONFIG_SYSROOT_DIR=/path/to/aarch64-sysroot

也可以在 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 仅检查编译而不尝试运行。

8. 参考资源

文档

工具链下载

相关阅读