编译器
把语言结构翻译成机器指令。例如看到函数调用时,生成“按照某种调用约定跳过去”的代码。
一条 CMake 命令背后,编译器、ABI、标准库、CRT 与链接器各自参与其中。本文沿着源码变成程序的路径逐层拆开它们,再回到 Windows 上的真实组合:为什么 Clang 可以使用 Microsoft 运行时,哪些边界需要逐项验证。
我曾经也习惯把 CMake、编译器、运行库统称为“工具链”,直到一次二进制兼容问题迫使我逐项核对,才发现这个词把太多责任压进了一个盒子。这里是原 XMind 的网页化重绘版:它保留“CMake → 原生构建系统 → 二进制工具链”的主干,并把最容易被一句“MSVC 工具链”掩盖的内部组件全部展开。
.xmind 本质上是包含 JSON、资源与元数据的 ZIP 文档格式,浏览器没有原生渲染器。网页重绘能离线工作、响应主题、可搜索且无需把文件上传到第三方服务;原文件仍作为可编辑附件保留。
原来的三层模型——CMake、构建系统、工具链——是正确的,但最下面的“工具链”仍然太大。真正遇到二进制兼容问题时,必须继续拆。
读取 CMakeLists.txt,生成下一层认识的构建文件。
判断依赖和增量变化,按顺序调度编译、链接、复制、打包。
预处理、语义分析、优化并生成目标文件;驱动还负责拼装默认参数。
规定目标文件格式、调用约定、类布局、名字修饰、异常模型。
提供 std::vector、malloc、异常展开、启动代码与编译器辅助函数。
解析符号、重定位地址,把 .obj 与 .lib 组合为 EXE 或 DLL。
不要只问“这个库是不是 MSVC 编的”,而要分别问:目标架构、对象格式、ABI、标准库、CRT、编译选项和依赖是否兼容。
它是驱动程序:解析参数、选择 target、调用 Clang/LLVM 阶段并组织默认库。
语境中可能指编译器、ABI、VC Tools、STL、VCRuntime、链接器或整套 Visual Studio 环境。
必须继续区分 UCRT、VCRuntime、C++ 标准库以及 compiler-rt builtins。
CMakeLists.txt
│ CMake -G Ninja
▼
build.ninja
│ Ninja 调度
├── clang-cl /c a.cpp ──► a.obj
├── clang-cl /c b.cpp ──► b.obj
└── lld-link a.obj b.obj libc++.lib ... ──► app.exe
CMake 不编译代码,Ninja 也不理解 C++ 语义。CMake 生成规则,Ninja 执行规则,编译器生成目标文件,链接器才把它们组装起来。
把语言结构翻译成机器指令。例如看到函数调用时,生成“按照某种调用约定跳过去”的代码。
提供现成的函数和基础设施,例如内存分配、进程启动、异常支持和安全检查。
// 源代码
void* p = malloc(128);
// Clang 的任务(概念化)
// 1. 按 Microsoft x64 调用约定准备参数
// 2. 生成对 malloc 符号的调用
// 链接与运行时的任务
// 3. 从 Microsoft CRT 找到 malloc 的实现
// 4. 在程序运行时真正执行内存分配
编译器并不需要亲自实现 malloc。只要它生成的调用方式、符号和目标文件格式与运行时遵守同一份合同,就可以使用那个运行时。
API 是源码层面的接口,ABI 是编译以后仍必须一致的规则。ABI 通常包括:
| ABI 项目 | 不一致的结果 |
|---|---|
| 调用约定 | 参数读错位置、栈失衡或直接崩溃 |
| 名称修饰 | 链接器找不到函数,出现 unresolved external |
| 类与结构体布局 | 同一段内存被解释为不同字段 |
| 异常与 RTTI | 异常无法跨模块传播,类型识别失败 |
| 目标文件格式 | 链接器无法读取目标文件 |
Clang 在 Windows 的 MSVC target 下会生成 COFF,并努力兼容 Microsoft C++ ABI;clang-cl 还提供与 cl.exe 相似的命令行界面。这就是混用成为可能的基础。
x86_64-pc-windows-msvc
│ │ │ └── ABI / 环境:Microsoft
│ │ └────────── OS:Windows
│ └───────────── Vendor:pc(通常不是兼容判断重点)
└──────────────────── Architecture:x86-64
x86_64-w64-windows-gnu // 同样是 Windows,但属于 MinGW/GNU 环境
“都由 Clang 编译”或“都运行在 Windows”都不足以证明兼容。至少还需确定环境字段、对象格式、运行时选择以及库自身的 ABI 配置。
| 跨模块内容 | 主要契约 | 风险判断 |
|---|---|---|
extern "C" int f(int) | C 调用约定、整数宽度、导出名 | 较低,仍需核对架构和 calling convention |
| 固定布局 C 结构体 | 字段宽度、对齐、packing、版本 | 可控,建议携带 size/version |
| C++ 类、虚函数、RTTI | 完整 C++ ABI、编译器特性与布局 | 高 |
| 异常跨 DLL | 异常对象、unwind、handler、CRT | 高,需同生态验证 |
std::string/vector | 标准库实现、ABI namespace、allocator、配置宏 | 极高,不同 STL 不可直接互传 |
| 类别 | 提供什么 | 常见实现 | 典型符号 |
|---|---|---|---|
| C++ 标准库 | 容器、算法、字符串、流、线程抽象 | MSVC STL、libc++、libstdc++ | std::vector、std::string |
| C / 平台运行时 | 启动、堆、stdio、时间、locale、异常底座 | UCRT、VCRuntime、glibc、musl | malloc、printf |
| 编译器运行时 | 编译器生成的低层辅助操作、sanitizer 支撑 | compiler-rt、libgcc | 整数运算 helpers、builtins |
Clang 不等于 libc++。 Windows 上的 clang-cl 默认经常搭配 MSVC STL。更换编译器,并不自动更换 <string> 的实现。
| 组件 | 职责 | /MT 静态侧 | /MD 动态侧 |
|---|---|---|---|
| UCRT | ISO C、POSIX/Microsoft 扩展、stdio、heap 等通用 C 能力 | libucrt.lib | ucrt.lib → ucrtbase.dll |
| VCRuntime | 程序启动、C++ 异常、RTTI、运行时检查、编译器相关支撑 | libvcruntime.lib | vcruntime.lib → 版本化 DLL |
| MSVC STL | 微软的 C++ 标准库实现 | libcpmt.lib | msvcprt.lib → msvcp*.dll |
| compiler-rt | LLVM 可能隐式调用的 builtins、sanitizer 等编译器辅助能力 | clang_rt.*;它补充而不是替代 UCRT/VCRuntime | |
工程口语中的“MSVC runtime”有时泛指前三项,但排错时必须说出具体库名。尤其是 _CxxFrameHandler3 属于异常处理支撑,而 std::string 属于 C++ 标准库;它们不是同一层问题。
| 编译器 | C++ 标准库 | C / 平台运行时 | 说明 |
|---|---|---|---|
cl.exe | MSVC STL | UCRT + VCRuntime | Visual Studio 默认组合 |
clang-cl | MSVC STL | UCRT + VCRuntime | 常见组合,替换编译器前端与代码生成 |
clang-cl | libc++ | UCRT + VCRuntime | 可行,但必须明确控制头文件、库和 ABI 配置 |
| MinGW Clang/GCC | libstdc++ 或 libc++ | MinGW-w64 CRT | 属于另一套 Windows GNU 生态,不能只看“也是 Windows” |
切换四个维度,观察它们各自负责什么。结果是心智模型演示,不代替实际 ABI 验证。
这套交付包不是“Clang 使用整套 MSVC C++ 库”,而是把多家组件拼成一套满足 Chromium 二进制约束的组合。
build_client.bat
│
├── 编译器:clang-cl 17
├── 目标:Windows x86 / COFF / Microsoft ABI
├── C++ 标准库:Chromium 同版本 libc++
│ ABI namespace = std::__Cr
├── C / 平台运行时:Microsoft CRT,/MT
├── 编译器辅助运行时:clang_rt.builtins-i386.lib
└── 链接器:lld-link
│
▼
client.lib + client.dll
std::__Cr 很重要标准只规定源码里写 std::string,并不规定某一家标准库内部必须怎样布局。libc++ 默认使用类似 std::__1 的 inline ABI namespace,vendor 可在构建时通过 LIBCXX_ABI_NAMESPACE 定制。Chromium 的配置表现为 std::__Cr::basic_string;它改变名称修饰,用于隔离不同 libc++ 实例,但名字隔离并不会自动证明布局和配置一致。
还必须匹配 __config_site 中的 ABI flags。LLVM 明确指出,部分 ABI flags 会改变 basic_string、容器、智能指针等可观察类型的布局;这些 flags 应由 vendor 在构建 libc++ 时确定,而不是由使用者临时拼凑。
client.cpp
│ include libc++/string + Chromium __config_site
▼
clang-cl 17 ── 语义/模板实例化/LLVM CodeGen
│ └─ 生成 i686-pc-windows-msvc 的 COFF .obj
│
├─ 未定义 C++ 标准库符号 ───────► libc++.lib
├─ 未定义 C / heap / stdio 符号 ─► libucrt.lib (/MT)
├─ 未定义 EH / RTTI / startup ───► libvcruntime.lib (/MT)
├─ 未定义 LLVM builtins ─────────► clang_rt.builtins-i386.lib
└─ Win32 API 导入符号 ───────────► Windows SDK import libraries
│
▼
lld-link
│
▼
client.dll + client.lib
调用者按 Chromium 的 string 布局、内联实现和 ABI 配置生成代码。
被调用方按另一套 string 布局生成代码。名字、尺寸和成员语义都可能不同。
如果接口暴露 std::string、std::vector 或模板类型,错误组合有时在链接期报错,有时却会把问题拖到运行期,表现为释放内存时崩溃或静默破坏堆。
/MT 与 /MD 选择的不是编译器| 选项 | 含义 | 典型结果 | 关注点 |
|---|---|---|---|
/MT | 使用多线程静态 CRT | 运行时代码进入各自模块 | 部署简单,但跨模块分配/释放尤其要小心 |
/MD | 使用 DLL 版本 CRT | 模块通过导入库使用共享 CRT DLL | 模块应统一运行时版本和配置 |
/MTd / /MDd | 相应的 Debug 运行时 | 额外调试设施 | 不要和 Release 运行时随意混链 |
clang-cl /MT source.cpp // Clang 编译 + 静态 Microsoft CRT
clang-cl /MD source.cpp // Clang 编译 + DLL 版 Microsoft CRT
// /MT 并不等于“用 cl.exe 编译”
// /MD 也不等于“整个程序只有一个堆”
Microsoft 文档要求同一次链接中的模块使用一致的运行时选项。工程上还要进一步约束 DLL 边界的所有权,因为独立构建的 DLL、插件与第三方分配器仍可能形成不同的内存域。
// 推荐:C ABI + 明确所有权
extern "C" {
struct ClientResult {
int code;
const char* message;
};
ClientResult client_login(const char* username);
void client_release_message(const char* message);
}
// 高风险:跨 DLL 暴露标准库实现细节
std::string client_login(std::vector<User> users);
C ABI 不是绝对无风险,但它显著缩小了兼容面。仍需固定结构体对齐、整数宽度、字符编码、调用约定、版本字段和内存所有权。
CMake 的 Generator 决定生成哪种原生构建文件;编译器、运行时和具体参数是另一组选择。以 Ninja + clang-cl 为例:
# 首次配置构建目录时选择 Generator 和编译器
cmake -S . -B build -G Ninja \
-DCMAKE_C_COMPILER=clang-cl \
-DCMAKE_CXX_COMPILER=clang-cl
# CMakeLists.txt:选择 MSVC ABI 风格工具链下的 CRT 模式
set(CMAKE_MSVC_RUNTIME_LIBRARY
"MultiThreaded$<$<CONFIG:Debug>:Debug>")
# 使用非默认 libc++ 时,最好封装成专门的 toolchain/preset,
# 集中管理 include 顺序、库目录、ABI 宏、默认库抑制和链接依赖。
CMAKE_CXX_COMPILER 在一个构建树首次配置后便成为缓存的一部分。更换编译器、架构或关键运行时设置时,使用独立构建目录比在旧缓存上覆盖更可靠。
Build Configuration = {
generator: Ninja,
compiler: clang-cl 17,
target: i686-pc-windows-msvc,
object_format: COFF,
cxx_abi: Microsoft ABI,
cxx_library: Chromium libc++ / std::__Cr,
c_runtime: Microsoft CRT static (/MT),
compiler_rt: compiler-rt builtins,
linker: lld-link,
sdk: Windows SDK,
flags_and_macros: exactly matched
}
在 MSVC 风格工具链中,编译器驱动与头文件可能通过对象文件的 .drectve 段写入 /DEFAULTLIB 指令,链接器据此自动选择库。使用 /NODEFAULTLIB、自定义 libc++ 或手写链接命令时,相当于接管这套责任,必须显式补齐 UCRT、VCRuntime、builtins、系统导入库及正确顺序。
普通 COFF 对象能被多个兼容链接器读取,不代表 LLVM bitcode/LTO 产物可被任意版本工具处理。启用 ThinLTO/LTO 后,还要匹配 LLVM bitcode 版本、链接器插件和优化配置。
/showIncludes 检查 <string> 究竟来自 libc++ 还是 Visual Studio。dumpbin /symbols、llvm-nm 可帮助识别 std::__Cr、MSVC 名字修饰和缺失符号。/VERBOSE:LIB 可以看到链接器为何选中某个库、符号由谁提供。_CxxFrameHandler3它属于 Microsoft C++ 异常处理运行时相关符号。出现 unresolved external 往往意味着相应 VCRuntime/default library 没有正确进入链接,不能简单解释成“因为使用了 MSVC 运行时”。要结合链接命令与 /VERBOSE:LIB 判断。
| 概念 | 一句话结论 |
|---|---|
| CMake | 生成并调度构建,不亲自编译 C++。 |
| Generator | 决定生成 Ninja、Make、Visual Studio 等哪一种原生构建描述。 |
| clang-cl | Clang 的 cl.exe 兼容驱动,面向 Windows/MSVC 生态。 |
| MSVC ABI | Clang 与 MSVC 二进制互操作所遵守的关键合同。 |
| MSVC STL | 微软的 C++ 标准库实现,不等于 MSVC 编译器,也不等于 CRT。 |
| libc++ | LLVM 的 C++ 标准库实现;换用它需要同时控制头文件和链接库。 |
| UCRT / VCRuntime | Microsoft 的 C 与平台运行时组成部分,可被 clang-cl 使用。 |
| compiler-rt | LLVM 的编译器辅助运行时;可与平台运行时共同出现。 |
/MT | 选择静态 Microsoft CRT,不代表使用 cl.exe。 |
/MD | 选择 DLL 版 Microsoft CRT,不等于自动解决所有跨 DLL 内存问题。 |
std::__Cr | 当前 Chromium libc++ 配置的 ABI 特征,提醒头、库、宏必须成套匹配。 |
| 最稳 DLL 接口 | C ABI、固定宽度类型、显式版本与“谁分配谁释放”。 |