静态派发
值类型对象的函数的调用方式是静态调用
,即直接地址调用
,调用函数指针,这个函数指针在编译、链接完成之后就已经确定了
,存放在代码段
,而结构体内部并不存放方法。因此可以通过地址直接调用
- 结构体函数符号调试如下:
- 打开
Mach-O
可执行文件,其中的__text
段,就是所谓的代码段,需要执行的汇编指令都在这里
对于上面的分析,有个疑问:直接地址调用后面是符号
,这个符号是怎么来的?
- 是从
Mach-O
文件的符号表 Symbol Table
,但是符号表中并不存储字符串
,字符串存储在字符串表 String Table
(存放所有的变量名和函数名,以字符串形式存储),然后根据符号表中的偏移值
到字符串表中
查找对应的字符,然后进行命名重整
-
Symbol Table
: 存储符号位于字符串表的位置 -
Dynamic Symbol Table
:动态库函数
位于符号表的偏移位置
还可以通过终端命令nm
,获取项目中的符号表
查看符号表:
nm mach-o文件路径
通过命令还原符号名称:
xcrun swift-demangle 符号
将
Edit Scheme
中的Debug
改成Release
,编译后查看,在可执行文件目录下,多了一个后缀为dSYM
的文件,此时,再去Mach-o
文件中查找teach
符号,发现是找不到的。 其主要原因是因为静态链接的函数,实际上是不需要符号的
,一旦编译完成,其地址确定后,当前的符号表就会删除当前函数对应的符号,在release环境下,符号表
中存储的只是不能确定地址的符号
对于不能确定地址的符号,是在
运行时确定的
,即函数第一次调用时(相当于懒加载
),例如print
,是通过dyld_stub_bind
确定地址的
函数符号命名规则
- 对于
C函数
来说,命名的重整规则就是在函数名之前加_
(注意:C中不允许函数重载
,因为没有办法区分)
#include
void test(){}
- 对于OC来说,也不支持函数重载,其符号命名规则是
-[类名 函数名]
- 对于Swift来说,是允许函数重载,主要是因为swift中的
重整命名规则
比较复杂,可以确保函数符号的唯一性
ASLR(随机地址偏移)
新建一个iOS项目,在 ViewController
中定义一下代码
struct HTStack {
func teacher() {
print("teacher")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let t = HTStack()
t.teacher()
print("end")
}
}
- 运行上述代码,查看
mach-o
文件,发现mach-o
文件中的地址 与 函数调用的地址不一致,主要原因是实际调用时地址多了一个ASLR
(地址空间布局随机化 address space layout randomizes)
- 在
mach-o
文件中查看,程序运行静态基地址
(VM address) 是0x0000000100000000
- 可以通过
image list
查看,其中0x100000000
程序运行的首地址,后八位是随机地址偏移0x20ef000
(即 ASLR)
- 函数地址等于
0x100000000
(程序运行首地址)+0x20ef000
(ASLR) +0x3A30
(符号表地址偏移)=0x1020F2A30
动态派发
汇编指令补充
ARM64汇编指令
-
blr
:带返回的跳转指令,跳转到指令后边跟随寄存器中保存的地址 -
mov
:将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器 或者 寄存器与常量之间 传值,不能用于内存地址)-
mov x1, x0
将寄存器x0的值复制到寄存器x1中
-
-
ldr
:将内存中的值读取到寄存器中-
ldr x0, [x1, x2]
将寄存器x1和寄存器x2 相加作为地址,取该内存地址的值翻入寄存器x0中
-
-
str
:将寄存器中的值写入到内存中-
str x0, [x0, x8]
将寄存器x0的值保存到内存[x0 + x8]处
-
-
bl
:跳转到某地址
探索class的调度方式
首先介绍下V_Table在SIL文件中的格式
//声明sil vtable关键字
decl ::= sil-vtable
//sil vtable中包含 关键字、标识(即类名)、所有的方法
2 sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'
//方法中包含了声明以及函数名称
3 sil-vtable-entry ::= sil-decl-ref ':' sil-linkage? sil-function-name
例如,以HTTacher为例,其SIL中的v-table如下所示
class HTTeacher {
func teacher() { print("teacher") }
func teacher1() { print("teacher1") }
func teacher2() { print("teacher2") }
func teacher3() { print("teacher3") }
}
-
sil_vtable
:关键字 -
HTTeacher
:表示是 HTTeacher类的函数表 - 其次就是当前方法的声明对应着方法的名称
- 函数表 可以理解为
数组
,声明在 class内部的方法在不加任何关键字修饰的过程中,是连续存放
在我们当前的地址空间中的。这一点,可以通过断点来印证
函数表源码探索
下面来进行函数表底层
的源码探索
- 源码中搜索
initClassVTable
,并加上断点,然后写上源码进行调试
- 其内部是通过 for循环编码,然后
offset+index
偏移,然后获取method
,将其存入到偏移后的内存中,从这里可以验证函数是连续存放的
- 对于class中函数来说,类的方法调度是通过
V-Taable
,其本质就是一个连续的内存空间(数组结构)。
问题:如果更改方法声明的位置呢?例如 extension
中的函数,此时的函数调度方式还是函数表调度吗?
- 定义一个 HTTeacher的
extension
extension HTTeacher {
func teacher4() { print("teacher4") }
}
- 再定义一个子类
HTStudent
继承自HTTeacher
,查看SIL中的V-Table
class HTStudent: HTTeacher {}
- 查看 SIL文件,发现子类只继承了class中定义的函数,即函数表中的函数
其原因是因为子类将父类的函数表全部继承
了,如果此时子类增加函数,会继续在连续的地址中插入,假设extension函数也是在函数表中
,则意味着子类也有,但是子类无法并没有相关的指针记录函数 是父类方法 还是 子类方法,所以不知道方法该从哪里插入
,导致extension中的函数无法安全的放入子类中。所以在这里可以侧面证明extension中的方法是直接调用的,且只属于类,子类是无法继承的
开发注意点:
- 需要继承的
方法和属性
,不能写在extension
中。 - 而
extension
中创建的函数,一定是只属于自己类
,但是其子类也有其访问权限
,只是不能继承和重写
final、@objc、dynamic修饰函数
final 修饰
-
final
修饰的方法是直接调度的
,可以通过SIL验证 + 断点验证
class HTTeacher {
final func teacher() { print("teacher") }
func teacher1() { print("teacher1") }
func teacher2() { print("teacher2") }
func teacher3() { print("teacher3") }
}
@objc 修饰
- 使用
@objc
关键字是将swift
中的方法暴露给OC
class HTTeacher {
@objc func teacher() { print("teacher") }
func teacher1() { print("teacher1") }
func teacher2() { print("teacher2") }
func teacher3() { print("teacher3") }
}
- 通过SIL+断点调试,发现
@objc
修饰的方法是函数表调度
【小技巧】:混编
头文件查看方式:查看项目名-Swift.h
头文件
- 如果只是通过 @objc修饰函数,OC还是无法调用swift方法的,因此如果想要
OC访问swift
,class需要继承NSObject
class HTTeacher: NSObject {
@objc func teacher() { print("teacher") }
func teacher1() { print("teacher1") }
func teacher2() { print("teacher2") }
func teacher3() { print("teacher3") }
}
SWIFT_CLASS("_TtC11HTSwiftDemo9HTTeacher")
@interface HTTeacher : NSObject
- (void)teacher;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end
查看 SIL
文件发现被 @objc
修饰的函数声明有两个:swift + OC(内部调用的swift中的teach函数)
即在SIL文件中生成了两个方法
- swift原有的函数
-
@objc
标记暴露给OC来使用的函数: 内部调用swift原有函数
dynamic 修饰
以下面代码为例,查看 dynamic
修饰的函数的调度方式
class HTTeacher: NSObject {
dynamic func teacher() { print("teacher") }
func teacher1() { print("teacher1") }
func teacher2() { print("teacher2") }
func teacher3() { print("teacher3") }
}
- 其中 teach函数的调度还是
函数表调度
,可以通过断点调试验证,使用dynamic
的意思是可以动态修改
,意味着当类继承自NSObject时,可以使用method-swizzling
@objc + dynamic
class HTTeacher: NSObject {
@objc dynamic func teacher() { print("teacher") }
func teacher1() { print("teacher1") }
func teacher2() { print("teacher2") }
func teacher3() { print("teacher3") }
}
- 通过断点调试,走的是
objc_msgSend
流程,即动态消息转发
swift中实现方法交换
在swift中的需要交换的函数前,使用dynamic修饰,然后通过: @_dynamicReplacement(for: 函数符号)
进行交换,如下所示
class HTTeacher {
dynamic func teacher() { print("teacher") }
func teacher1() { print("teacher1") }
func teacher2() { print("teacher2") }
func teacher3() { print("teacher3") }
}
extension HTTeacher {
@_dynamicReplacement(for: teacher)
func teacher5() {
print("teacher5")
}
}
var t = HTTeacher()
t.teacher()
将 teacher()
方法替换成了 teacher5
- 如果
teacher()
方法没有实现 或者没有dynamic
修饰符,会报错
方法调度总结
-
struct
是值类型
,其中函数的调度属于直接调用地址
,即静态调度
-
class
是引用
类型,其中函数的调度是通过V-Table函数表
来进行调度的,即动态调度
-
extension
中的函数调度方式是直接调度
-
final
修饰的函数调度方式是直接调度
-
@objc
修饰的函数调度方式是函数表调度
,如果OC中需要使用,class还必须继承NSObject
-
dynamic
修饰的函数的调度方式是函数表调度
,使函数具有动态性
-
@objc + dynamic
组合修饰的函数调度,是执行的是objc_msgSend
流程,即动态消息转发
内存插件 libfooplugin.dylib的使用
安装和使用
方式一
- 在根目录下创建 .lldbinit 文件:
vim /.lldbinit
- 然后输入
plugin load libfooplugin.dylib路径
方式二
- 在通过lldb调试的时候,直接输入
plugin load libfooplugin.dylib路径
使用
- 在
lldb
环境下,通过cat address 地址
使用
内存分区调试实践
堆区
对于堆区的内存来说,就是通过 new & malloc 关键字来申请的内存空间,不连续,类似链表的结构,最直观的就是类的实例对象。
定义代码如下,通过cat
查看类实例的内存分区
class HTTeahcer {
func teacher() {
print("teacher")
}
}
var t = HTTeahcer()
从上图可以看出,类的实例对象存储在堆区,即
heap pointer
栈区
查看以下代码的 age
内存地址位于哪个区?
func test() {
// 我们在函数内部声明的age变量就是一个局部变量
var age: Int = 18
print(age)
}
test()
从结果来看,
age
位于栈区,即 stack address
,此处的age
如果用 let
修饰,取不到地址
全局区
对于C的分析
下面是C语言的部分代码,查看其变量的内存地址
//全局已初始化变量
int a = 10;
//全局未初始化变量
int age;
//全局静态变量
static int age2 = 30;
int main(int argc, const char * argv[]) {
char *p = "CJLTeacher";
printf("%d", a);
printf("%d", age2);
return 0;
}
-
查看
a(全局已初始化变量)
的内存地址
其中__DATA.__data
表示segment.section
,这里的位置和全局区并不冲突,因为一个是人为的内存分配(内存布局分区)
,一个是 Mach-O的segment.section
段中,是文件的格式划分
-
查看
age(全局未初始化变量)
的内存地址
age
在Mach-O文件中,放在了__DATA.__common
段,主要放的就是未初始化的符号声明(mach-o相比内存划分更细,主要是为了更好的定位符号),当然此时的age
在内存中依然在全局区
-
查看
age2(全局已初始化静态变量)
的内存地址(其中需要注意:age2必须使用才能找到,否则会报错)
-
观察3个变量的地址,其地址都是相邻的,因为在内存中都放在了
全局区
,观察其内存地址,可以发现,在全局区中,未初始化变量地址
比已初始化变量地址
高
-
如果定义了一个
char *p = "CJLTeacher"
,查看*p
,Mach-O
存储在__TEXT.cstring
段,内存中存储在常量区
-
如果是
const
修饰的变量呢?存放在Mach-O文件中的__TEXT.__const
段
-
如果使用static + const修饰变量,此时变量在哪?
查看age4
的内存地址,地址特别大,而且使用cat
查看不了,因为mach-o没有记录,age4
就是50,即使用static+const
修饰的变量就相当于直接替换
对于Swift的分析
let age = 10
-
对于
let
修饰的变量,由于是不可变的
,所以不能通过po+cat
查看内存,通过汇编 首地址+偏移
来获取age
的内存,发现是在Mach-O的__DATA.__common
段
从这里可以发现,这与C中是有所区别的。swift的不同之处:已经初始化的全局变量放在__DATA.__common
段,猜测是因为age
开始是被标记为未初始化
的,当我们执行代码之后才将10
存储到对应的内存地址中
如果是
var
修饰的变量呢?可以发现与let
是一致的,还是__DATA.__common
段
var age2 = 20
总结
- 对于C语言中全局变量,根据是否已经初始化,存储在Mach-O中存储位置是不同的
-
已
初始化的全局变量:__DATA.__data
-
未
初始化的全局变量:__DATA.__common
-
已
初始化的全局静态
变量,即static
修饰:__DATA.__data
- 对于
char *p
类型的字符:__TEXT.cstring
-
const
修饰的全局变量:__TEXT.__const
-
static+const
修饰的全局变量:Mach-O中没有记录
-
- 对于 Swift中的全局变量
-
let
修饰的全局变量:__DATA.__common -
var
修饰的全局变量:__DATA.__common
-