Published on

Cpp学习(1)-引子

Authors
  • avatar
    Name
    Xiaolong Peng
    Twitter

📖 English version: Cpp Learning (1): Prelude (EN)

引子

一次关于 Kotlin/Native、JNI、Node-API、方舟编译器与 LLDB 调试器的讨论,最终都指向同一个问题:

当 C++ 必须与外部世界对话时,稳定边界在哪里?

答案几乎总是 C ABI——它是跨编译器、跨平台、跨语言的最低公共分母。本文整理成一份 C++ 底层技术全景笔记。


一、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++ 特性不 portablestd::function、lambda、成员函数、重载、name mangling 互不兼容
外部语言无 C++ 运行时Java/Kotlin/JS/Python 无法直接构造或析构 C++ 对象
FFI 与 C ABI 桥梁

不透明句柄(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++ 对象指针?

  1. C 语言无法调用构造函数 / 析构函数
  2. 内存布局(虚表、成员顺序)非标准
  3. 垃圾回收或对象移动可能导致指针失效(如 Kotlin/Native 的 ARC)

三、C++ 类型系统与 ABI 兼容性

POD 与非 POD

类型特征跨 ABI 用法
POD / 标准布局与 C 内存布局兼容可安全按值传递
非 POD含虚函数、非平凡构造/析构、引用成员等必须通过句柄操作
POD 与句柄选择决策

调用约定(Calling Convention)

约定平台栈清理者典型用途
__cdeclWindows调用方C 默认
__stdcallWindows被调用方Win32 API
__thiscallWindows被调用方(this 在 ECX)C++ 成员函数
System V AMD64Linux / 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 资源管理流程

在交叉编译环境下 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)

  1. 执行 0xCC → 触发 #BP 异常 → 内核 → Linux 上表现为 SIGTRAP
  2. 断点命中后:恢复原始指令 → 单步执行 → 重新插入 0xCC
软件断点工作流程

ptrace 系统调用

ptrace 用于进程跟踪,本身不是信号。常见能力:

  • 附着 / 分离目标进程
  • 读写内存与寄存器
  • 单步、继续执行

LLDB / GDB 在 Linux 上大量依赖 ptrace 实现断点、内存访问与线程控制。

LLDB 中的 Destroy 技术

LLDB 对象销毁流程
  • RAII + shared_ptr 自动管理对象生命周期
  • 显式 Destroy() 处理平台相关资源(如 ptrace(PTRACE_KILL)
  • 观察者模式广播销毁事件,避免悬挂引用

七、交叉编译与嵌入式「局部 C」

交叉编译与嵌入式局部 C

交叉编译(Cross Compile)

在一个平台(如 x86_64 开发机)上,生成另一个平台(ARM 裸机、RTOS)的可执行代码。

什么是「局部 C」?

.cpp 文件中仅使用 C 风格语法和库printfmalloc),但利用 C++ 的强类型检查与 namespace 组织代码。

常见于裸机 / RTOS 项目,配合:

  • 禁用全局构造
  • 禁用异常与 RTTI
  • 手动 CRT / 堆初始化

八、运行时基础:CRT 与 RTTI

概念作用嵌入式常用选择
CRT 初始化main 前设置堆、标准 I/O、全局对象构造禁用(-nostdlib),手动初始化
RTTItypeiddynamic_cast 识别类型禁用(-fno-rtti),节省空间
异常 unwind栈展开与清理禁用(-fno-exceptions

九、技术延伸:相邻概念地图

把本文概念放到更大的 C++ 系统编程图景中:

本文概念延伸方向一句话
C ABIItanium C++ ABIC++ 名字修饰、vtable 布局的工业标准
不透明句柄COM IUnknown、OpenSSL EVP_*30 年来最稳的跨模块封装手法
POD / 布局std::is_standard_layoutC++11 起可用 trait 静态检查
RAIIScope Guard / defer 模式Go 的 defer 思想源于此
ptraceperfstrace、eBPF同一套「观测执行」谱系
软件断点硬件断点 DR0–DR3数量有限,适合关键地址
交叉编译CMake Toolchain File现代工程的标准做法
Node-APIN-API 文档保证 ABI 稳定的 Node 原生扩展接口
JNIJNI 规范JVM 与 Native 的标准桥梁

十、相关链接与拓展阅读

官方文档与规范

推荐书籍

书名重点
The C++ Programming Language (4th Ed.)C++ 完整特性,尤其是 C 子集章节
Effective Modern C++现代 C++ 最佳实践,区分 C 与 C++ 边界
Linkers and LoadersABI、符号解析、动态链接
21st Century CC 语言现代实践,与 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 篇,由一次跨语言底层话题讨论整理而来。

英文版:Cpp Learning (1): Prelude (EN)