Swift库二进制接口(ABI)兼容性研究

前言

阿里云APP组件化过程中,我们拆分出了若干基础组件库和业务代码库,由于代码是采用Swift编写的,所以这些库都是动态库形式。在上一个正式版本,组件化达到了完全形态,主工程只剩下一个壳,所有代码都以pod组件的形式引入,最后交付的是一个主工程的壳二进制程序,和十几个动态库Framework。这些动态库都是运行时才链接到主程序中的。

开发中,我们在某个组件上工作时,其他组件也都是以动态库二进制形式引入的,节省Rebuild源码成本。但开发过程中我们遇到了一些问题。

遇到问题

组件拆分出来以后,无可避免,组件之间会存在一些非扁平的依赖关系,如A依赖B依赖C,同时A依赖C。开发A过程中,可能有需要改动C的情况,于是我们将C更新、打包、发布,而B保持不变(只有A用到C加的特性),A依赖C的新版本继续开发。但我们发现,有一些改动会导致APP闪退,只有在B重新依赖C的新版本也编译、打包、发布新版本之后,才恢复正常。我们注意到,出现这种情况时,一般是我们改动了A中某些类的属性。显而易见,我们遇到的是二进制兼容性问题

理论上ObjC使用静态库也会遇到这种问题的,但是ObjC2.0引入了non-fragile特性,同时runtime中采用消息转发实现方法调用,规避了大部分会引起ABI不兼容的情况。所以在ObjC中,只要接口兼容,底层组件改动一般是不需要自下往上重新编译链接的。

而Swift,在我们认识里,它没有ObjC那么“动态”,又不像C/C++那么直接(寻址是基地址+偏移量,内存布局改变引起ABI不兼容),那它会存在哪些能引起ABI不兼容的情况呢?上个版本我们没有弄明白,保险起见,底层改动后我们都把上层重新编译连接,非常蛋疼。所以,这两天我尝试把这个问题弄清楚。

复现实例

我们准备三个代码文件:

// Foo.swift

public class SimpleClass {

    public var x: Int = 0
    public var y: Int = 0

    public init() {
        x = 1
        y = 2
    }

    public func sum() -> Int {
        return x + y
    }
}
// Bar.swift

import Foo

public func bar() -> Int {
    return SimpleClass().sum()
}
// main.swift

import Foundation
import Foo
import Bar

print("bar \(bar())")

然后分别编译出动态库,链接到main,运行:

$ swiftc -emit-module -emit-library Foo.swift
$ swiftc -emit-module -emit-library Bar.swift -I. -L. -lFoo
$ swiftc -I. -L. -lFoo -lBar main.swift
$ ./main
bar 3 // output

然后我们给Foo.swift增加一个字段diff:

// Foo.swift

public class SimpleClass {

    public var diff: Int?
    public var x: Int = 0
    public var y: Int = 0

    public init() {
        x = 1
        y = 2
    }

    public func sum() -> Int {
        return x + y
    }
}

单独编译Foo.swiftBar.swiftmain.swift不再重新编译:

$ swiftc -emit-module -emit-library Foo.swift
$ ./main
[1]    52482 segmentation fault  ./main  // output
$ swiftc -I. -L. -lFoo -lBar main.swift
[1]    52645 segmentation fault  ./main  // output

可以看到,无论是否重新编译main,只要不重新编译Bar,运行main就会闪退。

排查

我们没有挑选一些在C/C++里容易理解的ABI不兼容的场景,如直接引用发生变化的对象成员变量,继承关系下父类字段增删等。上述复现实例中,我们在SimpleClass这个类中增加了字段,改变了对象的内存布局,但是,按照C/C++的思路,我们并未在Bar中直接引用任何成员变量,为什么会出现错误呢?我们也已经知道,Swift方法调用依赖vtable,但我们也并未对实例方法做增删。

还是要从内存布局说起。

对象内存布局

Swift对象的内存布局我也是这两天慢慢明白过来的,现在放在最前头,后边说起来就容易理解了。在上一次我为了实现HandyJSON做这方面调研的时候,这一块资料很少,但现在情况不同了。特别推荐Mike Ash的一个分享视频,他的思路很棒,主要依赖mach_vm_read_overwritedladder两个系统函数。前者利用系统调用检查一个指针是否合法,后者检查一个内存地址上是否绑定了什么symbol。基于此,从一个对象的起始指针出发,广度遍历一下可见的内存,构造指针继续往下,最后将得到和这个对象相关的所有地址、所有符号,把这些信息生成一张图,最后大概是这个样子(这个是简单情况):

