內存分區
內存五大區
- 內存分區按地址從高到低排列:
棧區
->堆區
->全局靜態區
->常量區
->代碼區
棧區
的地址比堆區
的地址大很多棧區
從高地址往低地址分配空間,堆區
、全局靜態區
、常量區
、代碼區
都是從低地址往高地址分配空間- 當
棧區
與堆區
邊界碰撞,就會出現開發中的溢出。
棧區
棧區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 TableSymbol Table
符號表里面
Symbol Table
:符號表,里面存儲的是符號位于String Table
字符串表的偏移地址
命名重整:包含工程名
、類名
、函數名
、參數
、參數類型
等信息
Symbol Table
雖然是符號表,但里面并不直接存儲符號。
打開Mach-O
文件,來到String Table
:
符號字符串實際存儲在String TableString 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 SymbolLazy Symbol
:懶加載符號表,里面存儲不能確定地址的符號。它們是在運行時才能確定,即函數第一次調用時。
例如
dyld_stub_bind
確定地址,很遺憾我在Xcode Version 12.3
版本中沒有找到
函數的命名重整規則
c
語言:_函數名
原函數c語言cFunc
,重整后函數符號:_cFunc
。簡單的在函數名前面加_
。所以c
語言不允許函數重載,因為重整規則過于簡單,函數重載在編譯后根本無法區分。
oc
:-[類名 函數名]
原函數ococFunc
,重整后函數符號:-[ocTest ocFunc]
。對于oc
來說,同樣不支持函數重載。
swift
:包含工程名
、類名
、函數名
、參數名
、參數類型
等信息
原函數swiftfunc 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-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
交換方法時,編譯報錯
未使用`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
動態消息轉發