swift为了实现快这么一个终极目标。在许多地方做了大量的优化。简直可以说是集现代编程语言之长。而这一点在swift中的方法调用尤为突出。我们来探究一下swift中的方法调用。
swift中函数调用方试
swift语言中总共有三种方法调用方式:
1.通过内存地址直接调用
2.通过v-table这么一个结构类似数组的函数表调用
3.就是我们非常熟悉的send_msg()消息派发。
他们的调用效率是1>2>3。动态性则是3>2>1。然后swift在任何时候都会优先的尽量使用内存直接调用,不能够使用内存地址直接调用的时候使用函数表调用。那么什么时候不能够使用内存直接调用了,很简单函数可能会被重写时就不能够使用内存直接调用了,而第三种消息表派发就是需要使用到runtime的时候会使用
stuct的方法调用
stuct一把都是采用的函数地址调用。在汇编代码中bl表示跳转到地址。这就效率最高的函数调用方式不用查找,撸起地址直接干。但是上面我们看的func2方法添加了一个mutating的关键字,要知道它的不同我们进入到sil中,能更详细的看的swift中做了什么
struct Person {
func func1()
mutating func func2()
init()
}
.....
// Person.func1()
sil hidden [ossa] @$s14ViewController6PersonV5func1yyF : $@convention(method) (Person) -> () {
// %0 "self" // user: %1
bb0(%0 : $Person):
debug_value %0 : $Person, let, name "self", argno 1 // id: %1
.....
// Person.func2()
sil hidden [ossa] @$s14ViewController6PersonV5func2yyF : $@convention(method) (@inout Person) -> () {
// %0 "self" // user: %1
bb0(%0 : $*Person):
debug_value_addr %0 : $*Person, var, name "self", argno 1 // id: %1
我截取了部分关键的sil代码。在这段代码里面 func1()和func2()第一个不同点在Person, var, name "self", argno 1。里面的Person取得是地址。
inout 的作用很简单可以让let 定义的变量可以修改
func func3(age:inout Int){
age = 20
print(age)
}
我们都知道在swift是不可以直接修改参数值的如果你要强行修改就会报这个错
但是如果我添加了inout关键字就可以修改这个let定义的变量了。调用inout修饰的参数的方法。参数需要传地址。像这样调用func3(age: &leoAge)。
那么在sil中func2()方法这里swift隐式的添加@inout的目的也就很明显了。意思就是可以修改传递进来的Person的参数。也就是struct本身。这就是mutaiting修饰的方法可以修改struct变量的原因。
struct中只有内存直接调用与函数表调用两种调用方式。因为要是使用消息派发必需继承自NSObject对象。
函数表调用
函数表的方式调用函数,在下面的截图中我们可以看到func2()的blr后面跟的不是函数地址而是一个变量。那这个变量swift是怎样存储的了?我们知道它是怎样存储的就知道了它是怎样调用的了。我们尝试着来找到函数表中函数地址的存储方式。
下面我们需要查看macho文件格式,我们先简单介绍一下macho文件的格式
Macho文件格式是这样的
它主要分为三个部分:1.Header 2.Load commands 3Data区
header中表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信 息,文件头信息影响后续的文件结构
Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表 等。
Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据 Segment 做内存映射的。
我们在看一下swift的类的构成,有助于我们在machoc中查找我们需要的信息。swift中类都有一个元数据结构,这个数据结构是一个Metadata 的struct。通过swift的源码我们可以推导出Metadata的内部结构是这样的
struct Metadata{
var kind: Int
var superClass: Any.Type
var cacheData: (Int, Int)
var data: Int
var classFlags: Int32
var instanceAddressPoint: UInt32
var instanceSize: UInt32
var instanceAlignmentMask: UInt16
var reserved: UInt16
var classSize: UInt32
var classAddressPoint: UInt32
var typeDescriptor: UnsafeMutableRawPointer
var iVarDestroyer: UnsafeRawPointer
}
其中我们需要关注typeDescriptor,不管是Class、Struct、Enumd都有Descriptor.用来描述它自身。类的描述对象如下
struct TargetClassDescriptor{
var flags: UInt32
var parent: UInt32
var name: Int32
var accessFunctionPointer: Int32
var fieldDescriptor: Int32
var superClassType: Int32
var metadataNegativeSizeInWords: UInt32
var metadataPositiveSizeInWords: UInt32
var numImmediateMembers: UInt32
var numFields: UInt32
var fieldOffsetVectorOffset: UInt32
var Offset: UInt32
var size: UInt32
...
}
我们都知道struct是值类型。如果我们现在能找到typeDescriptor这个指针地址,通过指针偏移就能访问到struct中值类型的变量的值或者引用类型的指针。那么v-table这个函数表我们推测它与位于size的后面。因为它如果不在这个地方我们考虑不到它能存放在什么地方了。下面我们来验证一下。如果能找到就说明确实是这样的。
1.我们用machoview打开我们编译后的可执行文件。
我们直接定位到数据段中的__TEXT,__swift5_types中。__TEXT,__swift5_types就是存放TargetClassDescriptor的地方。pFile是虚拟内存地址,我们来计算一下前面8位的值。0x0000BBDC+0x9CFBFFFF,注意这个地方的0x9CFBFFFF是小段地址所以应该是0xFFFFFB9C。我们得到的值是0x10000B778。然后这个值我们需要减去程序本身虚拟内存得地址,这个值在Load Commands中的LC_SEGMENT_64(__TEXT)中。
可以看到这个值是0x100000000。我们现在得到的值是0x0000B778,我们到data段中,在Section64(__TEXT,__const)中我们能看到0x0000B778位于的内存区间。
那么B778应该就是在0xB770偏移8位,那么0x80000050就应该是我们typeDescriptor的地址。
swift 方法的结构
struct TargetMethodDescriptor就是swift中函数的数据结构。其中第一个flags描述了函数的类型。
然后我们根据TargetClassDescriptor的结构在来偏移13个字节我们在地址0xB7B0找到了我们的TargetRelativeDirectPointer的偏移值。0x00000010ffffc5cc,根据TargetMethodDescriptor的数据结构,前面4个字节存放的是方法类型所以我们的函数本身的impl这个地方的偏移值应该是0xFFFFC5CC。我们当前程序的ASLR的地址为0x1025d0000
0xFFFFC5CC+0x0000B7b0 = 0x100007d7c
0x100007d7c+0x1025d0000 = 0x2025D7D7C
0x2025D7D7C - Vm地址(0x10000000) = 0x1025D7D7C
下面看代码
我们我们断点到leo.fun2()这里查看汇编代码,blr x8 其x8寄存器存放的就是func2()的地址。我们使用register read x8读取x8的地址 0x00000001025d7d7c。与我们计算得到的地址是一样的说明我们上面的推论是正确的。
消息派发
class Person:NSObject{
func func2(){
}
@objc dynamic func func3(){
}
@objc func func4(){
}
dynamic func func5(){
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let leo = Person()
leo.func2()
leo.func3()
leo.func4()
leo.func5()
}
}
我在Person中又添加了func3()、func4()、func5()方法。同时对他们分别使用@objc 、dynamic修饰
然后我们看sil代码
在Person的sil_vtable中func3()方法没有添加到vtable这个函数表中。因为func3()使用了@objc + danamic 修饰。
在sil中 这个地方就很明显的标识出来了他们调用方法的不同。func3()使用的是objc_method,而虚函数表使用的是class_method.
swift中的消息派发主要是为了兼容runtime的机制。所以使用消息派发的swift方法也就可以使用我们runtime的黑魔法了。