Contents

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 的更新而更新,那么可能是安全的。

哪些做法多半是安全的

  • 增加新的class

  • 增加non-virtual 成员函数或static 成员函数

  • 修改数据成员的名称,因为生产的二进制代码是按偏移量来访问的,当然,这会造成源码级的不兼容。

解决办法

  • 采用静态链接
  • 通过动态库的版本管理来控制兼容性
  • 用pimpl 技法,编译器防火墙

动态库的接口的推荐做法

  • 暴露的接口里边不要有虚函数,而且

    1
    
    sizeof(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 把可执行文件和动态库链接到一起。