CMake交叉编译

1. CMake 基础概念

1.1 CMake 工作流程

CMake 构建过程分为两个主要阶段:

配置阶段 (Configure)

1
cmake .. -DBUILD_32BIT=ON -DTARGET_TYPE=exe

配置阶段的作用:

  • 读取并解析 CMakeLists.txt 文件

  • 检查编译器、库和依赖项

  • 生成构建文件(Makefile、Ninja文件等)

  • 处理 -D 参数,设置缓存变量

  • 创建 CMakeCache.txt 缓存文件

构建阶段 (Build)

1
2
3
make
# 或者
cmake --build .

构建阶段的作用:

  • 执行实际的编译过程

  • 调用编译器和链接器

  • 生成最终的可执行文件或库

1.2 project() 命令详解

project() 的执行内容

1
project(packer)  # 这一行会触发大量的内部操作

project() 执行的主要任务:

  1. 编译器检测和验证

  2. 语言支持初始化

  3. 工具链配置

  4. 平台特定设置

  5. 内置变量设置

  6. 缓存变量创建

编译器检测过程

1
2
3
4
5
6
7
8
9
# CMake 会创建临时测试文件
# /build/CMakeFiles/CMakeScratch/TryCompile-xxxxx/testCCompiler.c
int main(void) { return 0; }

# 然后执行编译测试
${CMAKE_C_COMPILER} -o testCCompiler testCCompiler.c

# 如果成功,设置 CMAKE_C_COMPILER_WORKS = 1
# 如果失败,报错并停止配置

为什么编译器设置要在 project() 之前

1
2
3
4
5
6
7
# ❌ 错误的顺序
project(packer) # 这里已经检测编译器了
set(CMAKE_C_COMPILER /path/to/clang) # 太晚了,已经检测完了

# ✅ 正确的顺序
set(CMAKE_C_COMPILER /path/to/clang) # 先设置编译器
project(packer) # 再检测编译器

跳过编译器测试

1
2
3
set(CMAKE_C_COMPILER_WORKS 1)      # 告诉 CMake:C 编译器能正常工作,跳过测试
set(CMAKE_CXX_COMPILER_WORKS 1) # 告诉 CMake:C++ 编译器能正常工作,跳过测试
project(packer) # 不再执行编译器测试

适用场景:

  • 交叉编译环境

  • 编译器测试失败但编译器本身正常

  • 自定义工具链

  • 避免链接器配置问题

1.3 编译和链接过程

编译链接流程图

1
源代码(.cpp) → [编译器] → 目标文件(.o/.obj) → [链接器] → 可执行文件(.exe)

编译阶段 (Compilation):

  • 处理单个源文件

  • 进行语法检查和语义分析

  • 优化代码并生成机器码

  • 对于未定义的函数,编译器会”留坑”等待链接时填补
    链接阶段 (Linking):

  • 合并多个目标文件

  • 解析符号引用,”填坑”操作

  • 链接静态库和动态库

  • 生成最终的可执行文件

1.4 CMake 版本管理

版本要求

1
cmake_minimum_required(VERSION 3.15)

版本说明:

  • 推荐使用 CMake 3.15 以上版本

  • 版本号比较:3.15 > 3.5(数字大小比较,不是字符串)

  • 检查当前版本cmake --version


2. CMake 变量与配置

2.1 内置变量

常用内置变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 路径相关变量
CMAKE_SOURCE_DIR # 顶层源码目录
CMAKE_BINARY_DIR # 顶层构建目录(执行cmake命令的目录)
CMAKE_CURRENT_SOURCE_DIR # 当前CMakeLists.txt所在目录
CMAKE_CURRENT_BINARY_DIR # 当前构建目录

# 构建相关变量
CMAKE_BUILD_TYPE # 构建类型:Debug/Release/RelWithDebInfo/MinSizeRel
CMAKE_CXX_COMPILER # C++编译器路径
CMAKE_C_COMPILER # C编译器路径
CMAKE_LINKER # 链接器路径

# 系统相关变量
CMAKE_SYSTEM_NAME # 目标系统名称(Windows/Linux/Darwin等)
CMAKE_SYSTEM_PROCESSOR # 目标处理器架构(x86_64/i686/arm等)
CMAKE_HOST_SYSTEM_NAME # 主机系统名称

CMAKE_CURRENT_* 变量详解

1
2
3
4
5
6
7
# 在主项目 CMakeLists.txt 中
CMAKE_CURRENT_SOURCE_DIR # /path/to/packer
CMAKE_CURRENT_BINARY_DIR # /path/to/packer/build

# 在子项目 libs/lzma/CMakeLists.txt 中
CMAKE_CURRENT_SOURCE_DIR # /path/to/packer/libs/lzma
CMAKE_CURRENT_BINARY_DIR # /path/to/packer/build/libs/lzma

重要说明:

  • CMAKE_CURRENT_SOURCE_DIR 总是指向当前 CMakeLists.txt 所在的目录

  • 在主项目中 = 主项目目录

  • 在子项目中 = 子项目目录

  • 这是 CMake 的上下文相关变量,会随着当前处理的 CMakeLists.txt 而变化

编译器相关变量

1
2
3
4
CMAKE_CXX_COMPILER_ID     # 编译器标识(GNU/Clang/MSVC等)
CMAKE_CXX_COMPILER_VERSION # 编译器版本
CMAKE_CXX_STANDARD # C++标准版本
CMAKE_CXX_STANDARD_REQUIRED # 是否严格要求C++标准

2.2 变量作用域和传递机制

变量作用域类型

全局变量(Global Variables):

1
2
3
4
5
6
# 主项目中设置
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_BUILD_TYPE "Debug")
set(CMAKE_SYSTEM_NAME Windows)

add_subdirectory(libs/lzma) # 子项目会继承这些设置

自动传递的变量:

  • CMAKE_CXX_STANDARD - C++ 标准

  • CMAKE_BUILD_TYPE - 构建类型

  • CMAKE_SYSTEM_NAME - 目标系统

  • CMAKE_CXX_COMPILER - C++ 编译器

  • CMAKE_SYSROOT - 系统根目录

不会自动传递的变量:

  • 自定义变量(但子项目可以访问)

  • 目标特定的配置(target_* 命令)

变量传递的时机问题

1
2
3
4
5
6
7
8
cmake_minimum_required(VERSION 3.15)
project(packer)
add_subdirectory(libs/lzma) # ← 这里立即执行子项目

# 下面这些变量设置在子项目执行之后
set(CMAKE_CXX_STANDARD 17) # 子项目已经执行完了
set(ARCH "x64") # 子项目已经执行完了
set(MINGW_TRIPLE "x86_64-w64-mingw32") # 子项目已经执行完了

正确的顺序:

1
2
3
4
5
6
7
8
9
10
cmake_minimum_required(VERSION 3.15)
project(packer)

# 先设置变量
set(CMAKE_CXX_STANDARD 17)
set(ARCH "x64")
set(MINGW_TRIPLE "x86_64-w64-mingw32")

# 再添加子项目
add_subdirectory(libs/lzma) # 现在子项目能看到这些变量

2.3 用户自定义变量

option 变量(布尔类型)

1
2
3
4
5
6
7
8
9
10
# 定义布尔选项
option(BUILD_32BIT "Build 32-bit version" OFF)
option(ENABLE_TESTS "Enable unit tests" ON)
option(USE_STATIC_LIBS "Use static libraries" OFF)

# 使用方式
if(BUILD_32BIT)
message(STATUS "Building 32-bit version")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -m32")
endif()

