在上篇文章 结构体与类 中,我们了解到结构体与类的本质区别,并且通过阅读 Swift 源码以及验证,得知 Swift 类的本质就是 HeapObject 的结构体指针。
在结构体和类内部的方法中,引用当前实例的属性时,一般情况下可以不用在属性前加 self 关键字,比如 self.age 可以直接将 self. 省略,写成 age。接下来我们将对 Swift 的方法进行深入了解。
一、函数相关的修饰符
1. mutating
构体和枚举是值类型,默认情况下,值类型的属性不能被自身的实例方法修改,在 func 关键字前加 mutating 可以允许这种修改行为。
2. inout
inout - 输入输出参数(In-Out Parameter)。可以用 inout 定义一个输入输出参数,可以在函数内部修改外部实参的值。inout 需要注意的有以下几个点:
- 可变参数不能标记为 inout。
- inout 参数不能有默认值。
- inout 参数只能传入可以被多次赋值的。
- inout 参数的本质是地址传递(引用传递)。
3. @discardableResult
在 func 前面加个 @discardableResult,可以消除:函数调用后返回值未被使用的警告⚠。
4. final
- 被 final 修饰的类,禁止被继承。
- 被 final 修饰的方法、下标、属性,禁止被重写。并且添加了 final 关键字的函数无法被重写,使用静态派发,不会在 vtable 中出现,且 对 objc 运行时不可⻅。
5. dynamic
函数均可添加 dynamic 关键字,为非 objc 类和值类型的函数赋予动态性,但派发 方式还是函数表派发。
6.@objc
该关键字可以将 Swift 函数暴露给 objc 运行时,与 OC 交互,但依旧是函数表派发。
那么,@objc dynamic 就会变成消息派发的方式-也就是 OC 中的消息传递。
二、结构体的方法
接下来有一个 SHPoint 结构体,分别有 x,y 属性。当我在 moveBy 方法中修改 x,y 的值时,编译器报错如下:
当我们用 mutating 修饰的时候,就可以允许这种修改行为。
这里我们思考一个问题,添加了 mutating 之后为什么可以修改?其本质是什么。代码如下:
代码语言:javascript复制struct SHPoint {
var x = 5.0, y = 10.0
func sum() {
let result = x y
print(result)
}
mutating func moveBy(deltaX: Double, deltaY: Double) {
x = deltaX
y = deltaY
print(x,y)
}
}
var p = SHPoint()
p.sum()
p.moveBy(deltaX: 10, deltaY: 20)
(滑动显示更多)
我们把当前 Swift 的代码编译成 SIL 的代码,在编译之前有个坑我得填一下。
1. 生成 .sil 文件的注意点
我上一篇文章 结构体与类 中有对 SIL 做了介绍,通过 swiftc main.swift -emit-sil 命令生成了 .sil 文件,并在终端将 SIL 的代码输出。之后我打开 .sil 文件,发现里面什么都没有,然后我看了一下文件大小:
好家伙,虽然在终端有输出 SIL 的代码,但生成的文件却是啥都没有,这个时候我用的是 Intel 芯片的 Mac 电脑。后面我又踩了坑,用 M1 的电脑编译成 .sil 文件的时候,我直接就看不到 .sil 文件了。
后面查阅资料,发现 swiftc main.swift -emit-sil 只是将当前目录下的 main.swift 文件的代码编译成 SIL 代码并在终端输出。那咋办,我查阅了一些资料得到以下关于生成 .sil 文件 和 .ll 文件的命令。
代码语言:javascript复制// 将当前目录的 main.swift 文件编译成 main.sil 文件并保存到当前目录。
swiftc -emit-sil main.swift >> main.sil
// 编译成带转译的 sil
swiftc -emit-sil main.swift | xcrun swift-demangle >> main.sil
// 编译成带转译的 ir
swiftc -emit-ir main.swift | xcrun swift-demangle >> main.ll
// 将 UIKit 相关的编译成 sil
swiftc -emit-sil -target x86_64-apple-ios13.5-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) ViewController.swift > ViewController.sil
(滑动显示更多)
另外,如果想查看更多的 swiftc 的命令,可以在终端输入 swiftc -h。
为了能够打开 .sil 文件,方便阅读,我用 swiftc -emit-sil main.swift >> main.sil 这个命令生成了 main.sil 文件。
2. main.sil 分析
为了方便阅读,将 main.sil 文件拖拽到项目的目录文件下,与 main.swift 同一目录,如下:
具体的内容大家可以自己去生成看看,我只对重要的部分代码贴出来做讲解。第一段如下:
代码语言:javascript复制// 我们定义的 SHPoint 结构体
struct SHPoint {
// x 属性
@_hasStorage @_hasInitialValue var x: Double { get set }
// y 属性
@_hasStorage @_hasInitialValue var y: Double { get set }
// sum 方法
func sum()
// moveBy 方法
mutating func moveBy(deltaX: Double, deltaY: Double)
// 初始化方法
init()
init(x: Double = 5.0, y: Double = 10.0)
}
// 创建的 p 变量
@_hasStorage @_hasInitialValue var p: SHPoint { get set }
(滑动显示更多)
SHPoint 结构体的定义编译成 SIL 代码如同上面的一样,但貌似看不出 sum 和 moveBy 的本质区别。我们找到 main 函数,看看方法的调用情况。
2.1. sum 方法 SIL 的调用
代码语言:javascript复制// function_ref SHPoint.sum()
// 注意:s4main7SHPointV3sumyyF 是被 SIL 混写 sum 过后的名称,
= function_ref @$s4main7SHPointV3sumyyF : $@convention(method) (SHPoint) -> () // user:
= apply (%9) : $@convention(method) (SHPoint) -> ()
= float_literal $Builtin.FPIEEE64, 0x4024000000000000 // 10 // user:
= struct $Double ( : $Builtin.FPIEEE64) // user:
= float_literal $Builtin.FPIEEE64, 0x4034000000000000 // 20 // user:
= struct $Double ( : $Builtin.FPIEEE64) // user:
(滑动显示更多)
注意看,s4main7SHPointV3sumyyF 是被 SIL 混写 sum 过后的名称,那怎么证明呢,我们来通过一个命令:
代码语言:javascript复制xcrun swift-demangle <混写后的名称>
通过这个命令,终端打印出来的就是 SHPoint 里的 sum 方法.我们继续看 这一行,注意看,sum 方法我们在 Swift 的代码中是没有任何参数的,但在底层的 SIL 实现,会默认有一个参数 - SHPoint,其实这个参数就是 SHPoint 的实例 - self,这也是为什么我们在方法中可以调用 self. 的原因。
2.2. moveBy 方法 SIL 的调用
代码语言:javascript复制 = begin_access [modify] [dynamic] %3 : $*SHPoint // users: ,
// function_ref SHPoint.moveBy(deltaX:deltaY:)
= function_ref @$s4main7SHPointV6moveBy6deltaX0E1YySd_SdtF : $@convention(method) (Double, Double, @inout SHPoint) -> () // user:
= apply (, , ) : $@convention(method) (Double, Double, @inout SHPoint) -> ()
end_access : $*SHPoint // id:
(滑动显示更多)
看 这一行代码,我们的 moveBy 方法在 SIL 的实现中有三个参数,分别为 deltaX,deltaY,以及 SHPoint(self)。注意!和 sum 不同的是,moveBy 传的 SHPoint 前面有一个 inout。
这就说明,在结构体中,如果方法前加上 mutating 修饰符,底层会把 SHPoint 这个参数前加上 inout,本质上就是传 SHPoint 实例的地址。所以这个时候结构体才能被自身的实例方法修改自身的属性。
2.3. sum 方法 SIL 的实现
为了减少在方法中 print 生成的 SIL 代码,我把 print 注释重新生成了 main.sil 文件。
我们全局搜索 SIL 把 sum 方法混写后的名称 s4main7SHPointV3sumyyF,找到 sum 方法内部实现,如下:
代码语言:javascript复制// SHPoint.sum()
sil hidden @$s4main7SHPointV3sumyyF : $@convention(method) (SHPoint) -> () {
// %0 "self" // users: %3, %2, %1
bb0(%0 : $SHPoint):
debug_value %0 : $SHPoint, let, name "self", argno 1 // id: %1
%2 = struct_extract %0 : $SHPoint, #SHPoint.x // user: %4
%3 = struct_extract %0 : $SHPoint, #SHPoint.y // user: %5
%4 = struct_extract %2 : $Double, #Double._value // user: %6
%5 = struct_extract %3 : $Double, #Double._value // user: %6
%6 = builtin "fadd_FPIEEE64"(%4 : $Builtin.FPIEEE64, %5 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %7
%7 = struct $Double (%6 : $Builtin.FPIEEE64) // user: %8
debug_value %7 : $Double, let, name "result" // id: %8
%9 = tuple () // user:
return %9 : $() // id:
} // end sil function '$s4main7SHPointV3sumyyF'
(滑动显示更多)
这个实现的代码不就和 Swift 实现的过程一样么,拿到 SHPoint 的 x,y 的值,相加,赋值到名为 result 的常量。
2.4. moveBy 方法 SIL 的实现
moveBy 方法的搜索过程和 sum 一样,其 SIL 的实现如下:
代码语言:javascript复制// SHPoint.moveBy(deltaX:deltaY:)
sil hidden @$s4main7SHPointV6moveBy6deltaX0E1YySd_SdtF : $@convention(method) (Double, Double, @inout SHPoint) -> () {
// %0 "deltaX" // users: , %3
// %1 "deltaY" // users: , %4
// %2 "self" // users: , %6, %5
bb0(%0 : $Double, %1 : $Double, %2 : $*SHPoint):
debug_value %0 : $Double, let, name "deltaX", argno 1 // id: %3
debug_value %1 : $Double, let, name "deltaY", argno 2 // id: %4
debug_value_addr %2 : $*SHPoint, var, name "self", argno 3 // id: %5
%6 = begin_access [modify] [static] %2 : $*SHPoint // users: , %7
%7 = struct_element_addr %6 : $*SHPoint, #SHPoint.x // users: , %8
%8 = struct_element_addr %7 : $*Double, #Double._value // user: %9
%9 = load %8 : $*Builtin.FPIEEE64 // user:
= struct_extract %0 : $Double, #Double._value // user:
= builtin "fadd_FPIEEE64"(%9 : $Builtin.FPIEEE64, : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user:
= struct $Double ( : $Builtin.FPIEEE64) // user:
store to %7 : $*Double // id:
= tuple ()
end_access %6 : $*SHPoint // id:
= begin_access [modify] [static] %2 : $*SHPoint // users: %,
= struct_element_addr : $*SHPoint, #SHPoint.y // users: #,
= struct_element_addr : $*Double, #Double._value // user:
= load : $*Builtin.FPIEEE64 // user: !
= struct_extract %1 : $Double, #Double._value // user: !
! = builtin "fadd_FPIEEE64"( : $Builtin.FPIEEE64, : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: "
" = struct $Double (! : $Builtin.FPIEEE64) // user: #
store " to : $*Double // id: #
$ = tuple ()
end_access : $*SHPoint // id: %
& = tuple () // user: '
return & : $() // id: '
} // end sil function '$s4main7SHPointV6moveBy6deltaX0E1YySd_SdtF'
(滑动显示更多)
有点长,但注意看 begin_access 和 end_access ,这两个是成对出现的,第一对是 x = deltaX 的代码,那么第二对就是 y = deltaY 的代码。
并且细心看的话,在取 SHPoint 实例的值的时候,是通过 *SHPoint 这种形式的,而 sum 方法是 SHPoint 这种形式。这就更加证明了用 mutating 修饰后, 就是取地址,针对这个地址的内存进行操作。
3. 调用结构体方法的本质
在 OC 中,调用一个方法的本质是消息传递,底层通过 objc_msgSend 函数去查找方法并调用。而 Swift 是一门静态语言,没有运行时的机制,那原生的 Swift 方法又是如何调用的呢?
我们通过汇编先来看一下,调用结构体的方法时,底层是如何调用的。代码如下:
我们打来断点,进入查看当前调用的汇编代码:
可以发现,在 Swift 中,调用一个结构体的方法是直接拿到函数的地址直接调用,包括初始化方法,没有 OC 那么复杂的流程。
需要注意的是,结构体的类方法调用也和实例方法的调用一样,都是直接拿函数的地址调用。在 Swift 中声明一个类方法需要在 func 前家加上 static 关键字。
Swift 是一门静态语言,许多东西在运行的时候就可以确定了,所以才可以直接拿到函数的地址进行调用,这个调用的形式也可以称作静态派发。
三、类的方法
我们已经了解了 Swift 结构体的方法调用,那么 Swift 的类呢?和结构体一样吗,但不管和结构体一不一样,肯定是和 OC 的类不一样的。
1. 类的方法汇编调用
我们新建一个 Swift 项目,需要注意的是,一定要用真机跑,因为我们的 iOS 程序都是要装到手机上的,而手机的架构目前基本都是 arm64 的架构。
定义一个 SHPerson 类型,调用方法,并且打上断点,如下:
打个断点,来看一下 Swift 类的方法在汇编的调用情况。
代码语言:javascript复制class SHPerson {
func setName1() {}
func setName2() {}
func setName3() {}
}
前面提过,blr 指令是跳转到某地址(无返回),也就是那么这个过程中的 x8,x9 寄存器存的值就是函数的地址。
还有一个点,注意看!调用初始化方法的指令是 bl,也就意味着有返回值,这个返回值就是 SHPerson 的实例对象。那么一般情况下,x0 存放的就是这个函数的返回值。
注意看第 19 行。
代码语言:javascript复制0x1024cfd5c < 68>: ldr x8, [x0]
这句代码的意思是,取 x0 的地址,存到 x8,注意,这里是取 x0 地址的开始,往后算 8 个字节。那么这个时候 x8 的地址是谁的地址呢,是 metadata 的地址。
那咋就知道是 metadata 呢,把断点打在第 20 行,重新运行后,读取 x8 的值:
那此时 x8 就是 metadata 的地址,没毛病。接下来断点走到第 23 行按 control 点那个向下的按钮,跳进去。29,33 行的代码一样的操作,看看是不是就是对应 setName1,setName2,setName3 方法。
那此时 x8 就是 metadata 的地址,没毛病。接下来断点走到第 23 行按 control 点那个向下的按钮,跳进去。29,33 行的代码一样的操作,看看是不是就是对应 setName1,setName2,setName3 方法。
确实是 setName1,setName2,setName3 方法。虽然也是拿到函数的地址调用,但是很明显,系统是通过拿到 SHPerson 的实例对象,并且拿到 metadata 的地址后,通过内存平移的方式,拿到函数地址再进行调用。
函数的地址是连续存储的,不像 OC,是存放在无序的哈希表里。那么,函数的地址存放在哪里呢?
2. 虚函数表的引入
用这句命令生成 ViewController.sil 文件。
代码语言:javascript复制swiftc -emit-sil -target x86_64-apple-ios13.5-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) ViewController.swift > ViewController.sil
(滑动显示更多)
生成 ViewController.sil 文件后,我们打开文件,看到文件的最底部。
代码语言:javascript复制sil_vtable SHPerson {
#SHPerson.setName1: (SHPerson) -> () -> () : @$s14ViewController8SHPersonC8setName1yyF // SHPerson.setName1()
#SHPerson.setName2: (SHPerson) -> () -> () : @$s14ViewController8SHPersonC8setName2yyF // SHPerson.setName2()
#SHPerson.setName3: (SHPerson) -> () -> () : @$s14ViewController8SHPersonC8setName3yyF // SHPerson.setName3()
#SHPerson.init!allocator: (SHPerson.Type) -> () -> SHPerson : @$s14ViewController8SHPersonCACycfC // SHPerson.__allocating_init()
#SHPerson.deinit!deallocator: @$s14ViewController8SHPersonCfD // SHPerson.__deallocating_deinit
}
sil_vtable ViewController {
#ViewController.deinit!deallocator: @$s14ViewControllerAACfD // ViewController.__deallocating_deinit
}
(滑动显示更多)
sil_vtable 是啥,根据感觉翻译的话,vtable 应该是虚表或者叫虚函数表,在 C 类的函数是放在虚函数表中的,那么 Swift 类的方法就是会不会存储在 vtable 里面呢。
3. 源码查找虚函数表
在上一篇文章 结构体与类 中,我们把 Swift 类的本质挖掘出来了,它里面有一个 metadata,metadata 里有一个成员变量,当时归纳出来,这个成员变量应该是这样:
代码语言:javascript复制var typeDescriptor: UnsafeMutableRawPointer
(滑动显示更多)
这个成员量存放的是对自己的描述,类、结构体、枚举内部都有这个成员变量。
那么我们接下来找到源码中 typeDescriptor 的定义,根据上一篇文章 结构体与类,查找流程为:
- 找到 HeapObject。
- 从 HeapObject 中找到 HeapMetadata。
- 继续跟进,HeapMetadata 为 TargetHeapMetadata 的别名。
- 找到 TargetHeapMetadata 结构体。
那么找到 TargetHeapMetadata 后其内部有一个成员变量,如下:
代码语言:javascript复制TargetSignedPointer<Runtime, const TargetClassDescriptor<Runtime> * __ptrauth_swift_type_descriptor> Description;
(滑动显示更多)
此时,发现 Description 是一个为 TargetClassDescriptor 的类,并且继承了一堆的东西。
其中的结构可以去慢慢对比,对比出来的结果,TargetClassDescriptor 大概长这样:
代码语言:javascript复制class 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
//V-Table
}
(滑动显示更多)
那么 TargetClassDescriptor 其实有一个别名为 ClassDescriptor,其定义如下:
代码语言:javascript复制using ClassDescriptor = TargetClassDescriptor<InProcess>;
(滑动显示更多)
我们全局搜索 ClassDescriptor,找到了 GenMeta.cpp 文件,通过名字,可以猜到 GenMeta.cpp 文件的代码就是生成元数据的地方。
我们进入到 GenMeta.cpp 文件,直接定位到 ClassContextDescriptorBuilder 这个类-内容的描述建立者,这个类就是创建 metadata 和 Descriptor 的类。
在类中找到 layout 这个方法:
代码语言:javascript复制void layout() {
super::layout();
addVTable();
addOverrideTable();
addObjCResilientClassStubInfo();
maybeAddCanonicalMetadataPrespecializations();
}
(滑动显示更多)
在里面调用了 super::layout(),我们来看一下父类的实现:
代码语言:javascript复制void layout() {
asImpl().computeIdentity();
super::layout();
asImpl().addName();
asImpl().addAccessFunction();
asImpl().addReflectionFieldDescriptor();
asImpl().addLayoutInfo();
asImpl().addGenericSignature();
asImpl().maybeAddResilientSuperclass();
asImpl().maybeAddMetadataInitialization();
}
(滑动显示更多)
代码语言:javascript复制void layout() {
asImpl().addFlags();
asImpl().addParent();
}
到这里,基本就和 TargetClassDescriptor 类的成员变量对应起来了。那我们的重点当然还是 addVTable 函数,这个函数就是将 Swift 类的方法添加到虚函数表,其实现如下:
代码语言:javascript复制void addVTable() {
LLVM_DEBUG(
llvm::dbgs() << "VTable entries for " << getType()->getName() << ":n";
for (auto entry : VTableEntries) {
llvm::dbgs() << " ";
entry.print(llvm::dbgs());
llvm::dbgs() << 'n';
}
);
// Only emit a method lookup function if the class is resilient
// and has a non-empty vtable, as well as no elided methods.
if (IGM.hasResilientMetadata(getType(), ResilienceExpansion::Minimal)
&& (HasNonoverriddenMethods || !VTableEntries.empty()))
IGM.emitMethodLookupFunction(getType());
if (VTableEntries.empty())
return;
auto offset = MetadataLayout->hasResilientSuperclass()
? MetadataLayout->getRelativeVTableOffset()
: MetadataLayout->getStaticVTableOffset();
B.addInt32(offset / IGM.getPointerSize());
B.addInt32(VTableEntries.size());
for (auto fn : VTableEntries) {
emitMethodDescriptor(fn);
}
}
(滑动显示更多)
前面的代码可以先不用管,我们注意看最后的几行。计算 offset 之后,调用了 addInt32 函数,这个函数就是去计算添加方法到虚函数表的偏移量,具体可以看源码实现。最后 for 循环,添加函数的指针。
这个偏移量是 TargetClassDescriptor 这个结构中的成员变量所有内存大小之和,并且在最后还拿到了 VTableEntries.size()。
也就意味着,虚函数表的内存地址,是 TargetClassDescriptor 中的最后一个成员变量,并且,添加方法的形式是追加到数组的末尾。所以这个虚函数表是按顺序连续存储类的方法的指针。
4. MachOView 分析类的方法存储
4.1. Mach-O 文件
Mach-O 其实是 Mach Object 文件格式的缩写,是 mac 以及 iOS 上可执行文件的格 式, 类似于 windows 上的 PE 格式 (Portable Executable ), linux 上的 elf 格式 (Executable and Linking Format) 。常⻅的有 .o,.a .dylib Framework,dyld .dsym。
- 首先是文件头,表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信 息,文件头信息影响后续的文件结构安排。
- Load commands 是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表 等。
- Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load Command,Segment 中 Section 就可以被解读为是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据 Segment 做内存映射的。
用 MachOView 工具打开 Mach-O 文件的格式大概长这样:
下面是关于 Mach-O 文件一些字段的说明。
代码语言:javascript复制LC_SEGMENT_64 //将文件中(32位或64位)的段映射到进程地 址空间中
LC_DYLD_INFO_ONLY //动态链接相关信息
LC_SYMTAB //符号地址
LC_DYSYMTAB //动态符号表地址
LC_LOAD_DYLINKER //dyld加载
LC_UUID //文件的UUID
LC_VERSION_MIN_MACOSX //支持最低的操作系统版本
LC_SOURCE_VERSION //源代码版本
LC_MAIN //设置程序主线程的入口地址和栈大小
LC_LOAD_DYLIB //依赖库的路径,包含三方库
LC_FUNCTION_STARTS //函数起始地址表
LC_CODE_SIGNATURE //代码签名
(滑动显示更多)
4.2. Mach-O 文件查找方法内存结构地址
在运行项目之后,会生成一个可执行文件,用 MachOView 这个工具打开它。但是注意,我上面的工程有中文命名,有中文命名的项目生成的可执行文件用 MachOView 打开是会闪退的。所以重新建一个项目,命名不要带有中文。
在新建项目的时候,Xcode 不会自动生成 Products 文件,可以参考这篇文章:
blog.csdn.net/u012275628/…
解决之后我们 Show in Finder,显示包内容,用 MachOView 工具打开可执行文件。
swift5_types 这里存放的是结构体、枚举、类的 Descriptor,那么我们可以在 swift5_types 这里找到类的 Descriptor 的地址信息。
前面的四个字节 90 FB FF FF 就是 SHPerson 的 Descriptor 信息,那用 90 FB FF FF 加上前面的 0000BBDC 得到的就是 Descriptor 在当前 Mach-O 文件的内存地址。
它们怎么相加呢,iOS 属于小端模式,所以 90 FB FF FF 要从右边往左读。也就是:
代码语言:javascript复制FFFFFB90 0000BBDC = 0x10000B76C
0x10000B76C 这个值是我拿计算器算的,那么 0x100000000 是 Mach-O 文件中虚拟内存的基地址,如图所示。
我们用 0x10000B76C - 0x100000000 = B76C 就是 SHPerson 在整个 Data 区的内存地址。我们找到 TEXT, const。
如图所示,这个就是 B76C 的首地址,也是意味着,它后面的数据是 TargetClassDescriptor 的数据,所以我们可以在这里拿到 SHPerson 的虚函数表 - SHPerson 方法的地址。
计算 TargetClassDescriptor 中 VTable 前面的数据大小,求得偏移量。一共 12 个 4 字节(48字节)的成员变量,12 个四字节的成员变量再加上 size(4字节)得到 52 字节,在往后的 24 字节就是 setName1,setName2,setName3 方法的结构地址(一个函数地址占 8 字节)。如图所示:
如图中所示,B7A0 是 setName1 结构在 Mach-O 文件的地址。那么在程序中如何找到该地址呢。
4.3. 验证 Mach-O 分析的函数地址是否和程序运行的一致
ASLR 是一个随机偏移地址,这个随机偏移地址的目的是为了给应用程序一个随机内存地址。
我们打一个断点,程序运行起来后,输入 LLDB 命令: image list。如图所示:
image list 是列出应用程序运行的模块,我们找到第一个,其内存地址为 0x0000000100728000,那我们可以把这个地址当作应用程序的基地址。
接下来我在源码中找到这么一个结构体。TargetMethodDescriptor 是 Swift 的方法在内存中的结构,Impl 不是真正的 imp,而是相对指针 offset。
代码语言:javascript复制/// An opaque descriptor describing a class or protocol method. References to
/// these descriptors appear in the method override table of a class context
/// descriptor, or a resilient witness table pattern, respectively.
///
/// Clients should not assume anything about the contents of this descriptor
/// other than it having 4 byte alignment.
struct TargetMethodDescriptor {
/// Flags describing the method.
// 占 4 字节,Flags 标识是什么方法。
MethodDescriptorFlags Flags;
/// The method implementation.
// 不是真正的 imp,这里存储的是相对指针,offset。
TargetRelativeDirectPointer<Runtime, void> Impl;
// TODO: add method types or anything else needed for reflection.
};
(滑动显示更多)
到这里,TargetMethodDescriptor 结构体的地址就可以确定了,那么要找到函数地址,还需要偏移 Flags Impl,得到的就是函数的地址。综合以上的逻辑开始计算:
代码语言:javascript复制// 应用程序的基地址:0x0000000100728000,setName1 结构地址:B7A0,Flags:0x4,offset:90 C5 FF FF
// 注意!小端模式要从右往左,所以为 FFFFC590
0x0000000100728000 B7A0 0x4 FFFFC590 = 0x20072FD34
// 接下来需要减掉 Mach-O 文件的虚拟地址 0x100000000,得到的就是函数的地址。
0x20072FD34 - 0x100000000 = 0x10072FD34
(滑动显示更多)
打开汇编调试,读取汇编中 setName1 的地址,验证 0x10072FD34 是否就是 setName1 的地址。如图所示:
完美!0x10072FD34 就是 setName1 的函数地址。到这里就完全验证了 Swift 类的方法确实是存放在 VTable - 虚函数表里面的。
5. extension 中的方法调用
清楚 Swift 类中方法的存储位置及调用方式后,我们来看一下类的方法在 extension 中是怎么调用的。
在原有的 SHPerson 基础上添加 extension ,并添加 setName4 方法。
代码语言:javascript复制extension SHPerson {
func setName4() {}
}
程序跑起来后,通过汇编查看发现,Swift 类的 extension 里的方法竟然没有放在虚函数表,而是直接地址调用。我们来看一张图:
假设,现在有两个类,SHPerson 和 SHStudent , SHStudent 继承自 SHPerson。在 Swift 中,每个类都有自己的虚函数表,如果我此时在 SHPerson 的 extension 中添加一个方法,此时,除了 SHPerson 能调用 extension 的方法之外,SHStudent 也能调用。
这个时候,方法的存储就成了问题,不可能追加到虚函数表的末尾,那样就会导致方法的存储位置发生混乱,因为 SHStudent 的方法可能会比 SHPerson 多。那要考虑存储顺序的问题,就得需要记录索引,插入,查找等复杂的操作,而这个操作是比较消耗性能的。
所以为了优化,直接把 extension 独立于虚函数表之外,采用静态调用的方式不是更好么。在程序进行编译的时候,函数的地址就已经知道了,又何必去考虑记录索引,插入,查找方法等复杂的操作呢。
四、内联函数(Inline Function)
内联函数是一种编译器优化技术,它通过使用方法的内容替换直接调用该方法,从而优化性能。
如果开启了编译器优化(Release 模式默认会开启优化),编译器会自动将某些函数变成内联函数-将函数调用展开成函数体。手动修改的方式如图所示:
- always - 将确保始终内联函数。通过在函数前添加 @inline(__always) 来实现此行为。
- never - 将确保永远不会内联函数。这可以通过在函数前添加 @inline(never) 来实现。
// 永远不会被内联(即使开启了编译器优化)
@inline(never) func test() {
print("test")
}
// 开启编译器优化后,即使代码很长,也会被内联(递归调用函数、动态派发的函数除外)
@inline(__always) func test() {
print("test")
}
(滑动显示更多)
在 Release 模式下,编译器已经开启优化,会自动决定哪些函数需要内联,因此没必要使用 @inline。
哪些函数不会被自动内联?
- 函数体比较长。
- 包含递归调用。
- 包含动态派发。
- …
五、ARM 汇编的常见指令
- mov:将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器或者寄存器与常量之间传值,不能用于内存地址),如:
mov x1, x0 // 将寄存器 x0 的值复制到寄存器 x1 中
(滑动显示更多)
- add: 将某一寄存器的值和另一寄存器的值 相加 并将结果保存在另一寄存器中, 如:
add x0, x1, x2 // 将寄存器 x1 和 x2 的值相加后保存到寄存器 x0 中
(滑动显示更多)
- sub: 将某一寄存器的值和另一寄存器的值 相减 并将结果保存在另一寄存器中:
sub x0, x1, x2 // 将寄存器 x1 和 x2 的值相减后保存到寄存器 x0 中
(滑动显示更多)
- and: 将某一寄存器的值和另一寄存器的值 按位 与 并将结果保存到另一寄存器中, 如:
and x0, x0, #0x1 // 将寄存器 x0 的值和常量 1 按位与后保存到寄存器 x0 中
(滑动显示更多)
- orr: 将某一寄存器的值和另一寄存器的值 按位 或 并将结果保存到另一寄存器中, 如:
orr x0, x0, #0x1 // 将寄存器 x0 的值和常量 1 按位或后保存到寄存器 x0 中
(滑动显示更多)
- str : 将寄存器中的值写入到内存中,如:
str x0, [x0, x8] // 将寄存器 x0 中的值保存到栈内存 [x0, x8] 处
(滑动显示更多)
- ldr: 将内存中的值读取到寄存器中,如:
ldr x0, [x1, x2] // 将寄存器 x1 和寄存器 x2 的值相加作为地址,取该内存地址的值放入寄存器 x0 中
(滑动显示更多)
- cbz: 和 0 比较,如果结果为零就转移(只能跳到后面的指令)。
- cbnz: 和非 0 比较,如果结果非零就转移(只能跳到后面的指令)。
- cmp: 比较指令。
- bl: (branch)跳转到某地址(有返回)。
- blr: 跳转到某地址(无返回)。
- ret: 子程序(函数调用)返回指令,返回地址已默认保存在寄存器 lr (x30) 中。
原文链接:https://juejin.cn/post/7047388895670960164
- END -