一、通用¶
二、NDK¶
NDK 使用入门 | Android NDK | Android Developers Android NDK API Reference | Android Developers
2.1 版本注意¶
只看API行为、产物类型,不绑定特定NDK版本;静态库本身不做链接,只做编译,无链接选项。
| 目标API | 产物类型 | 编译&链接参数 | 平台关键行为变更节点 | STL相关约束 |
|---|---|---|---|---|
| ≤15 | 可执行 | 不能用 -fPIE -pie 无 -pie 链接参数 |
Android4.4‑及更早不支持PIE可执行,PIE程序直接崩溃 | gnustl可用;libc++未正式落地 |
静态库 .a |
编译:‑fPIC(推荐) 无链接参数,ar打包 |
静态库只是目标文件归档,本身不加载进进程;最终行为取决于最终链接成exe/so时的参数 | 静态库可内嵌STL对象;注意避免多重STL符号副本 | |
动态库 .so |
编译:‑fPIC 链接:‑shared;禁止‑pie |
so永远要求位置无关 | gnustl | |
| 16‑20 | 可执行 | 可选 -fPIE -pie |
API16开始实验支持PIE;系统同时支持PIE/非PIE可执行,不强制 | gnustl / libc++均可 |
静态库 .a |
编译:‑fPIC(推荐) 无链接参数,ar打包 |
静态库本身无PIE/PIC属性;若该.a会被链接进.so,必须用‑fPIC编译 |
同上,最终产物决定行为 | |
动态库 .so |
编译:‑fPIC 链接:‑shared;禁止‑pie |
so永远要求位置无关 | gnustl / libc++ | |
| ≥21 | 可执行 | 必须 -fPIE(编译) -pie(链接) |
API21(Android5.0) 强制PIE;非PIE可执行直接拒绝运行 | gnustl / libc++;ABI不互通,不能跨STL边界传递容器对象 |
静态库 .a |
编译:‑fPIC(推荐) 无链接参数,ar打包 |
1. .a被链接进PIE可执行:.a可以不带‑fPIC,但建议统一‑fPIC 2. .a被链接进.so:.a必须‑fPIC |
静态库内嵌STL容易出现符号重复;尽量不在.a中实例化STL全局对象 | |
动态库 .so |
编译:‑fPIC 链接:‑shared;禁止‑pie |
so永远要求位置无关,PIE仅针对独立可执行 | gnustl / libc++ |
| 参数 | 类型 | 作用对象 | 含义 |
|---|---|---|---|
-fPIC |
编译选项(Compiler flag) | .c/.cpp → .o |
生成位置无关代码,用于动态库 .so |
-fPIE |
编译选项(Compiler flag) | .c/.cpp → .o |
生成位置无关代码,用于独立可执行程序 |
-pie |
链接选项(Linker flag) | .o → 最终exe |
将目标文件链接输出为 PIE类型可执行文件 |
快速记忆:可执行用 fPIE + pie;动态库用 fPIC + shared;静态库只加 fPIC,不要pie不要shared。PIC英文为Position‑Independent Code, Position‑Independent Executable。
官方文档其实只有介绍性的说明,目前能了解到的最多的是--help的结果。
2.2 gnu¶
Android Bionic Libc 原理与实现综述_安卓libc-CSDN博客
深入Android系统(二)Bionic库咳咳,有木有发现这么多的了解字眼?因为从这几天本人大脑的表现来看,这种不常用的 - 掘金
ATL深度解析(5)Bionic 兼容层 — 让两种 libc 在同一进程共存 | Wacao's Den
GCC4.9 的 exe不识别‑target 参数;目标架构已经固化在 exe 里面。
NDK r18 彻底删除 gcc 整套多目录工具链,全部迁移 clang+lld,不再存在aarch64‑linux‑android‑4.9目录。
项目中使用的是CMAKE,但太重了,经常有自测的需求,因此可以封装下shell函数,方便可执行文件自测,封装后和通用的gcc g++命令使用方式基本一致:
export NDK_R16B_ROOT="${NDK_R16B_ROOT:-/d/Android/android-ndk-r16b}"
export NDK_R16B_API="${NDK_R16B_API:-27}"
export NDK_R16B_STL="${NDK_R16B_STL:-static}" # static | shared | system
android-gnu_gcc() {
local B="${NDK_R16B_ROOT}/toolchains/aarch64-linux-android-4.9/prebuilt/windows-x86_64/bin"
"${B}/aarch64-linux-android-gcc.exe" \
--sysroot="${NDK_R16B_ROOT}/platforms/android-${NDK_R16B_API}/arch-arm64" \
-isystem "${NDK_R16B_ROOT}/sysroot/usr/include" \
-isystem "${NDK_R16B_ROOT}/sysroot/usr/include/aarch64-linux-android" \
-D__ANDROID_API__="${NDK_R16B_API}" \
-fPIE -pie \
"$@"
}
android-gnu_gpp() {
local B="${NDK_R16B_ROOT}/toolchains/aarch64-linux-android-4.9/prebuilt/windows-x86_64/bin"
local G="${NDK_R16B_ROOT}/sources/cxx-stl/gnu-libstdc++/4.9"
local -a STL_INC=() STL_LIB=()
case "${NDK_R16B_STL}" in
static|shared)
STL_INC=(-isystem "${G}/include" -isystem "${G}/libs/arm64-v8a/include" -L "${G}/libs/arm64-v8a")
STL_LIB=(-lgnustl_${NDK_R16B_STL}) ;;
system)
STL_INC=(-isystem "${NDK_R16B_ROOT}/sources/cxx-stl/system/include") ;;
*) echo "android-gnu: NDK_R16B_STL 只能是 static / shared / system(当前=${NDK_R16B_STL})" >&2; return 1 ;;
esac
"${B}/aarch64-linux-android-g++.exe" \
--sysroot="${NDK_R16B_ROOT}/platforms/android-${NDK_R16B_API}/arch-arm64" \
-isystem "${NDK_R16B_ROOT}/sysroot/usr/include" \
-isystem "${NDK_R16B_ROOT}/sysroot/usr/include/aarch64-linux-android" \
-D__ANDROID_API__="${NDK_R16B_API}" \
"${STL_INC[@]}" \
-fPIE -pie \
"$@" "${STL_LIB[@]}"
}
2.3 llvm¶
export NDK_R27D_ROOT="${NDK_R27D_ROOT:-/d/Android/android-ndk-r27d}"
export NDK_R27D_API="${NDK_R27D_API:-24}"
export NDK_R27D_STL="${NDK_R27D_STL:-static}" # static | shared
android-llvm_gcc() {
"${NDK_R27D_ROOT}/toolchains/llvm/prebuilt/windows-x86_64/bin/clang.exe" \
--target="aarch64-linux-android${NDK_R27D_API}" \
-fPIE -pie \
"$@"
}
android-llvm_gpp() {
local STL=
case "${NDK_R27D_STL}" in
static) STL="-static-libstdc++" ;;
shared) STL= ;;
*) echo "android-llvm: NDK_R27D_STL 只能是 static 或 shared(当前=${NDK_R27D_STL})" >&2; return 1 ;;
esac
"${NDK_R27D_ROOT}/toolchains/llvm/prebuilt/windows-x86_64/bin/clang++.exe" \
--target="aarch64-linux-android${NDK_R27D_API}" \
-fPIE -pie \
${STL} \
"$@"
}
2.3.1 编程工具¶
LLVM 是一套编译器基础设施(编译器后端框架);Clang 是基于 LLVM 的 C/C++/Objective‑C 编译器前端。
LLVM/Clang 本身是“可重定向”的通用工具链:一份代码能生成支持任意目标的编译器。官方对每个宿主平台(Windows / Linux / macOS)都会构建全套工具,而不是只构建该宿主做 Android 开发需要的那几个。
真正给 Android 用的只有一小部分
- 编译:clang.exe / clang++.exe(及 .cmd wrapper)、ld.lld.exe
- 直接手写 ld.lld,就等于你要把 clang driver 那套平台推导逻辑自己手工复现一遍(
‑L、crt、dynamic‑linker),参数又多又容易错。 - 远优先调用
clang++.exe,把平台细节交给 driver;只有调试研究的时候才去看 / 抄 ld.lld 的命令。 - 静态库:llvm-ar.exe / llvm-ranlib.exe
- 排查崩溃栈/看二进制:llvm-addr2line.exe、llvm-nm.exe、llvm-readelf.exe、llvm-objdump.exe、llvm-symbolizer.exe、llvm- cxxfilt.exe
- 提体积:llvm-strip.exe
| 工具 | 核心定位 | 典型产出 | 重点说明 |
|---|---|---|---|
| clang‑format.exe | 代码格式化(空格、缩进、括号) | 格式化后的源码 | 只改排版;不做语法语义、不查bug |
| clang‑tidy.exe | Lint静态检查 + 部分自动修复 | 告警、可原地修复代码 | 做语义检查:bugprone、modernize、performance、readability;可调用clang静态分析器;支持.clang‑tidy配置文件 |
| clang‑check.exe | AST调试、快速语法语义检查 | AST转储、编译告警 | 底层LibTooling;多用于开发clang插件;日常业务代码优先用clang‑tidy |
| llvm‑cov.exe + llvm‑profdata.exe | 源码级覆盖率(PGO/‑fprofile‑instr‑generate) | 行/分支覆盖率报告 | 需要编译插桩;运行生成.profraw;合并为.profdata;输出源码覆盖率报告 |
| sancov.exe + sanstats.exeSanitizer‑Coverage(ASan/UBSan配套覆盖率,fuzz专用) | 二进制PC地址、已覆盖/未覆盖函数 | 和ASan等sanitizer一起用;输出.sancov;不生成源码行级报告,只看基本块/函数是否命中,多用于libFuzzer模糊测试 |
2.4 动态库加载¶
linux/Android
dlopen动态库搜索路径:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/your/
2.5 java加载动态库¶
java加到动态库并不会检查动态库的依赖,若运行时依赖动态库缺失时,才会报错。类似于C++ 运行时通过dlopen加载动态库。
package com.company.sdkname;
public class libtestmodule {
static {
System.loadLibrary("testsoname");
}
public static native <type> <JnifunctionName>( ...);
}
navtive 对接jni的cpp加入库的编译中,就能正常编译出可以被java使用的库。
2.6 程序运行¶
对于没有root权限的车机或Android手机,可以到/data/local/tmp路径下,更新LD_LABRARY_PATH,然后运行NDK直接编译出来的可执行文件,文件需要添加可执行权限。
三、windows local¶
3.1 MSVC¶
微软的C++编译工具链(MSVC,Microsoft Visual C++)是由微软公司开发的用于编译C++代码的工具套件。它包含编译器(cl.exe)、链接器(link.exe)和其他一些工具(如资源编译器(rc.exe)和宏处理器(c1.dll)等)。
MSVC支持多种编程范式,包括面向过程的编程、面向对象的编程和泛型编程等。它还支持多种平台,包括Windows、Linux和Mac OS等。
工具链版本:MSVC\14.29.30133
查看默认支持的C++版本:cl /?,vs2019默认支持C++14
3.1.1 path¶
The MSVC command-line tools use the
PATH,TMP,INCLUDE,LIB, andLIBPATHenvironment variables, and also use other environment variables specific to your installed tools, platforms, and SDKs.
3.1.2 start up¶
The simplest way to specify a particular build architecture in an existing command window is to use the Common7\Tools\vcvarsall.bat file. Use vcvarsall.bat to set environment variables to configure the command line for native 32-bit or 64-bit compilation. Arguments let you specify cross-compilation to x86, x64, ARM, or ARM64 processors.
环境设置后,就可以通过devenv来编译项目了。
devenv [解决方案文件 | 项目文件 | 任意文件.扩展名] [开关]
命令行开关一般会先后运行:/Clean, /Build
官网说明详见[-]
3.1.3 cmake use msvc¶
cmake是有平台限制的,下载exe安装包安装cmake
"C:\Program Files\CMake\bin\cmake.EXE" --no-warn-unused-cli -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -SD:/tmp/test -Bd:/tmp/test/build -G "Visual Studio 16 2019" -T host=x86 -A win32
cmake --no-warn-unused-cli:在构建项目时输出警告信息。
-SD:/tmp/test 是设置源代码的路径,也就是你的项目文件所在的地方。
-Bd:/tmp/test/build是设置构建的路径,也就是生成的构建文件(如Makefile或者Visual Studio的.sln文件)存放在哪里。
CMake -G还有其他选项,例如:
Unix Makefiles:使用Unix make作为构建工具。Ninja:使用Ninja作为构建工具。Visual Studio 16 2019:生成适用于Visual Studio 2019的解决方案。Visual Studio 16 2019 Win64:生成适用于64位Windows的Visual Studio解决方案。Visual Studio 16 2019 ARM:生成适用于ARM版的Visual Studio解决方案。
3.1.4 MSBuild use MSVC¶
Use MSBuild (msbuild.exe) and a project file (.vcxproj) to configure a build and invoke the toolset without loading the Visual Studio IDE. It's equivalent to running the Build project or Build Solution command in the Visual Studio IDE.
在IDE(Visutal Studio)中配置项:配置管理器中设置,并在属性页中选择。在是一个环境变量,它代表了当前项目的配置名称。在Visual Studio中,每个项目通常都有多个配置,例如Debug和Release。这些配置用于控制编译器的编译选项、链接器的链接选项等。
3.1.5 属性对照表¶
| 属性 | Win32 | gcc | 说明 |
|---|---|---|---|
| 开启调试信息 | /Zi | -g | |
| 编译优化 | /Od | -O0 | |
| 禁用警告 | /W0 | -w | |
| 静态编译 | /MT | -static | /MT静态链接的 C 运行时库 |
3.1.6 动态库加载顺序¶
启用"安全DLL查找模式"时,查找顺序如下:[-] a. 应用程序所在目录; b. 系统目录。GetSystemDirectory返回的目录,通常是系统盘\Windows\System32; c. 16位系统目录。该项只是为了向前兼容的处理,可以不考虑; d. Windows目录。GetWindowsDirectory返回的目录,通常是系统盘\Windows; e. 当前目录。GetCurrentDirectory返回的目录; f. 环境变量PATH中所有目录。
如果"安全DLL查找模式"被禁用,查找顺序如下: a. 应用程序所在目录; b. 当前目录。GetCurrentDirectory返回的目录; c. 系统目录。GetSystemDirectory返回的目录,通常是系统盘\Windows\System32; d. 16位系统目录。该项只是为了向前兼容的处理,可以不考虑; e. Windows目录。GetWindowsDirectory返回的目录,通常是系统盘\Windows; f. 环境变量PATH中所有目录。
运行exe依赖动态库的话,可以通过:
1、使用全路径加载: 将动态库的路径添加到$PATH
3.2 MINGW64¶
使用msys2的MINGW64终端:pacman -S mingw-w64-x86_64-cmake mingw-w64-x86_64-gcc make,cmake默认是ninja构建。要使用makefile需指定-G "Unix Makefiles"。
若项目依赖大量 mingw‑gcc 库,只想用 clang 编译器,仍用 libstdc++。则pacman -S mingw-w64-x86_64-clang mingw-w64-x86_64-lld
# 1.1 CMakeList.txt形式
# ⚠️必须放在 project() 之前;修改后删除build目录重生成
set(CMAKE_CXX_COMPILER "g++")
set(CMAKE_C_COMPILER "gcc")
project(MyProject LANGUAGES C CXX)
# 2. 命令行形式
cmake -G "Unix Makefiles" -B build -S . -DCMAKE_CXX_COMPILER="/c/Program Files/Git/mingw64/bin/g++.exe" -DCMAKE_C_COMPILER="/c/Program Files/Git/mingw64/bin/gcc.exe"
注:使用msys的相对路径可能不识别。我把git bash和msys2合并了,如果不合并,需要修改路径。
3.3 CLANG64¶
使用msys2的CLANG64终端打开:
pacman -S mingw-w64-clang-x86_64-clang mingw-w64-clang-x86_64-lld
四、嵌入式¶
4.1 none-eabi¶
重要背景:ARMCC (AC5 armcc) 已经停止新增功能,仅维护 bug 修复;新项目优先学习 Arm Compiler 6(armclang,基于 LLVM)。 工具链组成(脱离 Keil MDK 图形界面,命令行使用)
- AC5:
armcc(C 编译器)、armcpp(C++)、armasm(汇编器)、armlink(链接器)、fromelf(elf 转 hex/bin) - AC6:
armclang(C/C++ 合一)、armasm-clang、armlink、fromelf - GCC-ARM:
arm-none-eabi-gcc+.ld链接脚本;Arm-CC 用 .sct 分散加载脚本(Scatter-file),不使用 ld 脚本。
Arm Compiler for Embedded | Arm Learning Paths
Compiler Reference Guide: Arm Compiler for Embedded Reference Guide
五、cmake¶
cmake网上已有的说明太多了,这里不再赘述,能上衔接的直接上链接。
zhihu-全小鱼-CMake 如何入门?
如何组织你的项目 · Modern CMake
CMake Reference Documentation — CMake 4.4.2 Documentation
命令快查:https://cmake.org/cmake/help/latest/command/<命令名称>.html
注:和大多数工具一样,会基础命令和操作后,绝大多数需求都能解决了。剩下的就是优不优雅的问题了。
CMake 采用树形工程结构,工程由顶层CMakeLists.txt作为根,通过add_subdirectory递归引入子工程,构建完整的构建树。系统划分两类使用者,职责与工作模式相互隔离:
- 普通用户(应用开发者):聚焦业务构建流程
普通用户主要关注业务代码的构建逻辑,标准工作链路:创建子工程、定位文件路径、配置头文件搜索路径、添加源文件、指定目标产物类型、配置目标链接依赖。
用户可以通过
CMAKE_C_FLAGS/CMAKE_CXX_FLAGS这类编译标志变量,将配置项直接透传给底层编译工具;同时也可以通过命令行‑DXXX=VALUE定义 CMake 变量,在脚本中依靠if条件分支,实现应用层的构建流程控制。 - 编译链开发者(工具链编写者):提供交叉编译环境
工具链开发者编写
.cmake工具链文件,普通用户通过‑DCMAKE_TOOLCHAIN_FILE参数加载该文件,完成交叉编译环境初始化。 工具链脚本内部会读取命令行传入的‑D全局变量(例如平台选型、API Level 等配置),以此完成编译环境的配置;工具链开发者需要对外暴露可配置项,并配套文档,指导上层普通用户如何传入参数使用。
CMAKE注意事项:
- add_library, 新手需要注意的是 add = add a target to the project:向 CMake 项目新增一个库类型的构建目标, 而非target_link_libraries。
- 顺序敏感
- 大小写敏感
- 作用域多种
5.1 顺序敏感实验¶
抛开变量和流程控制的代码, 以add_xxx为基准,target_xxx命令放到后面,其他的一般是放到前面。
set(SRCS a.cpp b.cpp)
add_library(mylib SHARED ${SRCS}) # 先建 target
list(REMOVE_ITEM SRCS "b.cpp") # 之后才移除
add_definitions(-DAFTER_ADD_LIBRARY) # 之后才加宏
add_library(mylib_obj OBJECT ${SRCS})
结果:
mylib(SHARED) 实际编译: a.cpp.o b.cpp.o ← REMOVE_ITEM 无效 mylib_obj(OBJ) 实际编译: a.cpp.o ← 生效 mylib 的 CXX_DEFINES = -DAFTER_ADD_LIBRARY -Dmylib_EXPORTS ← 宏却倒灌进去了
5.2 多种作用域下的命令¶
目录级(下文生效)——向下:当前目录下文+后续子目录,不向上冒泡 (只在当前目录解析栈帧修改);✅ 强依赖 add_subdirectory 解析顺序:
include_directorieslink_directoriesadd_compile_definitionsadd_compile_optionslink_libraries
项目级——仅当前任务解析栈帧内修改:
project()cmake_minimum_required()option()set(CACHE)
目标级——依赖链传播(PUBLIC/INTERFACE);❌ 不依赖目录顺序,只看 target_link_libraries:
target_*set_target_properties
| 功能 | 目录级命令(Directory-scope) | 目标级命令(Target-scope) |
|---|---|---|
| 头文件搜索路径 | include_directories() |
target_include_directories() |
| 库文件搜索路径 | link_directories() |
target_link_directories() |
| 编译宏定义 | add_compile_definitions() |
target_compile_definitions() |
| 编译选项 | add_compile_options() |
target_compile_options() |
| 链接库依赖 | link_libraries() |
target_link_libraries() |
| 链接选项 | add_link_options() |
target_link_options() |
5.3 变量赋值命令¶
set(VAR VALUE):普通变量赋值,无CACHE、无PARENT_SCOPEaux_source_directory(dir VAR):扫描目录,把源文件列表存入当前栈帧普通变量;仅赋值,无目录级副作用;相对路径基于CMAKE_CURRENT_LIST_DIRlist(APPEND VAR ...):列表追加,修改当前栈帧普通列表变量list(PREPEND VAR ...):列表头部插入list(REMOVE_ITEM VAR ...):列表移除元素list(FILTER VAR ...):列表过滤list(SORT VAR ...):列表排序unset(VAR):清除当前栈帧普通变量
5.4 变量¶
5.4.1 环境变量¶
环境变量:$ENV{VAR}
5.4.2 内置变量¶
第一组:目录解析栈帧(由 add_subdirectory() 驱动;include() 不会改变这一组)
| 变量 | 含义 |
|---|---|
CMAKE_CURRENT_SOURCE_DIR |
当前正在处理的CMakeLists.txt所在源码目录;add_subdirectory切换;include保持调用者的值 |
CMAKE_CURRENT_BINARY_DIR |
当前CMakeLists.txt对应的构建输出目录;add_subdirectory切换;include保持调用者的值 |
⚠️坑:
include(xxx.cmake)进入脚本内部,这两个仍然是调用方的目录,不是xxx.cmake所在目录。
第二组:list‑file执行上下文(脚本文件上下文;include / add_subdirectory 都会切换)
| 变量 | 含义 |
|---|---|
CMAKE_CURRENT_LIST_DIR |
当前正在执行脚本文件的目录(CMakeLists.txt / .cmake都生效);写cmake模块首选 |
CMAKE_CURRENT_LIST_FILE |
当前正在执行脚本文件的完整绝对路径(带文件名) |
CMAKE_CURRENT_LIST_LINE |
当前执行到的脚本行号 |
关键:
include()会切换这一组;CMAKE_CURRENT_LIST_DIR拿到的是被include的脚本本身目录,不受调用方影响。
第三组:function() 内部专用(CMake ≥3.17,仅在函数体内有效;宏macro无效)
| 变量 | 含义 |
|---|---|
CMAKE_CURRENT_FUNCTION |
当前正在执行的函数名称,用于调试打印 |
CMAKE_CURRENT_FUNCTION_LIST_DIR |
定义该函数的那个文件所在目录(不是调用处的目录);函数内部读取同目录资源用这个 |
CMAKE_CURRENT_FUNCTION_LIST_FILE |
定义函数的脚本完整路径 |
CMAKE_CURRENT_FUNCTION_LIST_LINE |
函数定义所在的行号 |
第4组:project内置变量(任务解析栈帧)
| 变量 | 说明 |
|---|---|
PROJECT_NAME |
最近一次project()的项目名称 |
PROJECT_SOURCE_DIR |
最近一次project()所在源码目录 |
PROJECT_BINARY_DIR |
最近一次project()对应的构建目录 |
PROJECT_VERSION |
完整版本号(VERSION参数) |
注意:
CMAKE_PROJECT_NAME不属于本组,归到第5组全局变量。
第5组:全局内置变量(不受任何栈帧影响,整个配置周期固定)
| 变量 | 说明 |
|---|---|
CMAKE_SOURCE_DIR |
最顶层源码根目录,顶层CMakeLists.txt所在目录,永不改变 |
CMAKE_BINARY_DIR |
最顶层构建根目录,执行cmake的build目录,永不改变 |
CMAKE_PROJECT_NAME |
顶层项目名称;只有顶层project()可以修改;子目录project不会改动它 |
CMAKE_VERSION |
CMake版本号,如3.26.0 |
CMAKE_COMMAND |
cmake可执行文件完整路径 |
CMAKE_ROOT |
CMake安装根目录 |
CMAKE_GENERATOR |
当前使用生成器(Unix Makefiles / Ninja / Visual Studio…) |
CMAKE_MAKE_PROGRAM |
实际构建工具路径(make/ninja/msbuild) |
CMAKE_SIZEOF_VOID_P |
指针字节大小(4=32位,8=64位) |
补充:CACHE缓存变量(
set(...CACHE)/option())也属于全局持久状态,不属于栈帧,存在CMakeCache.txt。
5.4.3 普通变量和缓存变量¶
option(ENABLE_SUB "enable sub feature" ON):是set的语法糖set(FOO ON CACHE BOOL "description"):作用域特殊
| 写法 | 存储位置 | 作用域说明 | 是否写入 CMakeCache.txt | 能否被 -D 命令行覆盖 |
|---|---|---|---|---|
set(VAR val) |
目录上下文栈(普通变量) | 目录级普通变量;父→子拷贝副本,子目录可以读取;子目录修改仅修改自身副本,不会回写到父目录;子目录新增普通变量父目录不可见;拷贝发生在 add_subdirectory() 进入子目录时刻 |
❌ 否 | ❌ 不能 |
set(VAR val CACHE TYPE "desc") |
全局缓存池 | 全项目可读;必须执行到该行才创建缓存条目;执行之前的代码无法访问;不修改目录上下文栈;-D 命令行可以修改缓存条目 |
✅ 是 | ✅ 可以 |
set(VAR val PARENT_SCOPE) |
父目录的普通变量(内存) | 仅修改上一层父作用域的普通变量;当前子目录内该变量不会被改动;仅对父目录 add_subdirectory() 返回之后的下文生效;对子目录内部、孙子目录无直接影响;不会递归向上冒泡多层 |
❌ 否 | ❌ 不能 |
5.5 add_library类型¶
add_library(foo [STATIC|SHARED|MODULE|OBJECT] a.cpp)
SHARED vs MODULE 三平台对比:
✅ Linux:二进制格式完全一样(都是 ELF
.so),区别仅为 CMake 构建语义(即语法限制被target_link_libraries的调用) ⚠️ macOS:二进制文件类型不一样;SHARED 是MH_DYLIB,MODULE 是MH_BUNDLE(bundle 插件) ⚠️ Windows:二进制都是 PE‑DLL,但产物集合不一样;SHARED 输出 DLL + 导入库,MODULE 只输出 DLL,无导入库
OBJECT的使用:
# 典型应用场景:单元测试复用源码,进行多个测试执行文件的生成,单元测试开启覆盖率、开盒宏等。
add_library(foo OBJECT a.cpp b.cpp c.cpp)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE $<TARGET_OBJECTS:foo>)
5.6 链接形式¶
链接方式:
- 隐式动态链接
- 编译链接阶段:链接器把依赖库写入 ELF 的
DT_NEEDED段。 - 程序启动时:内核加载可执行文件,动态链接器自动遍历全部 DT_NEEDED 列表,递归加载所有依赖的.so。
- 显式动态链接
- 编译链接阶段:不写 DT_NEEDED;链接时只链接
libdl.so(dlopen/dlsym/dlclose 符号),不会把目标库加入 DT_NEEDED。 - 运行时,代码执行到 dlopen 调用才主动请求动态链接器加载库。当它加载一个
.so文件时,该 so 内部的 DT_NEEDED 依然会被动态链接器递归处理。 - 静态链接
- 静态库
.a只是一堆.o的打包,没有 DT_NEEDED、没有内部依赖记录。 当链接器处理静态库时:只提取当前未解析符号所需要的.o;不会自动去寻找该静态库所依赖的其它库。 依赖关系必须由链接命令行顺序显式给出。同 CMake 工程内部target_link_libraries(A PRIVATE B),target_link_libraries(B PRIVATE C)CMake 会把依赖链展开,下游链接命令自动带上-lA -lB -lC。 这是 CMake 的上层特性,不是静态链接器的递归能力。 - 特殊情况:构建静态库
.a时输入依赖动态库.so。例:A (SHARED)→B (STATIC)→C (SHARED)。A 动态链接 B,B 引用C的符号, 最终 B会没入A(严谨说法:提取 B.a 中需要的.o 对象拷贝进 A.so),同时 A DT_NEEDED C。 - 特殊情况下的特殊情况: 通过底层编译工具链 ld -r 可以 C 作为 B 的静态链接输入,知道就行,并理解「动态库不一定用于动态链接,也可能是静态链接输入」
- 静态库不一定用于静态链接,也可能被继续ar归档(追加.a或.o)。
5.7 依赖链¶
所有权属性相关详见接口可见性.md
Q: cmake 中,target_* 命令我都以 private 属性来出来, B 依赖 A 及 A 的头文件, C 我显式依赖 A 和 B 及其 AB 头文件 也是可以的吧
A: 语法上可以跑通,但属于手动堆砌依赖,没有利用 CMake 传递依赖机制。依赖链越长,维护爆炸。
对于依赖链:Comsumer → Provider → Third, 对于静态链接和隐式静态链接两种形式的处理(这里先不讨论cmake):
1️⃣ 当 Provider = STATIC(.a)
无论Consumer是静态还是动态,编译阶段都必须暴露Provider内部的Third依赖。
- Provider.a 只是.o归档,不会把Third“吃进去”。
- 不管Consumer是.a还是.so,链接Consumer的时候,Third必须参与链接。
- 想对外隐藏Third,只能做合并归档(把Provider.a + Third.a打成一个新.a)。
2️⃣ 当 Provider = SHARED(.so)
构建Provider.so的时候就完成链接,Consumer构建阶段不需要Third。
- 如果Third是
.a:代码被拷贝进Provider.so;运行时不需要单独Third.a。- 如果Third是
.so:Provider.so记录DT_NEEDED;运行时必须存在Third.so,编译期不用。
3️⃣ 特殊坑:Consumer = STATIC(.a) 依赖 SHARED(.so)
构建Consumer.a不会报错,但是静态库只是存符号引用,没有完成链接。 当你最后把Consumer.a链接到可执行程序时,必须提供Provider.so,否则报未定义。 👉 静态库不能“消化”动态库;动态库的符号引用只是被保存在静态库的.o里面。