set 变量(通用类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
# 字符串变量
set(TARGET_TYPE "exe" CACHE STRING "Target type: exe or dll")
set_property(CACHE TARGET_TYPE PROPERTY STRINGS exe dll) # 提供选项列表

# 路径变量
set(CUSTOM_INCLUDE_DIR "/usr/local/include" CACHE PATH "Custom include directory")

# 列表变量
set(SOURCE_FILES
main.cpp
utils.cpp
config.cpp
)

变量作用域

1
2
3
4
5
6
# 局部变量(当前作用域)
set(LOCAL_VAR "value")
# 缓存变量(全局持久化)
set(CACHE_VAR "value" CACHE STRING "Description")
# 环境变量
set(ENV{PATH} "/new/path:$ENV{PATH}")

2.4 命令行变量传递

-D 参数使用

1
2
3
4
5
6
7
8
9
10
11
# 设置布尔变量
cmake .. -DBUILD_32BIT=ON -DENABLE_TESTS=OFF
# 设置字符串变量
cmake .. -DTARGET_TYPE=dll -DCMAKE_BUILD_TYPE=Release
# 设置路径变量
cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local -DCUSTOM_LIB_DIR=/opt/libs
# 组合使用
cmake .. -DBUILD_32BIT=ON -DTARGET_TYPE=exe -DCMAKE_BUILD_TYPE=Debug
# 如果使用clangd,那么在构建时需要通过这个生成compile_commands.json,否则clangd会报很多语法错,比如找不到这个文件,找不到这个函数,导致无法跳转函数定义
cmake .. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON

重要说明:

  • -D 参数只在配置阶段cmake ..)有效

  • 构建阶段make)不需要也不能使用 -D 参数

  • 变量会保存在 CMakeCache.txt 中,下次配置时会自动加载

2.5 生成器表达式

基本语法

1
2
3
4
5
6
7
8
# 条件表达式语法
$<condition:true_value>
$<condition:true_value:false_value>
# 示例:根据构建类型设置编译选项
target_compile_options(myapp PRIVATE
$<$<CONFIG:Debug>:-g -O0>
$<$<CONFIG:Release>:-O3 -DNDEBUG>
)

实用生成器表达式

1
2
3
4
5
6
7
8
9
10
11
12
# 字符串比较
$<$<STREQUAL:${SUBSYSTEM_TYPE},gui>:-Wl,--subsystem,windows>
$<$<STREQUAL:${SUBSYSTEM_TYPE},cui>:-Wl,--subsystem,console>
# 布尔值判断
$<$<BOOL:${BUILD_32BIT}>:-m32>
$<$<NOT:$<BOOL:${USE_SHARED_LIBS}>>:-static>
# 编译器判断
$<$<CXX_COMPILER_ID:GNU>:-fPIC>
$<$<CXX_COMPILER_ID:Clang>:-fcolor-diagnostics>
# 平台判断
$<$<PLATFORM_ID:Windows>:-DWIN32>
$<$<PLATFORM_ID:Linux>:-DLINUX>

等价的 if 语句写法

1
2
3
4
5
6
7
8
# 生成器表达式方式
target_link_options(${TARGET_NAME} PRIVATE
$<$<STREQUAL:${SUBSYSTEM_TYPE},gui>:-Wl,--subsystem,windows>
)
# 等价的if语句方式
if(SUBSYSTEM_TYPE STREQUAL "gui")
target_link_options(${TARGET_NAME} PRIVATE -Wl,--subsystem,windows)
endif()

3. CMake 中的 PRIVATE/PUBLIC 可见性详解

3.1 概述

CMake 中的 PRIVATEPUBLICINTERFACE 关键字控制的是依赖传递性,而不是简单的可见性或 GUI 显示。它们决定了在项目依赖关系中,哪些配置会自动传递给依赖者。

3.2 三种可见性类型

3.2.1 PRIVATE

  • 作用范围:只对当前目标可见

  • 传递性:不会传递给依赖当前目标的其他目标

  • 使用场景:内部实现细节、私有头文件、内部依赖

3.2.2 PUBLIC

  • 作用范围:对当前目标和依赖者都可见

  • 传递性:会自动传递给依赖当前目标的其他目标

  • 使用场景:公共接口、用户需要的头文件

3.2.3 INTERFACE

  • 作用范围:只对依赖者可见,当前目标不使用

  • 传递性:只传递给依赖者

  • 使用场景:header-only 库、纯接口定义

3.3 实际应用示例

3.3.1 库的配置示例

1
2
3
4
5
6
7
8
9
10
11
12
# lzma 库的 CMakeLists.txt
add_library(lzma STATIC ${LZMA_SOURCES})
# 公共接口 - 用户需要的头文件
target_include_directories(lzma PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include/wrapper
)
# 内部实现 - 用户不需要的头文件
target_include_directories(lzma PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/include/easylzma
${CMAKE_CURRENT_SOURCE_DIR}/include/pavlov
)

3.3.2 使用库的项目配置

1
2
3
4
5
6
7
8
9
10
# packer 项目的 CMakeLists.txt
add_subdirectory(libs/lzma) # 添加 lzma 作为子项目
add_executable(packer ${PACKER_SOURCES})
# 只需要配置自己的头文件路径
target_include_directories(packer PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
# 不需要手动添加 lzma 的头文件路径
)
# 链接库 - 自动获得 lzma 的 PUBLIC 头文件路径
target_link_libraries(packer PRIVATE lzma)

3.4 编译命令的实际差异

3.4.1 使用 PRIVATE 时

1
2
3
4
# 编译 lzma 库
clang++ -I./include -I./include/wrapper -c src/file.cpp
# 编译 packer(需要手动添加路径)
clang++ -I./include -I./libs/lzma/include/wrapper -c src/main.cpp

3.4.2 使用 PUBLIC 时

1
2
3
4
5
# 编译 lzma 库
clang++ -I./include -I./include/wrapper -c src/file.cpp
# 编译 packer(自动获得 lzma 的 PUBLIC 路径)
clang++ -I./include -I./libs/lzma/include/wrapper -c src/main.cpp
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 自动添加的

3.5 重要限制:仅在项目依赖时有效

3.5.1 有效场景:项目内依赖

1
2
3
# 主项目中
add_subdirectory(libs/lzma) # lzma 作为子项目
target_link_libraries(packer PRIVATE lzma) # 链接到 CMake 目标
  • PRIVATE/PUBLIC 有效

  • PUBLIC 的配置会自动传递

  • 实现自动依赖管理

3.5.2 无效场景:独立编译

1
2
3
4
# 先独立编译 lzma → 生成 liblzma.a
# 然后在 packer 中:
target_link_libraries(packer PRIVATE /path/to/liblzma.a) # 直接链接文件
target_include_directories(packer PRIVATE /path/to/lzma/include/) # 必须手动添加
  • PRIVATE/PUBLIC 无效

  • 必须手动管理所有头文件路径

  • CMake 不知道 .a 文件对应的头文件位置

3.6 判断原则

3.6.1 使用 PRIVATE 当:

  • 头文件是内部实现细节

  • 用户不需要直接包含这些头文件

  • 第三方库的内部依赖

3.6.2 使用 PUBLIC 当:

  • 头文件是库的公共接口

  • 用户使用库时需要包含这些头文件

  • 库的公共 API 定义

3.6.3 使用 INTERFACE 当:

  • 创建 header-only 库

  • 纯接口定义

  • 库本身不需要,但使用者需要

3.7 最佳实践

  1. 明确区分公共接口和内部实现

  2. 公共接口使用 PUBLIC,内部实现使用 PRIVATE

  3. 在项目依赖场景下,充分利用自动传递特性

  4. 独立编译场景下,手动管理依赖关系

  5. 使用 VERBOSE=1 验证实际的编译命令

3.8 验证方法

1
2
3
4
# 查看实际的编译命令
make VERBOSE=1
# 检查 PRIVATE/PUBLIC 的传递效果
cmake --build . --verbose

通过观察编译命令中的 -I 参数,可以验证头文件路径的传递是否符合预期。

4. 项目结构与目标管理

4.1 add_subdirectory() 的添加时机

在 CMake 配置阶段执行

1
2
3
4
5
6
7
8
9
cmake_minimum_required(VERSION 3.15)
project(packer)

# 这里立即执行,不是在构建时
add_subdirectory(libs/lzma) # CMake 立即进入 libs/lzma 处理其 CMakeLists.txt
add_subdirectory(libs/common) # CMake 立即进入 libs/common 处理其 CMakeLists.txt

# 继续处理主项目的其余配置
add_executable(packer src/main.cpp)

位置要求