Paste_Image.png

从这个视频,我们知道,普通Swift对象,即使是纯Swift对象,其实instance的布局也是和ObjC保持一致,大概是这样:

Paste_Image.png

我们打开Swift ABI文档。在Class Metadata这一章节,我们看到,在ObjC中,isa指向一个Class类对象,Swift类似,但指向的是一个与Class类兼容的metadata,这个metadata用Class *去解释是没有问题的,同时它尾部追加了Swift中使用的一些字段。比如vtablevtable相关的资料,可以参考:Swift Method Dispatching

我们直接将Foo.swift编译为汇编代码(swiftc -S Foo.swift):

525 __TWoFC3Foo11SimpleClass3sumfT_Si:
526     .quad   152
527
528     .globl  __TMLC3Foo11SimpleClass
529 .zerofill __DATA,__common,__TMLC3Foo11SimpleClass,8,3
530     .section    __DATA,__data
531     .align  3
532 __TMfC3Foo11SimpleClass:
533     .quad   __TFC3Foo11SimpleClassD
534     .quad   __TWVBo
535     .quad   __TMmC3Foo11SimpleClass  // isa指向的位置
536     .quad   _OBJC_CLASS_$_SwiftObject
537     .quad   __objc_empty_cache
538     .quad   0
539     .quad   l__DATA__TtC3Foo11SimpleClass+1
540     .long   3
541     .long   0
542     .long   32
543     .short  7
544     .short  0
545     .long   176
546     .long   16
547     .quad   __TMnC3Foo11SimpleClass-(__TMfC3Foo11SimpleClass+80)
548     .quad   0
549     .quad   __TFC3Foo11SimpleClassg1xSi  // vTable起点
550     .quad   __TFC3Foo11SimpleClasss1xSi
551     .quad   __TFC3Foo11SimpleClassm1xSi
552     .quad   __TFC3Foo11SimpleClassg1ySi
553     .quad   __TFC3Foo11SimpleClasss1ySi
554     .quad   __TFC3Foo11SimpleClassm1ySi
555     .quad   __TFC3Foo11SimpleClasscfT_S0_
556     .quad   __TFC3Foo11SimpleClass3sumfT_Si  // sum方法
557     .quad   16
558     .quad   24
559
560     .section    __TEXT,__swift3_typeref,regular,no_dead_strip
561     .align  4
562 L___unnamed_7:
563     .asciz  "C3Foo11SimpleClass"

然后demangle相关的几个符号:

$ xcrun swift-demangle *

output:

_TMmC3Foo11SimpleClass ---> metaclass for Foo.SimpleClass
_TMfC3Foo11SimpleClass ---> full type metadata for Foo.SimpleClass
_TFC3Foo11SimpleClassD ---> Foo.SimpleClass.__deallocating_deinit
_TMmC3Foo11SimpleClass ---> metaclass for Foo.SimpleClass
_TMnC3Foo11SimpleClass ---> nominal type descriptor for Foo.SimpleClass
_TMfC3Foo11SimpleClass ---> full type metadata for Foo.SimpleClass
_TFC3Foo11SimpleClassg1xSi ---> Foo.SimpleClass.x.getter : Swift.Int
_TFC3Foo11SimpleClasss1xSi ---> Foo.SimpleClass.x.setter : Swift.Int
_TFC3Foo11SimpleClassm1xSi ---> Foo.SimpleClass.x.materializeForSet : Swift.Int
_TFC3Foo11SimpleClass3sumfT_Si ---> Foo.SimpleClass.sum () -> Swift.Int

很棒,一切符合预期。

调用细节

然后我们看一下Bar.swift中发生了什么(swiftc -I. -L. -lFoo -S Bar.swift):

 38 __TF3Bar3barFT_Si:
 39     .cfi_startproc
 40     pushq   %rbp
 41 Ltmp3:
 42     .cfi_def_cfa_offset 16
 43 Ltmp4:
 44     .cfi_offset %rbp, -16
 45     movq    %rsp, %rbp
 46 Ltmp5:
 47     .cfi_def_cfa_register %rbp
 48     subq    $32, %rsp
 49     callq   __TMaC3Foo11SimpleClass
 50     movq    %rax, %rdi
 51     callq   __TFC3Foo11SimpleClassCfT_S0_  // 构造SimpleClass实例
 52     movq    (%rax), %rdi
 53     movq    %rdi, -8(%rbp)
 54     movq    %rax, %rdi
 55     movq    -8(%rbp), %rcx
 56     movq    %rax, -16(%rbp)
 57     callq   *136(%rcx)  // 调用sum方法
 58     movq    -16(%rbp), %rdi
 59     movq    %rax, -24(%rbp)
 60     callq   _rt_swift_release
 61     movq    -24(%rbp), %rax
 62     addq    $32, %rsp
 63     popq    %rbp
 64     retq
 65     .cfi_endproc

