内存分区
内存五大区
- 内存分区按地址从高到低排列:
栈区->堆区->全局静态区->常量区->代码区栈区的地址比堆区的地址大很多栈区从高地址往低地址分配空间,堆区、全局静态区、常量区、代码区都是从低地址往高地址分配空间- 当
栈区与堆区边界碰撞,就会出现开发中的溢出。
栈区
栈区
Stack:栈区
- 从高地址往低地址分配空间,向下延伸,是连续的内存空间
- 栈区存放局部变量、函数调用上下文,由系统自动管理,使用完由系统回收
堆区
堆区
Heap:堆区
- 从低地址往高地址分配空间,向上延伸,堆空间是不连续的,结构类似链表
- 通过
new、malloc在堆区分配内存空间,由开发者手动管理,使用完手动释放
全局静态区
使用
c语言测试全局静态区
a、b、c都在全局静态区
- 从低地址往高地址分配空间
- 已初始化的全局变量,存储在
__DATA.__data段- 未初始化的全局变量,存储在
__DATA.__common段- 未初始化比已初始化的全局变量地址更高
swift和c的差异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语言测试使用static const修饰的变量
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文件:
Mach-O 函数地址存储在代码段
__TEXT.__text,而结构体内并不存储函数地址。
函数地址后面的符号,又是如何存储的?
符号
打开Mach-O文件,来到Symbol Table:Symbol Table 符号存储在
Symbol Table符号表里面
Symbol Table:符号表,里面存储的是符号位于String Table字符串表的偏移地址
命名重整:包含工程名、类名、函数名、参数、参数类型等信息
Symbol Table虽然是符号表,但里面并不直接存储符号。
打开Mach-O文件,来到String Table:
String Table 符号字符串实际存储在
String Table字符串表里面
String Table:字符串表,里面存储了所有变量名和函数名,它们都以字符串形式进行存储。符号字符串也在其内
通过首地址+偏移地址可以找到相应符号
Dynamic Symbol Table:动态库函数位于符号表的偏移信息
Dynamic Symbol Table
通过命令操作符号表
查看符号表:
nm【Mach-O路径】
查看符号表
搜索符号:
nm【Mach-O路径】| grep【地址】
搜索符号
还原符号名称:
xcrun swift-demangle【符号】
还原符号名称
还原符号表
Release模式编译项目,Mach-O中的符号表只保留不能确定地址的符号。同时在可执行文件目录下,多出一个.dSYM文件。因为静态链接的函数,实际上是不需要符号的。一旦编译完成,其地址确定后,当前符号表会删除当前函数对应的符号。这样可以减小Mach-O文件的大小。
- 可执行文件目录下,多出一个
.dSYM文件
执行文件目录
Release模式编译后的Mach-O文件,符号表中的符号少了很多,只保留不能确定地址的符号
Release模式编译后的Mach-O文件
什么是不能确定地址的符号?
打开
Mach-O文件,来到Lazy Symbol:
Lazy Symbol
Lazy Symbol:懒加载符号表,里面存储不能确定地址的符号。它们是在运行时才能确定,即函数第一次调用时。
例如
dyld_stub_bind确定地址,很遗憾我在Xcode Version 12.3版本中没有找到
函数的命名重整规则
c语言:_函数名
c语言 原函数
cFunc,重整后函数符号:_cFunc。简单的在函数名前面加_。所以c语言不允许函数重载,因为重整规则过于简单,函数重载在编译后根本无法区分。
oc:-[类名 函数名]
oc 原函数
ocFunc,重整后函数符号:-[ocTest ocFunc]。对于oc来说,同样不支持函数重载。
swift:包含工程名、类名、函数名、参数名、参数类型等信息
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
LGTeacher函数表
- 首先
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中的函数调用
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
LGChild函数表
- 在
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
LGTeacher函数表 被
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
LGTeacher函数表 函数表没有发生任何变化,被
@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代码demo-Swift.h
在ocTest.m中导入头文件,可以直接使用swift的类和方法ocTest.m
dynamic
使用
dynamic修饰的方法具有动态特性,可动态修改。调度方式没有改变,依然通过V-table函数表进行调度。
- 使用
dynamic修饰方法,如果Class继承NSObject,可以使用method-swizzlingswift中的方法交换:使用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交换方法时,编译报错
未使用`dynamic`修饰方法
方法不存在,使用@_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动态消息转发

内存五大区
栈区
堆区
全局静态区
Swift和C的差异
在
汇编代码
通过
常量区
硬编码的字符串存放位置
通过查看
代码区
代码段
使用static const修饰的变量
函数地址
函数地址在编译、链接后已经确定,通过
Mach-O
函数地址存储在代码段
符号
Symbol Table
符号存储在
String Table
符号字符串实际存储在
Dynamic Symbol Table
查看符号表
搜索符号
还原符号名称
执行文件目录
Release模式编译后的Mach-O文件
Lazy Symbol
print
c语言
原函数
oc
原函数
swift
原函数
运行时函数地址
编译时函数地址
可以看到
运行时基地址
编译时的基地址
通过公式进行验证
LGTeacher函数表
汇编验证
很明显
源码验证
extension中的函数调用
LGChild函数表
LGTeacher函数表
被
汇编代码验证
LGTeacher函数表
函数表没有发生任何变化,被
方法的声明
分别出现了
找到桥接文件
demo-Swift.h
ocTest.m
未使用`dynamic`修饰方法
方法不存在
汇编代码验证











网友评论