1
2
3
4
5
6
# ✅ 正确:先添加子项目,再使用其目标
add_subdirectory(libs/lzma)
add_subdirectory(libs/common)

add_executable(packer src/main.cpp)
target_link_libraries(packer PRIVATE lzma common) # 可以使用 lzma 和 common
1
2
3
4
5
6
# ❌ 错误:先使用目标,再添加子项目
add_executable(packer src/main.cpp)
target_link_libraries(packer PRIVATE lzma common) # 错误!lzma 和 common 还不存在

add_subdirectory(libs/lzma) # 太晚了
add_subdirectory(libs/common) # 太晚了

4.2 add_executable() 的添加时机

基本作用

1
add_executable(packer src/main.cpp src/compressor.cpp)

add_executable() 的作用:

  • 创建一个可执行文件目标

  • 注册目标到 CMake 的内部目标系统

  • 为后续的 target_* 命令做准备

  • 不会立即编译,只是定义目标

位置要求

1
2
3
4
5
6
7
8
# ✅ 正确:先定义目标,再配置目标
add_executable(packer src/main.cpp)
target_link_libraries(packer PRIVATE lzma) # 可以使用 packer
target_compile_options(packer PRIVATE -Wall) # 可以使用 packer

# ❌ 错误:先使用目标,再定义目标
target_link_libraries(packer PRIVATE lzma) # 错误!packer 还不存在
add_executable(packer src/main.cpp) # 太晚了

4.3 推荐的项目结构

标准的组织顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
cmake_minimum_required(VERSION 3.15)

# 1. 编译器设置(在 project 之前)
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_CXX_COMPILER /path/to/clang++)

project(packer)

# 2. 全局变量和设置
set(CMAKE_CXX_STANDARD 17)
set(ARCH "x64")
set(MINGW_TRIPLE "x86_64-w64-mingw32")

# 3. 添加子项目(定义依赖的目标)
add_subdirectory(libs/common)
add_subdirectory(libs/lzma)

# 4. 定义源文件
set(PACKER_SOURCES
src/main.cpp
src/compressor.cpp
)

# 5. 创建主目标
add_executable(packer ${PACKER_SOURCES})

# 6. 配置主目标
target_include_directories(packer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(packer PRIVATE lzma common kernel32 user32)
target_compile_options(packer PRIVATE -Wall -Wextra)

# 7. 条件配置
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(packer PRIVATE _DEBUG)
endif()

# 8. 目标属性设置
set_target_properties(packer PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
)

4.4 引用子项目的正确操作

推荐的目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
project_root/
├── CMakeLists.txt # 主项目配置
├── src/
│ ├── main.cpp
│ └── compressor.cpp
├── include/
│ └── compressor.h
├── libs/ # 子项目目录
│ ├── lzma/
│ │ ├── CMakeLists.txt # lzma 子项目配置
│ │ ├── include/
│ │ │ ├── wrapper/ # 公共接口
│ │ │ │ └── easylzmawrapper.h
│ │ │ ├── easylzma/ # 内部实现
│ │ │ └── pavlov/ # 内部实现
│ │ └── src/
│ │ ├── wrapper/
│ │ ├── pavlov/
│ │ └── ...
│ └── common/ # 另一个子项目
│ ├── CMakeLists.txt
│ ├── include/
│ └── src/
└── build/ # 构建目录

子项目 CMakeLists.txt 编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# libs/lzma/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(lzma)

# 基本设置
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 源文件列表
set(LZMA_SOURCES
src/wrapper/easylzmawrapper.cpp
src/compress.c
src/decompress.c
# ... 其他源文件
)

# 创建静态库
add_library(lzma STATIC ${LZMA_SOURCES})

# 公共接口 - 用户需要的头文件(会自动传递给依赖者)
target_include_directories(lzma PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include/wrapper
)

# 内部实现 - 只有库内部使用(不会传递给依赖者)
target_include_directories(lzma PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/include/easylzma
${CMAKE_CURRENT_SOURCE_DIR}/include/pavlov
)

# 编译选项
target_compile_options(lzma PRIVATE
-Wall
-Wextra
)

# 配置相关的编译选项
target_compile_options(lzma PRIVATE
$<$<CONFIG:Debug>:-O0 -g>
$<$<CONFIG:Release>:-O2>
)

主项目 CMakeLists.txt 编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# CMakeLists.txt (主项目)
cmake_minimum_required(VERSION 3.15)

# 编译器设置(必须在 project() 之前)
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_CXX_COMPILER /root/OLLVM_17/build/bin/clang++)
set(CMAKE_C_COMPILER /root/OLLVM_17/build/bin/clang)

project(packer)

# 基本设置
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_SKIP_RPATH TRUE)

# 全局变量设置(在 add_subdirectory 之前)
set(ARCH "x64")
set(MINGW_TRIPLE "x86_64-w64-mingw32")

if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Debug")
endif()

# 添加子项目(变量已经设置好)
add_subdirectory(libs/lzma)

# 主项目配置
set(PACKER_SOURCES
src/main.cpp
src/compressor.cpp
)

add_executable(packer ${PACKER_SOURCES})

# 包含目录(只需要自己的,子项目的 PUBLIC 路径会自动获得)
target_include_directories(packer PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
# 不需要手动添加子项目的头文件路径
)

# 链接库(自动获得子项目的 PUBLIC 配置)
target_link_libraries(packer PRIVATE
lzma # 来自子项目
kernel32 # 系统库
user32
)

# 编译和链接选项
target_compile_options(packer PRIVATE
-m64
--target=${MINGW_TRIPLE}
-Wall
-Wextra
)

target_link_options(packer PRIVATE
-m64
--target=${MINGW_TRIPLE}
)

5. 交叉编译环境搭建

5.1 MinGW-w64 工具链安装

Ubuntu 系统安装

1
2
3
4
5
6
7
8
# 更新包管理器
sudo apt update
# 安装32位工具链
sudo apt install gcc-mingw-w64-i686 g++-mingw-w64-i686
# 安装64位工具链
sudo apt install gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64
# 安装完整工具链(推荐)
sudo apt install mingw-w64 mingw-w64-tools gcc-mingw-w64 g++-mingw-w64

CentOS/RHEL 系统安装

1
2
3
4
5
# 启用EPEL仓库
sudo yum install epel-release
# 安装MinGW工具链
sudo yum install mingw32-gcc mingw32-gcc-c++
sudo yum install mingw64-gcc mingw64-gcc-c++

5.2 工具链验证

验证安装

1
2
3
4
5
6
7
8
9
10
# 验证32位工具链
i686-w64-mingw32-gcc --version
i686-w64-mingw32-g++ --version
i686-w64-mingw32-ar --version
i686-w64-mingw32-windres --version
# 验证64位工具链
x86_64-w64-mingw32-gcc --version
x86_64-w64-mingw32-g++ --version
x86_64-w64-mingw32-ar --version
x86_64-w64-mingw32-windres --version

查找工具链位置

