- Published on
Cpp学习(1)-引子
- Authors
- Name
- Xiaolong Peng
📖 English version: Cpp Learning (1): Prelude (EN)
引子
一次关于 Kotlin/Native、JNI、Node-API、方舟编译器与 LLDB 调试器的讨论,最终都指向同一个问题:
当 C++ 必须与外部世界对话时,稳定边界在哪里?
答案几乎总是 C ABI——它是跨编译器、跨平台、跨语言的最低公共分母。本文整理成一份 C++ 底层技术全景笔记。
一、C++ 底层技术全景
读图要点(自上而下):
- 应用层:各种跨语言 FFI 绑定(Kotlin/Native、JNI、Node-API 等)
- C ABI 稳定层:所有跨语言交互必须经过这里——
extern "C"、不透明句柄、POD 结构体 - C++ 实现层:可自由使用 RAII、虚函数、模板,但对外不可见
- 运行时与系统层:ptrace、信号、CRT、断点指令
0xCC、调用约定等 - 硬件 / OS 层:最底层的执行模型与系统资源抽象
二、跨语言 FFI 与 C ABI
为什么 FFI 必须使用 C 调用约定?
| 原因 | 说明 |
|---|---|
| C ABI 稳定 | 跨编译器、跨平台的最低公共分母 |
| C++ 特性不 portable | std::function、lambda、成员函数、重载、name mangling 互不兼容 |
| 外部语言无 C++ 运行时 | Java/Kotlin/JS/Python 无法直接构造或析构 C++ 对象 |
不透明句柄(Opaque Handle)
// C 头文件 —— 对外只暴露指针类型,不暴露布局
typedef struct MyState* MyHandle;
MyHandle create(void);
void destroy(MyHandle h);
void do_work(MyHandle h, int arg);
- 句柄可以是
void*或前向声明的结构体指针 - 所有操作通过 C 接口函数进行,内部再转换为 C++ 对象调用
- 用于封装有状态的 C++ 对象,隐藏内部布局
为什么不能直接暴露 C++ 对象指针?
- C 语言无法调用构造函数 / 析构函数
- 内存布局(虚表、成员顺序)非标准
- 垃圾回收或对象移动可能导致指针失效(如 Kotlin/Native 的 ARC)
三、C++ 类型系统与 ABI 兼容性
POD 与非 POD
| 类型 | 特征 | 跨 ABI 用法 |
|---|---|---|
| POD / 标准布局 | 与 C 内存布局兼容 | 可安全按值传递 |
| 非 POD | 含虚函数、非平凡构造/析构、引用成员等 | 必须通过句柄操作 |
调用约定(Calling Convention)
| 约定 | 平台 | 栈清理者 | 典型用途 |
|---|---|---|---|
__cdecl | Windows | 调用方 | C 默认 |
__stdcall | Windows | 被调用方 | Win32 API |
__thiscall | Windows | 被调用方(this 在 ECX) | C++ 成员函数 |
| System V AMD64 | Linux / macOS | 调用方(参数多用寄存器) | 统一约定 |
Linux / macOS 上通常无需显式关键字,ABI 已由平台规范固定。
四、C ABI 接口实现示例
头文件 myapi.h:
#ifdef __cplusplus
extern "C" {
#endif
typedef struct MyHandle* MyHandle;
MyHandle my_create(void);
void my_destroy(MyHandle h);
int my_do(MyHandle h, int x);
#ifdef __cplusplus
}
#endif
实现 myapi.cpp:
#include "myapi.h"
class MyImpl {
public:
int work(int x) { return x * 2; }
};
struct MyHandle {
MyImpl impl;
};
extern "C" {
MyHandle my_create() { return new MyHandle; }
void my_destroy(MyHandle h) { delete h; }
int my_do(MyHandle h, int x) { return h->impl.work(x); }
}
这是 Node-API、JNI、pybind11 等绑定层背后反复出现的模式:C 面向上层,C++ 面向实现。
五、资源管理与 RAII
RAII 核心思想
Resource Acquisition Is Initialization——构造函数获取资源,析构函数释放资源。
在交叉编译环境下 RAII 仍然有效,前提是目标平台的 C++ 运行时与编译器匹配。
Windows HANDLE 的 RAII 封装
struct HandleDeleter {
void operator()(HANDLE h) const {
if (h) CloseHandle(h);
}
};
using UniqueHandle = std::unique_ptr<void, HandleDeleter>;
嵌入式系统中常禁用的特性
| 特性 | 编译选项 | 原因 |
|---|---|---|
| 异常 | -fno-exceptions | 代码体积、确定性、无 unwind 表 |
| RTTI | -fno-rtti | 节省空间,禁用 dynamic_cast |
| 全局对象构造 | -nostdlib + 手动初始化 | 避免 CRT 依赖 |
六、调试器底层机制
软件断点原理
CPU 执行到目标地址时,调试器已将第一个字节替换为 0xCC(INT 3):
- 执行
0xCC→ 触发#BP异常 → 内核 → Linux 上表现为 SIGTRAP - 断点命中后:恢复原始指令 → 单步执行 → 重新插入
0xCC
ptrace 系统调用
ptrace 用于进程跟踪,本身不是信号。常见能力:
- 附着 / 分离目标进程
- 读写内存与寄存器
- 单步、继续执行
LLDB / GDB 在 Linux 上大量依赖 ptrace 实现断点、内存访问与线程控制。
LLDB 中的 Destroy 技术
- RAII +
shared_ptr自动管理对象生命周期 - 显式
Destroy()处理平台相关资源(如ptrace(PTRACE_KILL)) - 观察者模式广播销毁事件,避免悬挂引用
七、交叉编译与嵌入式「局部 C」
交叉编译(Cross Compile)
在一个平台(如 x86_64 开发机)上,生成另一个平台(ARM 裸机、RTOS)的可执行代码。
什么是「局部 C」?
在 .cpp 文件中仅使用 C 风格语法和库(printf、malloc),但利用 C++ 的强类型检查与 namespace 组织代码。
常见于裸机 / RTOS 项目,配合:
- 禁用全局构造
- 禁用异常与 RTTI
- 手动 CRT / 堆初始化
八、运行时基础:CRT 与 RTTI
| 概念 | 作用 | 嵌入式常用选择 |
|---|---|---|
| CRT 初始化 | main 前设置堆、标准 I/O、全局对象构造 | 禁用(-nostdlib),手动初始化 |
| RTTI | typeid、dynamic_cast 识别类型 | 禁用(-fno-rtti),节省空间 |
| 异常 unwind | 栈展开与清理 | 禁用(-fno-exceptions) |
九、技术延伸:相邻概念地图
把本文概念放到更大的 C++ 系统编程图景中:
| 本文概念 | 延伸方向 | 一句话 |
|---|---|---|
| C ABI | Itanium C++ ABI | C++ 名字修饰、vtable 布局的工业标准 |
| 不透明句柄 | COM IUnknown、OpenSSL EVP_* | 30 年来最稳的跨模块封装手法 |
| POD / 布局 | std::is_standard_layout | C++11 起可用 trait 静态检查 |
| RAII | Scope Guard / defer 模式 | Go 的 defer 思想源于此 |
| ptrace | perf、strace、eBPF | 同一套「观测执行」谱系 |
| 软件断点 | 硬件断点 DR0–DR3 | 数量有限,适合关键地址 |
| 交叉编译 | CMake Toolchain File | 现代工程的标准做法 |
| Node-API | N-API 文档 | 保证 ABI 稳定的 Node 原生扩展接口 |
| JNI | JNI 规范 | JVM 与 Native 的标准桥梁 |
十、相关链接与拓展阅读
官方文档与规范
- Itanium C++ ABI — C++ 底层 ABI 参考
- JNI Specification — Java 本地接口
- LLDB 源码与文档 — 调试器实现参考
- Linux ptrace man page — 进程跟踪系统调用
- System V AMD64 ABI — x86-64 调用约定
推荐书籍
| 书名 | 重点 |
|---|---|
| The C++ Programming Language (4th Ed.) | C++ 完整特性,尤其是 C 子集章节 |
| Effective Modern C++ | 现代 C++ 最佳实践,区分 C 与 C++ 边界 |
| Linkers and Loaders | ABI、符号解析、动态链接 |
| 21st Century C | C 语言现代实践,与 C++ 对比 |
| Computer Systems: A Programmer's Perspective (CSAPP) | 程序在机器上的真实行为 |
| Debugging with GDB | 调试器原理与命令(LLDB 概念相通) |
进阶话题(准备后续可写 Cpp学习(2))
- 动态链接:PLT/GOT、延迟绑定、
dlopen与插件架构 - 内存模型:
memory_order、无锁编程与 FFI 边界的线程安全 - Sanitizer 家族:ASan / TSan / UBSan 如何与调试器协作
- wasm / emscripten:又一种「C ABI 作为边界」的跨平台方案
小结
C++ 系统编程的核心张力可以概括为:
对内用 C++ 表达力,对外用 C ABI 保稳定。
不透明句柄是桥梁,RAII 是纪律,POD 是契约,ptrace 是观测窗口。掌握这套「全景图」,再去看 Kotlin/Native、Node-API 或 LLDB 源码,许多设计就不再是魔法,而是同一套边界策略的不同实例。
本文是「Cpp学习」系列第 1 篇,由一次跨语言底层话题讨论整理而来。