跳过细节,我们直接看line 57: callq *136(%rcx)行,这正是对sum方法的调用。rcx寄存器存储的值为SimpleClass.metadata的起始指针,偏移136字节后,便是上述中_TFC3Foo11SimpleClass3sumfT_Si ---> Foo.SimpleClass.sum () -> Swift.Int符号。

引起不兼容的原因

这时候我们就可以考虑一下,在复现实例中,我们加入diff字段发生的事情了。加入diff字段后重新编译Foo.swift,可以看到汇编代码中metadata布局变成了:

671     .quad   __TMnC3Foo11SimpleClass-(__TMfC3Foo11SimpleClass+80)
672     .quad   0
673     .quad   __TFC3Foo11SimpleClassg4diffGSqSi_
674     .quad   __TFC3Foo11SimpleClasss4diffGSqSi_
675     .quad   __TFC3Foo11SimpleClassm4diffGSqSi_
676     .quad   __TFC3Foo11SimpleClassg1xSi
677     .quad   __TFC3Foo11SimpleClasss1xSi
678     .quad   __TFC3Foo11SimpleClassm1xSi
679     .quad   __TFC3Foo11SimpleClassg1ySi
680     .quad   __TFC3Foo11SimpleClasss1ySi
681     .quad   __TFC3Foo11SimpleClassm1ySi
682     .quad   __TFC3Foo11SimpleClasscfT_S0_
683     .quad   __TFC3Foo11SimpleClass3sumfT_Si

也就是说,Swift为新增的字段添加了三个方法,插在vtable其他方法的前面。这时候,__TFC3Foo11SimpleClass3sumfT_Si的位置就变了,而Bar中仍然以callq *136(%rcx)调用sum函数,自然就发生了错误,导致闪退。

其他情况

上述例子主要是展示了一种研究ABI兼容性的思路。据此,我们可以研究其他情况。

Struct

Swift中,Struct不允许继承,所以它的方法派发不必要依赖vtable实现,而是直接被编译为全局函数(未研究可见修饰符的影响)。那它还会遇到Class中遇到的问题吗?测试结果是,仍然会。我们把SimpleClass修改为SimpleStruct,同时直接在xy字段之间增加diff字段方便印证逻辑,看下Bar中的变化:

// Foo.swift节选
public struct SimpleStruct {
    public var x: Int = 0
    public var diff: Int = 0
    public var y: Int = 0
...
}
// foo.s节选
 63 __TFV3Foo12SimpleStruct3sumfT_Si:
 64     .cfi_startproc
 65     pushq   %rbp
 66 Ltmp6:
 67     .cfi_def_cfa_offset 16
 68 Ltmp7:
 69     .cfi_offset %rbp, -16
 70     movq    %rsp, %rbp
 71 Ltmp8:
 72     .cfi_def_cfa_register %rbp
 73     addq    %rdx, %rdi  // .x + .y
 74     seto    %al
 75     movq    %rsi, -8(%rbp)
 76     movq    %rdi, -16(%rbp)
 77     movb    %al, -17(%rbp)
 78     jo  LBB2_2
 79     movq    -16(%rbp), %rax
 80     popq    %rbp
 81     retq
 82 LBB2_2:
 83     ud2
 84     .cfi_endproc
// bar.s节选
 46 Ltmp5:
 47     .cfi_def_cfa_register %rbp
 48     callq   __TFV3Foo12SimpleStructCfT_S0_
 49     movq    %rax, %rdi  // .x
 50     movq    %rdx, %rsi
 51     movq    %rcx, %rdx  // .y
 52     callq   __TFV3Foo12SimpleStruct3sumfT_Si
 53     popq    %rbp
 54     retq
 55     .cfi_endproc

可以看到,在bar.s中,调用sum时直接调用全局符号来完成的,而非Class中以偏移量实现。而且,在调用前,参数并不是以指向struct实例的指针方式提供self.,而是直接传值!传值过程依赖于SimpleStruct的内存布局,所以,一旦布局改变,这里就会发生错误。

Protocol

参考Swift进阶之内存模型和方法调度一文,Swift对于协议类型的采用如下的内存模型 - Existential Container:

Paste_Image.png

Existential Container包括以下三个部分:

  • 前三个word:Value buffer。用来存储Inline的值,如果word数大于3,则采用指针的方式,在堆上分配对应需要大小的内存
  • 第四个word:Value Witness Table(VWT)。每个类型都对应这样一个表,用来存储值的创建,释放,拷贝等操作函数。
  • 第五个word:Protocol Witness Table(PWT),用来存储协议的函数。
Paste_Image.png

那么,显然,如果属性增删导致属性存储区在栈、堆之间变化,或者类的方法(包括可见属性的getter、setter)增删引起vtable变化,都会引起不兼容问题。

Extension

我们常常在各个地方为类增加扩展,显然它是不会引起不兼容问题的。为Foo.swift增加一个扩展,其中实现一个空方法f0(),我们从汇编代码可以看到只是增加了一个global方法定义:

320     .globl  __TFC3Foo11SimpleClass2f0fT_T_
321     .align  4, 0x90
322 __TFC3Foo11SimpleClass2f0fT_T_:
323     .cfi_startproc
324     pushq   %rbp
325 Ltmp39:
326     .cfi_def_cfa_offset 16
327 Ltmp40:
328     .cfi_offset %rbp, -16
329     movq    %rsp, %rbp
330 Ltmp41:
331     .cfi_def_cfa_register %rbp
332     movq    %rdi, -8(%rbp)
333     popq    %rbp
334     retq
335     .cfi_endproc

静态方法

静态方法和扩展类似,只是增加了global方法定义,不会影响到类的metadata,所以不会引起不兼容问题。

总结

Swift 3出来时,我们曾看到Chris Lattner的邮件: 回顾Swift3,展望Swift4。他提到Swift4阶段1的任务时说:

  • Resilience: This provides a way for public APIs to evolve over time, even in the face of ABI stability. For example, we don’t want the C++ “fragile base class" problem to ever exist in Swift. ** Much of the design and implementation work was done in the Swift 3 timeframe**, but there are still major missing pieces, including the user-visible part of the model (e.g. new attributes).

现在看起来,Swift的整个设计完全是静态的,"易碎的"(fragile),这使得Much of the design and implementation work was done in the Swift 3 timeframe这句话让我十分费解。我不知道是不是我理解的方向都错了。

但回到我们工程,这个问题已经拦在前面了,我们只能尽量不要频繁在底层组件做非二进制兼容的改动,即使有必要,我们也应该找到一个方案,自动化地完成有时序依赖的构建过程。

更多的情况,我将继续研究,大家有不同的见解,欢迎交流~

Reference

  1. http://quotation.github.io/objc/2015/05/21/objc-runtime-ivar-access.html
  2. http://llvm.org/docs/LangRef.html#getelementptr-instruction
  3. https://github.com/apple/swift/blob/master/docs/ABI.rst#class-metadata
  4. //www.greatytc.com/p/6495a6ce65ed
  5. http://allegro.tech/2014/12/swift-method-dispatching.html
  6. https://www.cs.cmu.edu/~fp/courses/15213-s07/misc/asm64-handout.pdf
  7. https://www.lri.fr/~filliatr/ens/compil/x86-64.pdf
  8. http://www.imada.sdu.dk/Courses/DM18/Litteratur/IntelnATT.htm
  9. https://www.youtube.com/watch?v=ERYNyrfXjlg&feature=youtu.be
  10. https://developer.apple.com/videos/play/wwdc2014/404/
  11. https://developer.apple.com/videos/play/wwdc2016/416/
  12. https://developer.apple.com/videos/play/wwdc2015/409/
  13. http://www.atatech.org/articles/69335
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,029评论 4 62
  • 今天听到一条音频说读书和玩游戏。音频中讲说读书的时候觉得很累,而玩游戏并不会累是为什么? 音频解释说,其实并不...
    Q喜悦_Liu阅读 286评论 0 0
  • 从四川归来已有几日,感触颇多,奈何笔头不行,一直挨到现在,突然发现是九寨地震七日祭,惟愿逝者安息,生者安好! ...
    炑綿阅读 281评论 0 0
  • 妙玉在《红楼梦》中的出场并不算多,仅有的几次现身也是以或明或暗的形式,曹雪芹在她身上的笔墨很少,与黛玉、宝钗湘云等...
    绛洞花王阅读 616评论 13 12
  • 火红小狐仙 修炼越千年 世间但闻名 几人能得见
    冷冬寒梅阅读 199评论 5 3