1
2
3
4
5
6
# 查找编译器位置
which i686-w64-mingw32-gcc
which x86_64-w64-mingw32-gcc
# 查找工具链目录
find /usr -name "*mingw*" -type d 2>/dev/null
ls -la /usr/bin/*mingw*

5.3 简单编译测试

创建测试程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// hello.cpp
#include <iostream>
using namespace std;
int main() {
cout << "Hello from MinGW cross-compilation!" << endl;
#ifdef _WIN32
cout << "Target: Windows" << endl;
#endif
#ifdef _WIN64
cout << "Architecture: 64-bit" << endl;
#else
cout << "Architecture: 32-bit" << endl;
#endif
return 0;
}

编译测试

1
2
3
4
5
6
# 编译32位Windows程序
i686-w64-mingw32-g++ -o hello32.exe hello.cpp
# 编译64位Windows程序
x86_64-w64-mingw32-g++ -o hello64.exe hello.cpp
# 静态链接编译
x86_64-w64-mingw32-g++ -static -o hello_static.exe hello.cpp

5.4 目标三元组理解

三元组格式

1
架构-厂商-系统-ABI

常见三元组

1
2
3
4
x86_64-w64-mingw32    # 64位Windows
i686-w64-mingw32 # 32位Windows
x86_64-linux-gnu # 64位Linux
arm-linux-gnueabihf # ARM Linux

重要说明:

  • mingw32 不表示32位,而是指 mingw 环境和 Win32 API

  • w64 是 mingw-w64 项目的标识

  • 架构由前缀区分x86_64(64位)vs i686(32位)


6. 交叉编译配置

6.1 CMake 工具链文件

32位 Windows 工具链文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# mingw32.cmake
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR i686)
# 指定编译器
set(CMAKE_C_COMPILER i686-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER i686-w64-mingw32-g++)
set(CMAKE_RC_COMPILER i686-w64-mingw32-windres)
set(CMAKE_AR i686-w64-mingw32-ar)
set(CMAKE_RANLIB i686-w64-mingw32-ranlib)
# 设置查找路径
set(CMAKE_FIND_ROOT_PATH /usr/i686-w64-mingw32)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

64位 Windows 工具链文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# mingw64.cmake
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
# 指定编译器
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
set(CMAKE_AR x86_64-w64-mingw32-ar)
set(CMAKE_RANLIB x86_64-w64-mingw32-ranlib)
# 设置查找路径
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

6.2 使用工具链文件

配置命令

1
2
3
4
5
6
# 使用32位工具链
cmake -DCMAKE_TOOLCHAIN_FILE=mingw32.cmake ..
# 使用64位工具链
cmake -DCMAKE_TOOLCHAIN_FILE=mingw64.cmake ..
# 结合其他选项
cmake -DCMAKE_TOOLCHAIN_FILE=mingw64.cmake -DCMAKE_BUILD_TYPE=Release ..

6.3 直接设置编译器

简单设置方式

1
2
3
4
# CMakeLists.txt 中直接设置
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)

命令行设置

1
2
3
4
5
# 通过命令行参数设置
cmake .. \
-DCMAKE_SYSTEM_NAME=Windows \
-DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc \
-DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++

6.4 架构特定配置

条件编译配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 根据架构设置不同的配置
if(ARCH STREQUAL "x86")
# 32位配置
set(CMAKE_C_COMPILER i686-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER i686-w64-mingw32-g++)
target_include_directories(${TARGET_NAME} PRIVATE
/usr/i686-w64-mingw32/include
)
target_compile_options(${TARGET_NAME} PRIVATE
--target=i686-pc-windows-gnu
--sysroot=/usr/i686-w64-mingw32
-m32
)
else()
# 64位配置
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
target_include_directories(${TARGET_NAME} PRIVATE
/usr/x86_64-w64-mingw32/include
)
target_compile_options(${TARGET_NAME} PRIVATE
--target=x86_64-pc-windows-gnu
--sysroot=/usr/x86_64-w64-mingw32
-m64
)
endif()

6.5 自动宏定义

编译器自动定义的宏

1
2
3
4
5
6
7
8
9
// 32位编译时自动定义
WIN32 // Windows平台
**i386** // x86架构
_WIN32 // Win32 API
// 64位编译时自动定义
WIN32 // Windows平台(仍然定义)
_WIN64 // 64位Windows
__x86_64__ // x86_64架构
_WIN32 // Win32 API(仍然定义)

在代码中使用

1
2
3
4
5
6
7
8
9
10
#ifdef _WIN32
// Windows特定代码
#ifdef _WIN64
// 64位Windows特定代码
cout << "Windows 64-bit" << endl;
#else
// 32位Windows特定代码
cout << "Windows 32-bit" << endl;
#endif
#endif

7. 编译器与链接器选项

7.1 编译器选项分类

警告控制选项 (-W系列)

1
2
3
4
5
6
7
8
target_compile_options(${TARGET_NAME} PRIVATE
-Wall # 启用大部分常见警告
-Wextra # 启用额外警告
-Wno-unknown-pragmas # 禁用"未知pragma指令"警告
-Wno-unknown-attributes # 禁用"未知属性"警告
-Wno-microsoft-annotations # 禁用"微软注解"警告
-Wno-unused-parameter # 禁用"未使用参数"警告
)

功能控制选项 (-f系列)

1
2
3
4
5
6
7
8
9
target_compile_options(${TARGET_NAME} PRIVATE
-fno-exceptions # 禁用C++异常处理
-fno-rtti # 禁用运行时类型信息
-fno-stack-protector # 禁用栈保护
-ffunction-sections # 每个函数放入独立段
-fdata-sections # 每个数据放入独立段
-fno-lto # 禁用链接时优化
-fPIC # 生成位置无关代码
)

优化选项 (-O系列)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Debug模式
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_options(${TARGET_NAME} PRIVATE
-O0 # 不优化
-g # 生成调试信息
-DDEBUG # 定义DEBUG宏
)
endif()
# Release模式
if(CMAKE_BUILD_TYPE STREQUAL "Release")
target_compile_options(${TARGET_NAME} PRIVATE
-O3 # 最高优化级别
-DNDEBUG # 定义NDEBUG宏
-g0 # 不生成调试信息
-fomit-frame-pointer # 省略帧指针
)
endif()
# 大小优化
if(CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
target_compile_options(${TARGET_NAME} PRIVATE
-Os # 优化大小
-DNDEBUG
)
endif()

架构相关选项

1
2
3
4
5
6
7
# 32位编译
target_compile_options(${TARGET_NAME} PRIVATE -m32)
# 64位编译
target_compile_options(${TARGET_NAME} PRIVATE -m64)
# 指定CPU架构
target_compile_options(${TARGET_NAME} PRIVATE -march=native)
target_compile_options(${TARGET_NAME} PRIVATE -mtune=generic)

7.2 链接器选项

基本链接选项

1
2
3
4
5
target_link_options(${TARGET_NAME} PRIVATE
-static # 静态链接所有库
-static-libgcc # 静态链接libgcc
-static-libstdc++ # 静态链接libstdc++
)

Windows 特定链接选项

1
2
3
4
5
6
target_link_options(${TARGET_NAME} PRIVATE
-Wl,--subsystem,console # 控制台程序
-Wl,--subsystem,windows # 窗口程序
-Wl,--entry,main # 指定入口点
-Wl,--out-implib,libname.a # 生成导入库
)

优化链接选项

1
2
3
4
5
6
7
target_link_options(${TARGET_NAME} PRIVATE
-Wl,--gc-sections # 删除未使用的段
-Wl,--strip-all # 去除所有符号
-Wl,--strip-debug # 去除调试符号
-Wl,--allow-multiple-definition # 允许重复定义
-s # 去除符号表
)

7.3 -Wl 参数详解

-Wl 的含义

  • -Wl = “传递给链接器”的前缀

  • 逗号分隔多个链接器参数

  • 编译器会将 -Wl, 后的参数直接传递给链接器

示例用法

1
2
3
4
5
6
7
8
9
10
11
12
# 单个链接器参数
target_link_options(${TARGET_NAME} PRIVATE -Wl,--gc-sections)
# 多个链接器参数
target_link_options(${TARGET_NAME} PRIVATE
-Wl,--gc-sections,--strip-all,--subsystem,console
)
# 等价写法
target_link_options(${TARGET_NAME} PRIVATE
-Wl,--gc-sections
-Wl,--strip-all
-Wl,--subsystem,console
)

7.4 链接器选择

在Linux下交叉编译生成Windows可执行文件时:
lld-link: 生成PE/COFF格式(Windows可执行文件) ld.lld: 生成ELF格式(Linux可执行文件)

1
2
3
4
5
6
7
8
9
10
11
# 使用 lld-link 链接器生成Windows程序
set(CMAKE_LINKER lld-link)
set(CMAKE_C_LINK_EXECUTABLE
"lld-link <FLAGS> <CMAKE_C_LINK_FLAGS> <LINK_FLAGS> <OBJECTS> /OUT:<TARGET> <LINK_LIBRARIES>"
)
set(CMAKE_CXX_LINK_EXECUTABLE
"lld-link <FLAGS> <CMAKE_CXX_LINK_FLAGS> <LINK_FLAGS> <OBJECTS> /OUT:<TARGET> <LINK_LIBRARIES>"
)
# 设置目标架构
set(CMAKE_C_COMPILER_TARGET x86_64-pc-windows-msvc)
set(CMAKE_CXX_COMPILER_TARGET x86_64-pc-windows-msvc)

7.5 库和启动文件控制

控制标准库链接

1
2
3
4
5
6
7
8
9
10
11
12
target_compile_options(${TARGET_NAME} PRIVATE
-nostdlib # 不链接标准库
-nostartfiles # 不链接启动文件
-nodefaultlibs # 不链接默认库
)
# 手动链接需要的库
target_link_libraries(${TARGET_NAME} PRIVATE
gcc # GCC运行时库
mingw32 # MinGW运行时库
kernel32 # Windows内核库
user32 # Windows用户界面库
)

8. 实际项目配置

8.1 完整的 CMakeLists.txt 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
cmake_minimum_required(VERSION 3.15)
project(CrossCompileExample VERSION 1.0.0)
# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 定义选项
option(BUILD_32BIT "Build 32-bit version" OFF)
set(TARGET_TYPE "exe" CACHE STRING "Target type: exe or dll")
set_property(CACHE TARGET_TYPE PROPERTY STRINGS exe dll)
set(SUBSYSTEM_TYPE "cui" CACHE STRING "Subsystem type: cui or gui")
set_property(CACHE SUBSYSTEM_TYPE PROPERTY STRINGS cui gui)
# 确定架构
if(BUILD_32BIT)
set(ARCH "x86")
message(STATUS "Building for 32-bit architecture")
else()
set(ARCH "x86_64")
message(STATUS "Building for 64-bit architecture")
endif()
# 设置目标名称
if(TARGET_TYPE STREQUAL "dll")
set(TARGET_NAME "${PROJECT_NAME}_${ARCH}")
else()
set(TARGET_NAME "${PROJECT_NAME}_${ARCH}")
endif()
# 源文件
set(SOURCES
src/main.cpp
src/utils.cpp
src/config.cpp
)
set(HEADERS
include/utils.h
include/config.h
)
# 创建目标
if(TARGET_TYPE STREQUAL "dll")
add_library(${TARGET_NAME} SHARED ${SOURCES} ${HEADERS})
else()
add_executable(${TARGET_NAME} ${SOURCES} ${HEADERS})
endif()
# 设置包含目录
target_include_directories(${TARGET_NAME} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
# 根据架构设置编译器特定配置
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
if(ARCH STREQUAL "x86")
# 32位配置
target_include_directories(${TARGET_NAME} PRIVATE
/usr/i686-w64-mingw32/include
)
target_compile_options(${TARGET_NAME} PRIVATE
-m32
--sysroot=/usr/i686-w64-mingw32
)
else()
# 64位配置
target_include_directories(${TARGET_NAME} PRIVATE
/usr/x86_64-w64-mingw32/include
)
target_compile_options(${TARGET_NAME} PRIVATE
-m64
--sysroot=/usr/x86_64-w64-mingw32
)
endif()
endif()
# 通用编译选项
target_compile_options(${TARGET_NAME} PRIVATE
-Wall
-Wno-unknown-pragmas
-fno-exceptions
-fno-rtti
-ffunction-sections
-fdata-sections
)
# 根据构建类型设置选项
target_compile_options(${TARGET_NAME} PRIVATE
$<$<CONFIG:Debug>:-O0 -g -DDEBUG>
$<$<CONFIG:Release>:-O3 -DNDEBUG -g0 -fomit-frame-pointer>
$<$<CONFIG:MinSizeRel>:-Os -DNDEBUG>
)
# 链接选项
target_link_options(${TARGET_NAME} PRIVATE
-static
-static-libgcc
-static-libstdc++
-Wl,--gc-sections
)
# Windows子系统设置
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
target_link_options(${TARGET_NAME} PRIVATE
$<$<STREQUAL:${SUBSYSTEM_TYPE},gui>:-Wl,--subsystem,windows>
$<$<STREQUAL:${SUBSYSTEM_TYPE},cui>:-Wl,--subsystem,console>
)
endif()
# Release模式额外的链接选项
target_link_options(${TARGET_NAME} PRIVATE
$<$<CONFIG:Release>:-s -Wl,--strip-all>
)
# 链接库
if(WIN32)
target_link_libraries(${TARGET_NAME} PRIVATE
kernel32
user32
gdi32
winspool
comdlg32
advapi32
shell32
ole32
oleaut32
uuid
odbc32
odbccp32
)
endif()
# 设置输出目录
set_target_properties(${TARGET_NAME} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
)
# 安装配置
install(TARGETS ${TARGET_NAME}
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
# 调试信息输出
message(STATUS "Project: ${PROJECT_NAME}")
message(STATUS "Target: ${TARGET_NAME}")
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
message(STATUS "Target type: ${TARGET_TYPE}")
message(STATUS "Architecture: ${ARCH}")
message(STATUS "Subsystem: ${SUBSYSTEM_TYPE}")
message(STATUS "C++ Compiler: ${CMAKE_CXX_COMPILER}")
message(STATUS "System: ${CMAKE_SYSTEM_NAME}")

8.2 构建脚本示例

build.sh 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#!/bin/bash
# 构建脚本
set -e
# 参数解析
BUILD_TYPE="Release"
ARCH="x86_64"
TARGET_TYPE="exe"
SUBSYSTEM="cui"
CLEAN=false
while [[ $# -gt 0 ]]; do
case $1 in
--debug)
BUILD_TYPE="Debug"
shift
;;
--32bit)
ARCH="x86"
shift
;;
--dll)
TARGET_TYPE="dll"
shift
;;
--gui)
SUBSYSTEM="gui"
shift
;;
--clean)
CLEAN=true
shift
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
# 清理构建目录
if [ "$CLEAN" = true ]; then
echo "Cleaning build directory..."
rm -rf build
fi
# 创建构建目录
mkdir -p build
cd build
# 配置CMake
echo "Configuring CMake..."
if [ "$ARCH" = "x86" ]; then
cmake .. \
-DCMAKE_TOOLCHAIN_FILE=../cmake/mingw32.cmake \
-DCMAKE_BUILD_TYPE=$BUILD_TYPE \
-DBUILD_32BIT=ON \
-DTARGET_TYPE=$TARGET_TYPE \
-DSUBSYSTEM_TYPE=$SUBSYSTEM
else
cmake .. \
-DCMAKE_TOOLCHAIN_FILE=../cmake/mingw64.cmake \
-DCMAKE_BUILD_TYPE=$BUILD_TYPE \
-DBUILD_32BIT=OFF \
-DTARGET_TYPE=$TARGET_TYPE \
-DSUBSYSTEM_TYPE=$SUBSYSTEM
fi
# 构建
echo "Building..."
make -j$(nproc)
echo "Build completed successfully!"
echo "Output directory: $(pwd)/bin"
ls -la bin/

使用示例

1
2
3
4
5
6
7
8
9
10
# 构建64位Release版本
./build.sh
# 构建32位Debug版本
./build.sh --32bit --debug
# 构建GUI应用程序
./build.sh --gui
# 构建DLL
./build.sh --dll
# 清理重建
./build.sh --clean

9. 问题排查与最佳实践

9.1 常见问题及解决方案

1. 编译器找不到

错误信息:

1
2
make[2]: /root/OLLVM_17/build/bin/clang++: Permission denied
make[2]: /root/OLLVM_17/build/bin/clang++: No such file or directory

排查步骤:

1
2
3
4
5
6
7
8
9
10
# 检查文件是否存在
ls -la /root/OLLVM_17/build/bin/clang++
# 检查文件权限
chmod +x /root/OLLVM_17/build/bin/clang++
# 检查是否为符号链接
readlink /root/OLLVM_17/build/bin/clang++
# 检查依赖库
ldd /root/OLLVM_17/build/bin/clang-17
# 临时解决:使用系统编译器
set(CMAKE_CXX_COMPILER clang++)

2. 路径不匹配错误

错误信息:

1
The source directory "/home/user/old_path/..." does not exist.

解决方法:

1
2
3
4
5
6
7
8
9
# 清理CMake缓存
rm -rf CMakeCache.txt CMakeFiles/
# 重新配置
cmake .. -DBUILD_32BIT=ON -DCMAKE_BUILD_TYPE=Release
# 完全重建
rm -rf build/
mkdir build && cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/mingw64.cmake
make

3. 架构配置错误

问题: 32位编译使用了64位头文件路径
正确配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if(ARCH STREQUAL "x86")
# 32位:使用i686路径
target_include_directories(${TARGET_NAME} PRIVATE
/usr/i686-w64-mingw32/include
)
set(MINGW_PREFIX "i686-w64-mingw32")
else()
# 64位:使用x86_64路径
target_include_directories(${TARGET_NAME} PRIVATE
/usr/x86_64-w64-mingw32/include
)
set(MINGW_PREFIX "x86_64-w64-mingw32")
endif()
# 使用变量设置编译器
set(CMAKE_C_COMPILER ${MINGW_PREFIX}-gcc)
set(CMAKE_CXX_COMPILER ${MINGW_PREFIX}-g++)

4. 链接错误

常见链接错误:

  • undefined reference to __imp_xxx

  • 找不到入口点

  • 库文件不兼容
    解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 确保正确的库搜索路径
if(ARCH STREQUAL "x86")
link_directories(/usr/i686-w64-mingw32/lib)
else()
link_directories(/usr/x86_64-w64-mingw32/lib)
endif()
# 显式链接Windows系统库
target_link_libraries(${TARGET_NAME} PRIVATE
kernel32 user32 gdi32 winspool comdlg32
advapi32 shell32 ole32 oleaut32 uuid
odbc32 odbccp32
)
# 静态链接运行时库
target_link_options(${TARGET_NAME} PRIVATE
-static-libgcc
-static-libstdc++
)

9.2 调试技巧

CMake 调试

1
2
3
4
5
6
7
8
9
# 输出变量值
message(STATUS "CMAKE_CXX_COMPILER: ${CMAKE_CXX_COMPILER}")
message(STATUS "CMAKE_SYSTEM_NAME: ${CMAKE_SYSTEM_NAME}")
message(STATUS "BUILD_32BIT: ${BUILD_32BIT}")
message(STATUS "ARCH: ${ARCH}")
# 输出生成的命令
set(CMAKE_VERBOSE_MAKEFILE ON)
# 或者在make时使用
make VERBOSE=1

查看实际执行的命令

1
2
3
4
5
6
7
# 详细输出编译命令
make VERBOSE=1
# 或者设置环境变量
export VERBOSE=1
make
# 查看链接命令
make VERBOSE=1 2>&1 | grep -E "(gcc|g\+\+|ld)"

测试生成的可执行文件

1
2
3
4
5
6
7
8
# 查看文件信息
file output.exe
# 查看依赖库
objdump -p output.exe | grep "DLL Name"
# 查看符号表
objdump -t output.exe
# 查看段信息
objdump -h output.exe

9.3 最佳实践

1. 项目结构建议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
project/
├── CMakeLists.txt
├── cmake/
│ ├── mingw32.cmake
│ ├── mingw64.cmake
│ └── Modules/
├── src/
│ ├── main.cpp
│ └── ...
├── include/
│ └── ...
├── build/
└── scripts/
├── build.sh
└── clean.sh

2. CMakeLists.txt 组织

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1. 基本项目信息
cmake_minimum_required(VERSION 3.15)
project(ProjectName VERSION 1.0.0)
# 2. 设置标准和选项
set(CMAKE_CXX_STANDARD 17)
option(BUILD_32BIT "Build 32-bit version" OFF)
# 3. 变量和配置检查
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
# 4. 交叉编译设置
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
# Windows特定配置
endif()
# 5. 源文件定义
# 6. 目标创建
# 7. 编译和链接选项设置
# 8. 安装配置

3. 版本控制

1
2
3
4
5
6
7
8
9
10
# .gitignore 示例
build/
*.exe
*.dll
*.a
*.so
CMakeCache.txt
CMakeFiles/
Makefile
cmake_install.cmake

4. 持续集成配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# .github/workflows/build.yml
name: Cross Compile Build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
arch: [x86, x86_64]
build_type: [Debug, Release]
steps:
- uses: actions/checkout@v2
- name: Install MinGW
run: |
sudo apt update
sudo apt install mingw-w64
- name: Configure
run: |
mkdir build && cd build
if [ "${{ matrix.arch }}" = "x86" ]; then
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/mingw32.cmake \
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \
-DBUILD_32BIT=ON
else
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/mingw64.cmake \
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \
-DBUILD_32BIT=OFF
fi
- name: Build
run: |
cd build
make -j$(nproc)
- name: Upload artifacts
uses: actions/upload-artifact@v2
with:
name: build-${{ matrix.arch }}-${{ matrix.build_type }}
path: build/bin/

5. 清理和重建脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# clean.sh
echo "Cleaning build artifacts..."
# 清理CMake生成的文件
rm -rf CMakeCache.txt
rm -rf CMakeFiles/
rm -rf cmake_install.cmake
rm -rf Makefile
# 清理构建目录
rm -rf build/
# 清理可执行文件
find . -name "*.exe" -delete
find . -name "*.dll" -delete
find . -name "*.a" -delete
echo "Clean completed!"

9.4 性能优化建议

1. 编译优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Release模式优化
if(CMAKE_BUILD_TYPE STREQUAL "Release")
target_compile_options(${TARGET_NAME} PRIVATE
-O3 # 最高优化级别
-DNDEBUG # 禁用断言
-fomit-frame-pointer # 省略帧指针
-ffast-math # 快速数学运算(谨慎使用)
-funroll-loops # 循环展开
-ffunction-sections # 函数独立段
-fdata-sections # 数据独立段
)
target_link_options(${TARGET_NAME} PRIVATE
-Wl,--gc-sections # 删除未使用段
-Wl,--strip-all # 去除符号表
)
endif()

2. 大小优化

1
2
3
4
5
6
7
8
9
10
11
# 最小化可执行文件大小
target_compile_options(${TARGET_NAME} PRIVATE
-Os # 优化大小
-fno-exceptions # 禁用异常
-fno-rtti # 禁用RTTI
-fno-stack-protector # 禁用栈保护
)
target_link_options(${TARGET_NAME} PRIVATE
-Wl,--gc-sections # 删除未使用段
-s # 去除符号表
)

9.5 容器化开发

Dockerfile 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FROM ubuntu:20.04
# 安装必要工具
RUN apt-get update && apt-get install -y \
build-essential \
cmake \
mingw-w64 \
git \
&& rm -rf /var/lib/apt/lists/*
# 设置工作目录
WORKDIR /workspace
# 复制项目文件
COPY . .
# 构建脚本
RUN mkdir build && cd build && \
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/mingw64.cmake && \
make
CMD ["bash"]

Docker 构建命令

1
2
3
4
5
6
# 构建镜像
docker build -t cross-compile-env .
# 运行容器
docker run -it --rm -v $(pwd):/workspace cross-compile-env
# 在容器中构建
docker run --rm -v $(pwd):/workspace cross-compile-env make -C build

9.6 使用 VSCode + SSH + Docker 的开发环境搭建

9.6.1 开发环境架构概述

环境架构图:

1
2
3
4
WSL 文件系统 ←→ Docker 容器 ←→ VSCode Remote-SSH
↑ ↑ ↑
代码存储 编译环境 开发界面
(持久化) (工具链+库) (编辑+调试)

优势:

  • ✅ 代码在 WSL 中持久化存储,不会丢失

  • ✅ 编译环境在 Docker 中隔离,避免污染主机

  • ✅ VSCode 通过 SSH 连接,获得完整的开发体验

  • ✅ 支持语法高亮、自动补全、调试等功能

9.6.2 环境准备

前置条件检查:

1
2
3
4
5
6
7
8
9
10
# 1. 检查 Docker 是否可用
docker --version
docker ps

# 2. 检查项目目录结构
ls -la /path/to/your/project
# 应该看到:CMakeLists.txt, src/, libs/, include/

# 3. 检查 Docker 镜像是否存在
docker images | grep your_image_name

如果 Docker 权限有问题:

1
2
3
4
5
6
7
8
9
# 添加用户到 docker 组
sudo usermod -aG docker $USER

# 重启 WSL(在 Windows PowerShell 中执行)
wsl --shutdown
wsl

# 验证权限修复
docker ps # 应该不需要 sudo

9.6.3 创建开发容器

完整的容器创建命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 停止并删除旧容器(如果存在)
sudo docker stop old_container_name 2>/dev/null
sudo docker rm old_container_name 2>/dev/null

# 创建新的开发容器
sudo docker run -d \
--name packer-dev \
-p 2222:22 \
-v /home/trg/share_work:/home \
-w /home/kfx_project/compress_pack/packer \
ollvm_dev_rel \
bash -c "
echo '=== 开始配置开发环境 ===' && \
apt update && \
apt install -y openssh-server vim nano tree htop && \
echo '=== 设置 root 密码为 dev123 ===' && \
echo 'root:dev123' | chpasswd && \
echo '=== 配置 SSH 服务 ===' && \
echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config && \
echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config && \
echo '=== 启动 SSH 服务 ===' && \
service ssh start && \
echo '=== 配置完成 ===' && \
echo 'SSH 连接信息:' && \
echo ' 地址: ssh root@localhost -p 2222' && \
echo ' 密码: dev123' && \
echo ' 项目目录: /home/kfx_project/compress_pack/packer' && \
tail -f /dev/null
"

命令参数详解:

  • docker run -d:后台运行容器

  • --name packer-dev:给容器命名,方便后续管理

  • -p 2222:22:端口映射,将容器的22端口映射到主机的2222端口

  • -v /home/trg/share_work:/home:目录挂载,WSL目录映射到容器目录

  • -w /home/kfx_project/compress_pack/packer:设置工作目录为项目根目录

  • ollvm_dev_rel:使用的 Docker 镜像名称

  • bash -c "...":在容器中执行的初始化脚本

初始化脚本说明:

  • apt update && apt install -y openssh-server:安装 SSH 服务器

  • echo 'root:dev123' | chpasswd:设置 root 用户密码为 dev123

  • echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config:允许 root 用户登录

  • echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config:启用密码认证

  • service ssh start:启动 SSH 服务

  • tail -f /dev/null:保持容器运行(重要!)

9.6.4 验证容器配置

等待配置完成:

1
2
3
4
5
6
# 等待容器配置完成(约1-2分钟)
echo "等待容器配置完成..."
sleep 60

# 查看配置日志
sudo docker logs packer-dev

检查容器状态:

1
2
3
4
5
6
7
# 检查容器是否正在运行
sudo docker ps | grep packer-dev
# 应该看到:STATUS 为 "Up X minutes"

# 检查端口映射
sudo docker port packer-dev
# 应该看到:22/tcp -> 0.0.0.0:2222

测试 SSH 连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 从 WSL 测试 SSH 连接
ssh root@localhost -p 2222

# 第一次连接会提示:
# The authenticity of host '[localhost]:2222' can't be established.
# 输入:yes

# 然后输入密码:dev123

# 连接成功后应该看到:
root@container_id:/home/kfx_project/compress_pack/packer#

# 验证环境
pwd # 应该显示项目目录
ls -la # 应该看到 CMakeLists.txt, src/, libs/
which clang++ # 应该显示 OLLVM 编译器路径

# 退出测试连接
exit

9.6.5 配置 VSCode Remote-SSH

安装 Remote-SSH 扩展:

1
2
3
4
# 1. 在 VSCode 中按 Ctrl+Shift+X(打开扩展面板)
# 2. 搜索 "Remote - SSH"
# 3. 安装 Microsoft 的 "Remote - SSH" 扩展
# 4. 等待安装完成(扩展图标会出现在左侧活动栏)

添加 SSH 主机:

1
2
3
4
5
# 1. 在 VSCode 中按 Ctrl+Shift+P(打开命令面板)
# 2. 输入 "Remote-SSH: Add New SSH Host"
# 3. 在弹出的输入框中输入:ssh root@localhost -p 2222
# 4. 选择 SSH 配置文件(通常选择第一个,如 C:\Users\YourName\.ssh\config)
# 5. 看到 "Host added!" 提示表示添加成功

连接到 Docker 容器:

1
2
3
4
5
6
7
# 1. Ctrl+Shift+P
# 2. 输入 "Remote-SSH: Connect to Host"
# 3. 选择 "root@localhost""localhost"
# 4. 选择 "Linux" 作为平台类型
# 5. 如果提示信任主机,选择 "Continue"
# 6. 输入密码:dev123
# 7. 等待连接建立(首次连接可能需要1-2分钟)

连接成功的标志:

  • VSCode 左下角显示:”SSH: localhost”

  • 会打开一个新的 VSCode 窗口

  • 状态栏显示远程连接信息

9.6.6 配置开发环境

打开项目目录:

1
2
3
4
5
# 在连接成功的 VSCode 窗口中:
# 1. File → Open Folder
# 2. 输入:/home/kfx_project/compress_pack/packer
# 3. 点击 OK
# 4. 如果提示信任文件夹,点击 "Yes, I trust the authors"

安装必要的扩展:

1
2
3
4
5
6
7
# 在 SSH 连接的 VSCode 窗口中安装扩展:
# 1. Ctrl+Shift+X 打开扩展面板
# 2. 搜索并安装以下扩展:
# - C/C++ (Microsoft)
# - CMake Tools (Microsoft)
# - C/C++ Extension Pack (Microsoft)
# 3. 等待扩展安装完成

配置 CMake Tools:

1
2
3
4
5
6
# 1. 按 Ctrl+Shift+P
# 2. 输入 "CMake: Select a Kit"
# 3. 选择 "Unspecified" 或手动配置的编译器
# 4. 按 Ctrl+Shift+P
# 5. 输入 "CMake: Configure"
# 6. 等待配置完成

9.6.7 验证开发环境

检查终端环境:

1
2
3
4
5
6
7
8
9
10
# 在 VSCode 中按 Ctrl+`(打开集成终端)
# 应该看到:
root@container_id:/home/kfx_project/compress_pack/packer#

# 验证环境变量和工具
pwd # 应该显示项目根目录
ls -la # 应该看到项目文件
which cmake # 检查 CMake 路径
which clang++ # 检查编译器路径
cmake --version # 检查版本

测试编译功能:

1
2
3
4
5
6
7
8
9
# 在 VSCode 终端中测试编译
mkdir -p build && cd build
cmake ..
make 2>&1 | grep -i error

# 或者使用 VSCode 的 CMake 功能:
# 1. Ctrl+Shift+P
# 2. 输入 "CMake: Build"
# 3. 等待编译完成

验证 IntelliSense 功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 在 VSCode 中打开 src/main.cpp
// 2. 检查以下功能是否正常:
// ✅ 语法高亮正确
// ✅ 自动补全功能(输入 std:: 应该有提示)
// ✅ 错误提示(红色波浪线应该准确)
// ✅ 跳转到定义(Ctrl+点击函数名)
// ✅ 头文件包含无错误(#include <windows.h> 不应该有红线)

// 测试代码示例:
#include <iostream>
#include <string>

int main() {
std::string test = "hello"; // 应该有自动补全
std::cout << test << std::endl; // 应该无错误提示
return 0;
}

9.6.8 日常开发工作流程

启动开发环境:

1
2
3
4
5
6
7
8
# 1. 检查容器是否运行
docker ps | grep packer-dev

# 2. 如果容器停止了,启动它
docker start packer-dev

# 3. 在 VSCode 中连接
# Ctrl+Shift+P → "Remote-SSH: Connect to Host" → 选择 localhost

编译和调试:

1
2
3
4
5
6
7
# 方法1:使用 VSCode 的 CMake Tools
# - 底部状态栏点击 "Build" 按钮
# - 或 Ctrl+Shift+P → "CMake: Build"

# 方法2:使用终端命令
# - Ctrl+` 打开终端
# - cd build && make

文件编辑:

1
2
3
4
5
# VSCode 中的文件修改会实时同步到 WSL
# 1. 在 VSCode 中编辑文件
# 2. Ctrl+S 保存
# 3. 文件自动同步到 WSL 的原始目录
# 4. 在容器中立即可见更改

9.6.9 容器管理脚本

创建管理脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# 在 WSL 的项目目录中创建 docker-dev.sh
cat > docker-dev.sh << 'EOF'
#!/bin/bash

CONTAINER_NAME="packer-dev"
IMAGE_NAME="ollvm_dev_rel"
PROJECT_PATH="/home/trg/share_work"
WORK_DIR="/home/kfx_project/compress_pack/packer"
SSH_PORT="2222"
PASSWORD="dev123"

case "$1" in
start)
echo "=== 启动开发容器 ==="
if docker ps | grep -q $CONTAINER_NAME; then
echo "容器已在运行"
elif docker ps -a | grep -q $CONTAINER_NAME; then
echo "启动现有容器..."
docker start $CONTAINER_NAME
else
echo "创建新容器..."
docker run -d \
--name $CONTAINER_NAME \
-p $SSH_PORT:22 \
-v $PROJECT_PATH:/home \
-w $WORK_DIR \
$IMAGE_NAME \
bash -c "
apt update && \
apt install -y openssh-server vim nano tree htop && \
echo 'root:$PASSWORD' | chpasswd && \
echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config && \
echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config && \
service ssh start && \
tail -f /dev/null
"
echo "等待容器配置完成..."
sleep 30
fi
echo "容器已就绪"
echo "SSH 连接: ssh root@localhost -p $SSH_PORT"
echo "密码: $PASSWORD"
;;

stop)
echo "=== 停止开发容器 ==="
docker stop $CONTAINER_NAME
echo "容器已停止"
;;

remove)
echo "=== 删除开发容器 ==="
docker stop $CONTAINER_NAME 2>/dev/null
docker rm $CONTAINER_NAME 2>/dev/null
echo "容器已删除"
;;

status)
echo "=== 容器状态 ==="
docker ps -a --filter "name=$CONTAINER_NAME"
echo ""
echo "端口映射:"
docker port $CONTAINER_NAME 2>/dev/null || echo "容器未运行"
;;

logs)
echo "=== 容器日志 ==="
docker logs $CONTAINER_NAME
;;

shell)
echo "=== 进入容器 Shell ==="
docker exec -it $CONTAINER_NAME bash
;;

ssh-test)
echo "=== 测试 SSH 连接 ==="
echo "连接地址: ssh root@localhost -p $SSH_PORT"
echo "密码: $PASSWORD"
ssh root@localhost -p $SSH_PORT
;;

*)
echo "Docker 开发环境管理脚本"
echo ""
echo "用法: $0 {start|stop|remove|status|logs|shell|ssh-test}"
echo ""
echo "命令说明:"
echo " start - 启动或创建开发容器"
echo " stop - 停止开发容器"
echo " remove - 删除开发容器"
echo " status - 查看容器状态"
echo " logs - 查看容器日志"
echo " shell - 直接进入容器 Shell"
echo " ssh-test - 测试 SSH 连接"
echo ""
echo "VSCode 连接信息:"
echo " 主机: localhost"
echo " 端口: $SSH_PORT"
echo " 用户: root"
echo " 密码: $PASSWORD"
;;
esac
EOF

# 给脚本执行权限
chmod +x docker-dev.sh

使用管理脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 启动开发环境
./docker-dev.sh start

# 查看容器状态
./docker-dev.sh status

# 测试 SSH 连接
./docker-dev.sh ssh-test

# 查看容器日志
./docker-dev.sh logs

# 停止容器
./docker-dev.sh stop

9.6.10 VSCode 配置优化

创建 VSCode 工作区配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 在项目根目录创建 .vscode/settings.json
{
"cmake.configureArgs": [
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON"
],
"cmake.buildDirectory": "${workspaceFolder}/build",
"C_Cpp.default.compilerPath": "/root/OLLVM_17/build/bin/clang++",
"C_Cpp.default.cppStandard": "c++17",
"C_Cpp.default.cStandard": "c17",
"C_Cpp.default.intelliSenseMode": "clang-x64",
"C_Cpp.default.defines": [
"_WIN32",
"WIN32",
"_WIN64",
"WIN64",
"_WINDOWS",
"_DEBUG"
],
"files.associations": {
"*.h": "c",
"*.hpp": "cpp",
"*.cpp": "cpp",
"CMakeLists.txt": "cmake"
},
"editor.tabSize": 4,
"editor.insertSpaces": true
}

创建调试配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 创建 .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Packer",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/x64/Debug/packer.exe",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}

9.6.11 常见问题和解决方案

问题1:SSH 连接被拒绝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 错误:ssh: connect to host localhost port 2222: Connection refused

# 解决步骤:
# 1. 检查容器是否运行
docker ps | grep packer-dev

# 2. 检查 SSH 服务状态
docker exec -it packer-dev service ssh status

# 3. 重启 SSH 服务
docker exec -it packer-dev service ssh restart

# 4. 重新测试连接
ssh root@localhost -p 2222

问题2:密码认证失败

1
2
3
4
5
6
7
8
9
10
11
# 错误:Permission denied (publickey,password)

# 解决步骤:
# 1. 重新设置密码
docker exec -it packer-dev bash -c "echo 'root:dev123' | chpasswd"

# 2. 检查 SSH 配置
docker exec -it packer-dev grep -E "(PermitRootLogin|PasswordAuthentication)" /etc/ssh/sshd_config

# 3. 重启 SSH 服务
docker exec -it packer-dev service ssh restart

问题3:VSCode 连接后看不到项目文件

1
2
3
4
5
6
7
8
9
# 解决步骤:
# 1. 检查工作目录
# 在 VSCode 终端中:pwd

# 2. 手动切换到项目目录
cd /home/kfx_project/compress_pack/packer

# 3. 重新打开正确的文件夹
# File → Open Folder → /home/kfx_project/compress_pack/packer

问题4:编译器找不到

1
2
3
4
5
6
# 在 VSCode 终端中检查:
which clang++
ls -la /root/OLLVM_17/build/bin/

# 如果编译器不存在,检查镜像是否正确
docker images | grep ollvm_dev_rel

9.6.12 开发环境维护

定期维护任务:

1
2
3
4
5
6
7
8
9
10
11
# 1. 清理停止的容器
docker container prune

# 2. 清理未使用的镜像
docker image prune

# 3. 查看磁盘使用情况
docker system df

# 4. 备份重要配置
cp .vscode/settings.json .vscode/settings.json.bak

容器数据备份:

1
2
3
4
5
6
7
8
# 1. 提交容器为新镜像(保存环境配置)
docker commit packer-dev packer-dev-configured

# 2. 导出镜像
docker save packer-dev-configured > packer-dev-backup.tar

# 3. 恢复镜像
docker load < packer-dev-backup.tar

环境重置:

1
2
3
# 完全重置开发环境
./docker-dev.sh remove # 删除容器
./docker-dev.sh start # 重新创建

9.6.13 最佳实践总结

开发流程最佳实践:

  1. 一次性设置:创建配置好的开发容器

  2. 持久化使用:使用命名容器,避免重复配置

  3. 代码分离:代码在 WSL 中,环境在 Docker 中

  4. 工具集成:充分利用 VSCode 的开发功能

  5. 脚本管理:使用脚本简化日常操作

安全注意事项:

  • 🔒 使用强密码或 SSH 密钥认证

  • 🔒 不要在生产环境中使用简单密码

  • 🔒 定期更新容器中的软件包

  • 🔒 及时清理不需要的容器和镜像

性能优化建议:

  • ⚡ 使用 SSD 存储提高 I/O 性能

  • ⚡ 分配足够的内存给 Docker

  • ⚡ 使用多核编译:make -j$(nproc)

  • ⚡ 启用 CMake 并行构建

通过这套完整的 VSCode + SSH + Docker 开发环境,你可以享受到现代 IDE 的所有功能,同时保持环境的隔离性和可重复性,非常适合交叉编译和团队协作开发。