内存分区
- 内存分区按地址从高到低排列:
栈区
->堆区
->全局静态区
->常量区
->代码区
栈区
的地址比堆区
的地址大很多栈区
从高地址往低地址分配空间,堆区
、全局静态区
、常量区
、代码区
都是从低地址往高地址分配空间- 当
栈区
与堆区
边界碰撞,就会出现开发中的溢出。
栈区
Stack
:栈区
- 从高地址往低地址分配空间,向下延伸,是连续的内存空间
- 栈区存放局部变量、函数调用上下文,由系统自动管理,使用完由系统回收
堆区
Heap
:堆区
- 从低地址往高地址分配空间,向上延伸,堆空间是不连续的,结构类似链表
- 通过
new
、malloc
在堆区分配内存空间,由开发者手动管理,使用完手动释放
全局静态区
使用
c
语言测试a
、b
、c
都在全局静态区
- 从低地址往高地址分配空间
- 已初始化的全局变量,存储在
__DATA.__data
段- 未初始化的全局变量,存储在
__DATA.__common
段- 未初始化比已初始化的全局变量地址更高
swift
和c
的差异在main.swift
中定义变量age1
和常量age2
。
age1
可以正常获取地址并打印,它存储在__DATA.__common
段age2
由于是不可变,不允许使用withUnsafePointer
获取地址
使用断点查看汇编代码寻找
age2
的地址通过首地址+偏移地址
,找到age2
地址并打印,它同样存储在__DATA.__common
段
常量区
使用
c
语言测试a
、b
都在常量区
- 从低地址往高地址分配空间
- 常量存储在
__DATA.__data
段
查看硬编码的字符串存放位置
char *p="Zang";
上述代码中的字符串
"Zang"
存储在哪里?通过查看Mach-O
文件,"Zang"
存储在__TEXT.__cstring
段,内存分区中的常量区
代码区
代码段__TEXT.__text
:里面存放了要执行的汇编代码。每一个swift
文件都会经过编译,然后汇编形成.o
文件(目标文件),最终.o
文件会合成为一个文件,当前代码会按照链接顺序依次在.o
文件里排列好,放在.o
文件的__TEXT.__text
段。
使用
static const
修饰的变量使用
c
语言测试
a
处于全局区,存储在__DATA.__data
段b
处于常量区,存储在__DATA.__data
段c
提示找不到地址,因为使用static const
修饰的变量,Mach-O
没有记录。c
实际只是一个别名,没有独立内存空间
方法调度
静态调度
值类型的函数调用方式是静态调度。
例如结构体中的⽅法调度就是静态调度,通过地址直接调用。在编译、链接完成之后,当前的函数地址就已经确定,存放在代码段__TEXT.__text
,结构体内并不存储函数地址。
struct LGTeacher{
func test() {
print("test")
}
}
var t=LGTeacher()
t.test();
通过断点查看汇编代码:
函数地址在编译、链接后已经确定,通过callq
指令的跳转,直接地址调用。
打开
Mach-O
文件:
函数地址存储在代码段__TEXT.__text
,而结构体内并不存储函数地址。
函数地址后面的符号,又是如何存储的?
打开Mach-O
文件,来到Symbol Table
:符号存储在Symbol Table
符号表里面
Symbol Table
:符号表,里面存储的是符号位于String Table
字符串表的偏移地址
命名重整:包含工程名
、类名
、函数名
、参数
、参数类型
等信息
Symbol Table
虽然是符号表,但里面并不直接存储符号。
打开Mach-O
文件,来到String Table
:
符号字符串实际存储在String Table
字符串表里面
String Table
:字符串表,里面存储了所有变量名和函数名,它们都以字符串形式进行存储。符号字符串也在其内
通过首地址+偏移地址
可以找到相应符号
Dynamic Symbol Table
:动态库函数位于符号表的偏移信息
通过命令操作符号表
查看符号表:
nm
【Mach-O
路径】
搜索符号:
nm
【Mach-O
路径】| grep
【地址】
还原符号名称:
xcrun swift-demangle
【符号】
还原符号表
Release
模式编译项目,Mach-O
中的符号表只保留不能确定地址的符号。同时在可执行文件目录下,多出一个.dSYM
文件。因为静态链接的函数,实际上是不需要符号的。一旦编译完成,其地址确定后,当前符号表会删除当前函数对应的符号。这样可以减小Mach-O
文件的大小。
- 可执行文件目录下,多出一个
.dSYM
文件
Release
模式编译后的Mach-O
文件,符号表中的符号少了很多,只保留不能确定地址的符号
什么是不能确定地址的符号?
打开
Mach-O
文件,来到Lazy Symbol
:
Lazy Symbol
:懒加载符号表,里面存储不能确定地址的符号。它们是在运行时才能确定,即函数第一次调用时。
例如
dyld_stub_bind
确定地址,很遗憾我在Xcode Version 12.3
版本中没有找到
函数的命名重整规则
c
语言:_函数名
原函数cFunc
,重整后函数符号:_cFunc
。简单的在函数名前面加_
。所以c
语言不允许函数重载,因为重整规则过于简单,函数重载在编译后根本无法区分。
oc
:-[类名 函数名]
原函数ocFunc
,重整后函数符号:-[ocTest ocFunc]
。对于oc
来说,同样不支持函数重载。
swift
:包含工程名
、类名
、函数名
、参数名
、参数类型
等信息
原函数func test(abc : Int)
,重整后函数符号:_$s4demo4test3abcySi_tF
原函数func test(abc : String)
,重整后函数符号:_$s4demo4test3abcySS_tF
swift
支持函数重载,它的命名重整规则也比c
和oc
复杂得多,包含工程名
、类名
、函数名
、参数名
、参数类型
等信息,目的是确保函数符号的唯一性。
ASLR
ASLR
:随机地址偏移(address space layout randomizes
)
每次APP
启动,都会随机生成一个地址偏移值。造成编译后Mach-O
文件中的地址与App
运行时的地址产生偏差。
在
test
方法上设置断点,使用真机运行,可以看到运行时test
函数地址:0x100ab2cf8
打开Mach-O
文件,来到Symbol Table
,搜索test
,可以看到编译时test
函数地址:0x0100006CF8
可以看到test
函数地址,在运行时和编译时有明显的差异
公式:
ASLR
随机偏移值 = 运行时基地址 - 编译时基地址- 运行时函数地址 = 编译时函数地址 +
ASLR
随机偏移值
首先找到
App
运行时基地址,使用image list
打印镜像文件的地址。第一个镜像文件地址就是App
运行时的基地址:0x100aac000
再打开Mach-O
文件,通过Load Comands
->LC_SEGMENT_64(__TEXT)
->VM Address
,找到App
编译时的基地址:0x100000000
通过刚才的公式进行验证:
ASLR
随机偏移值:0x100aac000
-0x100000000
=0x000aac000
运行时函数地址:0x0100006CF8
+0x000aac000
=0x100ab2cf8
通过公式计算出的结果,和断点里输出的运行时函数地址完全一致
动态调度
结构体中的⽅法都是静态调度,而类中的方法通过
V-table
函数表进行调度,是动态调度。
V-table
在SIL文件中的格式:
//声明sil vtable关键字
decl ::= sil-vtable
//sil vtable中包含的关键字、标识(当前的类名)、所有方法
sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'
//方法中包含了声明以及函数名称
sil-vtable-entry ::= sil-decl-ref ':' sil-linkage? sil-function-na me
通过⼀个简单的源⽂件进行演示:
class LGTeacher{
func test1() {}
func test2() {}
func test3() {}
@objc deinit{}
init() {}
}
将上述代码生成SIL文件:
swiftc -emit-sil main.swift | xcrun swift-demangle
- 首先
sil_vtable
是关键字,后面LGTeacher
表明当前是LGTeacher Class
的函数表- 其次就是当前⽅法声明对应着⽅法名称
- 函数表本质可以理解为数组,声明在
Class
内部的方法在不加任何关键字修饰的过程中,会连续存放在我们当前的地址空间中
我们可以通过断点,查看汇编代码进行验证:
很明显test1
、test2
、test3
这三个函数,是连续存放在当前的地址空间中
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
:跳转到某地址
我们还可以通过源码进行验证,搜索
initClassVTable
,设置断点并调试:
initClassVTable
的核心代码,通过for
循环,从i
等于0
截止到VTableSize
的大小。循环过程中,先通过offset+i
偏移,再调用getMethod(i)
得到对应的method
,将其存入偏移后的内存中。从上述代码可以看出,函数是连续存放在当前的地址空间中。
extension中声明的函数,是通过V-table进行调度吗?
class LGTeacher {
func test1() {}
func test2() {}
func test3() {}
@objc deinit{}
init() {}
}
extension LGTeacher{
func test4() {}
}
通过断点,查看汇编代码进行验证:
extension
中的函数,并不是通过V-table
函数表进行调度,而是直接地址调用
子类继承父类,函数表会变成什么样?
class LGTeacher {
func test1() {}
func test2() {}
func test3() {}
@objc deinit{}
init() {}
}
class LGChild : LGTeacher {
override func test2() {}
func test5() {}
}
extension LGTeacher{
func test4() {}
}
将上述代码生成SIL文件:
swiftc -emit-sil main.swift | xcrun swift-demangle
- 在
sil_vtable LGChild
中,由子类声明的函数,被追加到父类函数下面。- 被子类重写的父类函数,位置不变,但被记录为子类函数。
- 未被子类重写的父类函数,位置不变,依旧记录为父类函数。
extension
中的函数,并不是通过V-table
函数表进行调度,也不能被子类重写,只能被子类调用。
extension
中的函数,不通过V-table
函数表调度而是直接地址调用,其原因在于编译时无法将extension
中的函数插入到该类函数表的正确位置。例如子类将父类的函数表继承后,如果存在子类声明的函数,会继续在连续地址中插入,也就是刚才看到的子类声明的函数被追加到父类函数的下面。而声明
extension
在代码中的位置无法确定,很有可能在子类编译后才被读取到。这时子类中并没有指针记录来区分哪些函数属于子类、哪些函数属于父类,故此extension
中的函数无法正确插入到指定位置。这也是extension
中的函数不能被子类重写,只能被子类调用的原因。
final
使用
final
修饰的方法,并不是通过V-table
函数表进行调度,而是直接地址调用。不能被子类重写,只能被子类调用。
class LGTeacher {
final func test1() {}
func test2() {}
func test3() {}
@objc deinit{}
init() {}
}
将上述代码生成SIL文件:
swiftc -emit-sil main.swift | xcrun swift-demangle
被final
修饰的test1
方法,在函数表里不见了。修饰后的test1
方法不再通过V-table
进⾏调度,变成直接地址调用。
我们可以通过断点,查看汇编代码进行验证:
final
修饰的test1
方法是直接地址调用。test2
、test3
方法首地址+偏移
,是通过V-table
函数表进行调度。
@objc
使用
@objc
修饰可以将swift
方法暴露给oc
使用。
class LGTeacher {
@objc func test1() {}
func test2() {}
func test3() {}
@objc deinit{}
init() {}
}
将上述代码生成SIL文件:
swiftc -emit-sil main.swift | xcrun swift-demangle
函数表没有发生任何变化,被@objc
修饰的test1
方法,依然通过V-table
函数表进行调度。
@objc
修饰的方法,虽然调度方式没有改变,但方法的声明变成了两个。
分别出现了swift
的test1
方法和oc
的test1
方法,而oc
的test1
方法内部调用的还是swift
的test1
方法。
演示一下
oc
如何访问swift
的方法:
class LGTeacher : NSObject {
@objc func test1() {}
func test2() {}
func test3() {}
@objc deinit{}
override init() {}
}
方法只通过
@objc
修饰方法,oc
并不能访问到,还要将Class
继承NSObject
在
main.swift
里写入上述代码,编译后找到桥接文件
打开桥接文件,可以看到被@objc
修饰的方法和属性都生成了oc
代码
在ocTest.m
中导入头文件,可以直接使用swift
的类和方法
dynamic
使用
dynamic
修饰的方法具有动态特性,可动态修改。调度方式没有改变,依然通过V-table
函数表进行调度。
- 使用
dynamic
修饰方法,如果Class
继承NSObject
,可以使用method-swizzling
swift
中的方法交换:使用dynamic
修饰方法,使用@_dynamicReplacement
交换方法
演示一下
swift
中的方法交换:
class LGTeacher {
dynamic func test1() {
print("test1")
}
}
extension LGTeacher{
@_dynamicReplacement(for:test1)
func test2() {
print("test2")
}
}
var t = LGTeacher()
t.test1()
//输出以下内容:
//test2
方法未使用
dynamic
修饰,使用@_dynamicReplacement
交换方法时,编译报错
方法不存在,使用@_dynamicReplacement
交换方法时,编译报错
@objc + dynamic
使用
@objc + dynamic
修饰方法,会改变方法的调度方式。
class LGTeacher {
@objc dynamic func test1() {}
func test2() {}
func test3() {}
@objc deinit{}
init() {}
}
我们可以通过断点,查看汇编代码进行验证:
test1
方法的调用方式,变为消息调度,使用objc_msgSend
动态消息转发
总结:
- 值类型的函数调用方式是静态调度
- 引用类型通过
V-table
函数表进行调度,是动态调度extension
中的函数调用方式是静态调度final
修饰的函数调用方式是静态调度@objc
修饰的函数通过V-table
函数表进行调度,是动态调度dynamic
修饰的函数通过V-table
函数表进行调度,是动态调度@objc + dynamic
修饰的函数调用方式是消息调度,使用objc_msgSend
动态消息转发