ABI (application binary interface)
C++ ABI 的主要内容:
-
函数参数传递的方式,比如x86-64 用寄存器来传函数的前4 个整数参数
-
虚函数的调用方式,通常是vptr/vtbl 然后用vtbl[offset] 来调用
-
struct 和class 的内存布局,通过偏移量来访问数据成员
-
name mangling
-
RTTI 和异常处理的实现
一些源代码兼容但是二进制代码不兼容例子:
-
给函数增加默认参数,现有的可执行文件无法传这个额外的参数。
-
增加虚函数,会造成vtbl 里的排列变化。(不要考虑“只在末尾增加”这种取巧行为,因为你的class 可能已被继承。)
-
增加默认模板类型参数,比方说Foo
改为Foo<T, Alloc=alloc >,这会改变name mangling -
改变enum 的值,把enum Color { Red = 3 }; 改为Red = 4。这会造成错位。当然,由于enum 自动排列取值,添加enum 项也是不安全的,在末尾添加除外。
-
给class Bar 增加数据成员,造成sizeof(Bar) 变大,以及内部数据成员的offset 变化,这是不是安全的?通常不是安全的,但也有例外。
- 如果客户代码里有new Bar,那么肯定不安全,因为new 的字节数不够装下新Bar 对象。相反,如果library 通过factory 返回Bar* (并通过factory 来销毁对象)或者直接返回shared_ptr
,客户端不需要用到sizeof(Bar),那么可能是安全的。 - 如果客户代码里有Bar* pBar; pBar->memberA = xx;,那么肯定不安全,因为memberA 的新Bar 的偏移可能会变。相反,如果只通过成员函数来访问对象的数据成员,客户端不需要用到data member 的offsets,那么可能是安全的。
- 如果客户调用pBar->setMemberA(xx); 而Bar::setMemberA() 是个inline function,那么肯定不安全,因为偏移量已经被inline 到客户的二进制代码里了。如果setMemberA() 是outline function,其实现位于shared library 中,会随着Bar 的更新而更新,那么可能是安全的。
- 如果客户代码里有new Bar,那么肯定不安全,因为new 的字节数不够装下新Bar 对象。相反,如果library 通过factory 返回Bar* (并通过factory 来销毁对象)或者直接返回shared_ptr
哪些做法多半是安全的
-
增加新的class
-
增加non-virtual 成员函数或static 成员函数
-
修改数据成员的名称,因为生产的二进制代码是按偏移量来访问的,当然,这会造成源码级的不兼容。
解决办法
- 采用静态链接
- 通过动态库的版本管理来控制兼容性
- 用pimpl 技法,编译器防火墙
动态库的接口的推荐做法
-
暴露的接口里边不要有虚函数,而且
1sizeof(Graphics) == sizeof(Graphics::Impl*) -
在库的实现中把调用转发(forward) 给实现Graphics::Impl ,这部分代码位于.so/.dll 中,随库的升级一起变
-
如果要加入新的功能,不必通过继承来扩展,可以原地修改,且很容易保持二进制兼容性
为什么non-virtual 函数比virtual 函数更健壮?
因为virtual function 是bindby-vtable-offset,而non-virtual function 是bind-by-name。加载器(loader) 会在程序启动时做决议(resolution),通过mangled name 把可执行文件和动态库链接到一起。