ABI(Application Binary Interface,应用二进制接口)是二进制层面的契约。它用于处理已经编译好的目标文件、静态库、动态库,之间如何互相调用、传递数据。
ABI 规定了什么
- 调用约定(calling convention):参数和返回值放寄存器还是栈、顺序如何、谁负责清栈(x86 的
cdecl/stdcall/fastcall,x64 的 System V AMD64 / Microsoft x64) - 名字修饰(name mangling):同一个函数在不同 ABI 下会有不同的符号名
- Itanium(GCC / Clang):
void foo(int)→_Z3fooi - MSVC:
void foo(int)→?foo@@YAXH@Z
- Itanium(GCC / Clang):
- 对象布局:成员顺序、填充字节、基类子对象位置、
vptr位置(见 类的内存占用) - 类型大小与对齐:
sizeof/alignof、位域布局 - 虚函数表布局:虚函数的槽位顺序,决定
virtual调用跳到哪里(见 多态) - 异常与 RTTI:栈展开表、
type_info的结构 - 动态链接细节:符号可见性、SONAME、符号版本
- 文件格式:ELF(Linux)、PE/COFF(Windows)、Mach-O(macOS)
| 对比维度 | API(源码契约) | ABI(二进制契约) |
|---|---|---|
| 内容 | 头文件里的声明、类定义、文档里的语义约定 | 符号名、调用约定、对象布局、vtable、异常、动态链接 |
| 阶段 | 编译期 | 链接期 + 运行期 |
ABI 兼容是什么意思
ABI 兼容(binary compatible)指的是:用旧版本编译出来的程序,不重新编译,直接换成新版本的库也能正常运行。
- 保证源码之间仍旧能通过编译的是 API 兼容,保证旧的二进制文件能直接用的才是 ABI 兼容
- 动态库发布新版本时,ABI 兼容才能让用户直接替换
.so/.dll,否则只能重新编译整个程序
谁定义 ABI
C++ 标准不规定 ABI,只规定语义。具体二进制长什么样由实现决定:
| 平台 / 编译器 | ABI |
|---|---|
| GCC / Clang(Linux、macOS) | Itanium C++ ABI |
| MSVC(Windows) | Microsoft C++ ABI |
| 标准库 | libstdc++ / libc++ / MSVC STL 各自实现,布局可能不同 |
因此同一份源码用 GCC 和 MSVC 编出来的二进制不能互相链接,甚至同一编译器换了标准库(libstdc++/libc++)也不行。
C 语言本身没有 name mangling,调用约定按平台固定,因此跨语言调用时经常用 C ABI 当作标准。
怎么保持 ABI 稳定
- pImpl(编译防火墙):头文件里只放一个指向实现的指针,成员全藏在
.cpp里;改实现不动头文件,对象大小也不变 - 纯 C 接口:
extern "C"关掉 name mangling,调用约定按平台固定;配合不透明指针(typedef struct Foo Foo;)把实现藏起来(见 extern “C”) - 预留槽位:接口里留空的虚函数或私有成员,新功能往里填
- 符号版本化:glibc 用
.symver给同一符号保留多个版本(如GLIBC_2.2.5),旧程序仍绑到旧实现 - SONAME 升级:ABI 破坏时把
libfoo.so.1升成libfoo.so.2,让包管理器同时装两个版本 abi_tag:GCC / Clang 的__attribute__((abi_tag("v2")))会把标签编进符号名,避免新旧 ABI 悄悄混用
工具
nm -D --defined-only libfoo.so | c++filt # 看导出的符号
readelf -sW libfoo.so # 符号表 / 版本信息
objdump -T libfoo.so # 动态符号表
abi-dumper libfoo.so -o foo.abi # 导出 ABI 描述
abi-compliance-checker -l foo -old old.abi -new new.abi
abidiff old.so new.so # libabigail 的 ABI diff
GCC 的 -fabi-version=n 可以指定遵循哪一版 Itanium ABI。


