內(nèi)存分區(qū)
內(nèi)存五大區(qū)
- 內(nèi)存分區(qū)按地址從高到低排列:
棧區(qū)->堆區(qū)->全局靜態(tài)區(qū)->常量區(qū)->代碼區(qū)棧區(qū)的地址比堆區(qū)的地址大很多棧區(qū)從高地址往低地址分配空間,堆區(qū)、全局靜態(tài)區(qū)、常量區(qū)、代碼區(qū)都是從低地址往高地址分配空間- 當(dāng)
棧區(qū)與堆區(qū)邊界碰撞,就會(huì)出現(xiàn)開發(fā)中的溢出。
棧區(qū)
棧區(qū)Stack:棧區(qū)
- 從高地址往低地址分配空間,向下延伸,是連續(xù)的內(nèi)存空間
- 棧區(qū)存放局部變量、函數(shù)調(diào)用上下文,由系統(tǒng)自動(dòng)管理,使用完由系統(tǒng)回收
堆區(qū)
堆區(qū)Heap:堆區(qū)
- 從低地址往高地址分配空間,向上延伸,堆空間是不連續(xù)的,結(jié)構(gòu)類似鏈表
- 通過
new、malloc在堆區(qū)分配內(nèi)存空間,由開發(fā)者手動(dòng)管理,使用完手動(dòng)釋放
全局靜態(tài)區(qū)
使用
c語言測(cè)試全局靜態(tài)區(qū)a、b、c都在全局靜態(tài)區(qū)
- 從低地址往高地址分配空間
- 已初始化的全局變量,存儲(chǔ)在
__DATA.__data段- 未初始化的全局變量,存儲(chǔ)在
__DATA.__common段- 未初始化比已初始化的全局變量地址更高
swift和c的差異在Swift和C的差異main.swift中定義變量age1和常量age2。
age1可以正常獲取地址并打印,它存儲(chǔ)在__DATA.__common段age2由于是不可變,不允許使用withUnsafePointer獲取地址
使用斷點(diǎn)查看匯編代碼尋找
age2的地址通過匯編代碼首地址+偏移地址,找到age2地址并打印,它同樣存儲(chǔ)在__DATA.__common段
常量區(qū)
使用
c語言測(cè)試常量區(qū)a、b都在常量區(qū)
- 從低地址往高地址分配空間
- 常量存儲(chǔ)在
__DATA.__data段
查看硬編碼的字符串存放位置
char *p="Zang";
上述代碼中的字符串
"Zang"存儲(chǔ)在哪里?通過查看硬編碼的字符串存放位置Mach-O文件,"Zang"存儲(chǔ)在__TEXT.__cstring段,內(nèi)存分區(qū)中的常量區(qū)
代碼區(qū)
代碼段代碼區(qū)__TEXT.__text:里面存放了要執(zhí)行的匯編代碼。每一個(gè)swift文件都會(huì)經(jīng)過編譯,然后匯編形成.o文件(目標(biāo)文件),最終.o文件會(huì)合成為一個(gè)文件,當(dāng)前代碼會(huì)按照鏈接順序依次在.o文件里排列好,放在.o文件的__TEXT.__text段。
使用
static const修飾的變量使用
c語言測(cè)試使用static const修飾的變量
a處于全局區(qū),存儲(chǔ)在__DATA.__data段b處于常量區(qū),存儲(chǔ)在__DATA.__data段c提示找不到地址,因?yàn)槭褂?code>static const修飾的變量,Mach-O沒有記錄。c實(shí)際只是一個(gè)別名,沒有獨(dú)立內(nèi)存空間
方法調(diào)度
靜態(tài)調(diào)度
值類型的函數(shù)調(diào)用方式是靜態(tài)調(diào)度。
例如結(jié)構(gòu)體中的?法調(diào)度就是靜態(tài)調(diào)度,通過地址直接調(diào)用。在編譯、鏈接完成之后,當(dāng)前的函數(shù)地址就已經(jīng)確定,存放在代碼段__TEXT.__text,結(jié)構(gòu)體內(nèi)并不存儲(chǔ)函數(shù)地址。
struct LGTeacher{
func test() {
print("test")
}
}
var t=LGTeacher()
t.test();
通過斷點(diǎn)查看匯編代碼:
函數(shù)地址在編譯、鏈接后已經(jīng)確定,通過函數(shù)地址callq指令的跳轉(zhuǎn),直接地址調(diào)用。
打開
Mach-O文件:
函數(shù)地址存儲(chǔ)在代碼段Mach-O__TEXT.__text,而結(jié)構(gòu)體內(nèi)并不存儲(chǔ)函數(shù)地址。
函數(shù)地址后面的符號(hào),又是如何存儲(chǔ)的?
符號(hào)
打開Mach-O文件,來到Symbol Table:符號(hào)存儲(chǔ)在Symbol TableSymbol Table符號(hào)表里面
Symbol Table:符號(hào)表,里面存儲(chǔ)的是符號(hào)位于String Table字符串表的偏移地址
命名重整:包含工程名、類名、函數(shù)名、參數(shù)、參數(shù)類型等信息
Symbol Table雖然是符號(hào)表,但里面并不直接存儲(chǔ)符號(hào)。
打開Mach-O文件,來到String Table:
符號(hào)字符串實(shí)際存儲(chǔ)在String TableString Table字符串表里面
String Table:字符串表,里面存儲(chǔ)了所有變量名和函數(shù)名,它們都以字符串形式進(jìn)行存儲(chǔ)。符號(hào)字符串也在其內(nèi)
通過首地址+偏移地址可以找到相應(yīng)符號(hào)
Dynamic Symbol Table:動(dòng)態(tài)庫函數(shù)位于符號(hào)表的偏移信息
Dynamic Symbol Table
通過命令操作符號(hào)表
查看符號(hào)表:
nm【Mach-O路徑】
查看符號(hào)表搜索符號(hào):
nm【Mach-O路徑】| grep【地址】
搜索符號(hào)還原符號(hào)名稱:
xcrun swift-demangle【符號(hào)】
還原符號(hào)名稱
還原符號(hào)表
Release模式編譯項(xiàng)目,Mach-O中的符號(hào)表只保留不能確定地址的符號(hào)。同時(shí)在可執(zhí)行文件目錄下,多出一個(gè).dSYM文件。因?yàn)殪o態(tài)鏈接的函數(shù),實(shí)際上是不需要符號(hào)的。一旦編譯完成,其地址確定后,當(dāng)前符號(hào)表會(huì)刪除當(dāng)前函數(shù)對(duì)應(yīng)的符號(hào)。這樣可以減小Mach-O文件的大小。
- 可執(zhí)行文件目錄下,多出一個(gè)
.dSYM文件
執(zhí)行文件目錄Release模式編譯后的Mach-O文件,符號(hào)表中的符號(hào)少了很多,只保留不能確定地址的符號(hào)
Release模式編譯后的Mach-O文件
什么是不能確定地址的符號(hào)?
打開
Mach-O文件,來到Lazy Symbol:
Lazy SymbolLazy Symbol:懶加載符號(hào)表,里面存儲(chǔ)不能確定地址的符號(hào)。它們是在運(yùn)行時(shí)才能確定,即函數(shù)第一次調(diào)用時(shí)。
例如
dyld_stub_bind確定地址,很遺憾我在Xcode Version 12.3版本中沒有找到
函數(shù)的命名重整規(guī)則
c語言:_函數(shù)名
原函數(shù)c語言cFunc,重整后函數(shù)符號(hào):_cFunc。簡(jiǎn)單的在函數(shù)名前面加_。所以c語言不允許函數(shù)重載,因?yàn)橹卣?guī)則過于簡(jiǎn)單,函數(shù)重載在編譯后根本無法區(qū)分。
oc:-[類名 函數(shù)名]
原函數(shù)ococFunc,重整后函數(shù)符號(hào):-[ocTest ocFunc]。對(duì)于oc來說,同樣不支持函數(shù)重載。
swift:包含工程名、類名、函數(shù)名、參數(shù)名、參數(shù)類型等信息
原函數(shù)swiftfunc test(abc : Int),重整后函數(shù)符號(hào):_$s4demo4test3abcySi_tF
原函數(shù)func test(abc : String),重整后函數(shù)符號(hào):_$s4demo4test3abcySS_tF
swift支持函數(shù)重載,它的命名重整規(guī)則也比c和oc復(fù)雜得多,包含工程名、類名、函數(shù)名、參數(shù)名、參數(shù)類型等信息,目的是確保函數(shù)符號(hào)的唯一性。
ASLR
ASLR:隨機(jī)地址偏移(address space layout randomizes)
每次APP啟動(dòng),都會(huì)隨機(jī)生成一個(gè)地址偏移值。造成編譯后Mach-O文件中的地址與App運(yùn)行時(shí)的地址產(chǎn)生偏差。
在
test方法上設(shè)置斷點(diǎn),使用真機(jī)運(yùn)行,可以看到運(yùn)行時(shí)test函數(shù)地址:0x100ab2cf8
運(yùn)行時(shí)函數(shù)地址
打開Mach-O文件,來到Symbol Table,搜索test,可以看到編譯時(shí)test函數(shù)地址:0x0100006CF8可以看到編譯時(shí)函數(shù)地址test函數(shù)地址,在運(yùn)行時(shí)和編譯時(shí)有明顯的差異
公式:
ASLR隨機(jī)偏移值 = 運(yùn)行時(shí)基地址 - 編譯時(shí)基地址- 運(yùn)行時(shí)函數(shù)地址 = 編譯時(shí)函數(shù)地址 +
ASLR隨機(jī)偏移值
首先找到
App運(yùn)行時(shí)基地址,使用image list打印鏡像文件的地址。第一個(gè)鏡像文件地址就是App運(yùn)行時(shí)的基地址:0x100aac000
運(yùn)行時(shí)基地址
再打開Mach-O文件,通過Load Comands->LC_SEGMENT_64(__TEXT)->VM Address,找到App編譯時(shí)的基地址:0x100000000編譯時(shí)的基地址
通過剛才的公式進(jìn)行驗(yàn)證:
ASLR隨機(jī)偏移值:0x100aac000-0x100000000=0x000aac000
運(yùn)行時(shí)函數(shù)地址:0x0100006CF8+0x000aac000=0x100ab2cf8通過公式進(jìn)行驗(yàn)證通過公式計(jì)算出的結(jié)果,和斷點(diǎn)里輸出的運(yùn)行時(shí)函數(shù)地址完全一致
動(dòng)態(tài)調(diào)度
結(jié)構(gòu)體中的?法都是靜態(tài)調(diào)度,而類中的方法通過
V-table函數(shù)表進(jìn)行調(diào)度,是動(dòng)態(tài)調(diào)度。
V-table在SIL文件中的格式:
//聲明sil vtable關(guān)鍵字
decl ::= sil-vtable
//sil vtable中包含的關(guān)鍵字、標(biāo)識(shí)(當(dāng)前的類名)、所有方法
sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'
//方法中包含了聲明以及函數(shù)名稱
sil-vtable-entry ::= sil-decl-ref ':' sil-linkage? sil-function-na me
通過?個(gè)簡(jiǎn)單的源?件進(jìn)行演示:
class LGTeacher{
func test1() {}
func test2() {}
func test3() {}
@objc deinit{}
init() {}
}
將上述代碼生成SIL文件:
swiftc -emit-sil main.swift | xcrun swift-demangle
LGTeacher函數(shù)表
- 首先
sil_vtable是關(guān)鍵字,后面LGTeacher表明當(dāng)前是LGTeacher Class的函數(shù)表- 其次就是當(dāng)前?法聲明對(duì)應(yīng)著?法名稱
- 函數(shù)表本質(zhì)可以理解為數(shù)組,聲明在
Class內(nèi)部的方法在不加任何關(guān)鍵字修飾的過程中,會(huì)連續(xù)存放在我們當(dāng)前的地址空間中
我們可以通過斷點(diǎn),查看匯編代碼進(jìn)行驗(yàn)證:
很明顯匯編驗(yàn)證test1、test2、test3這三個(gè)函數(shù),是連續(xù)存放在當(dāng)前的地址空間中
ARM64匯編指令
blr:帶返回的跳轉(zhuǎn)指令,跳轉(zhuǎn)到指令后邊跟隨寄存器中保存的地址mov:將某一寄存器的值復(fù)制到另一寄存器(只能用于寄存器與起存起或者寄存器與常量之間傳值,不能用于內(nèi)存地址)
mov x1, x0將寄存器x0的值復(fù)制到寄存器x1中ldr:將內(nèi)存中的值讀取到寄存器中
ldr x0, [x1, x2]將寄存器x1和寄存器x2相加作為地址,取該內(nèi)存地址的值翻入寄存器x0中str:將寄存器中的值寫入到內(nèi)存中
str x0, [x0, x8]將寄存器x0的值保存到內(nèi)存[x0 + x8]處bl:跳轉(zhuǎn)到某地址
我們還可以通過源碼進(jìn)行驗(yàn)證,搜索
initClassVTable,設(shè)置斷點(diǎn)并調(diào)試:
源碼驗(yàn)證initClassVTable的核心代碼,通過for循環(huán),從i等于0截止到VTableSize的大小。循環(huán)過程中,先通過offset+i偏移,再調(diào)用getMethod(i)得到對(duì)應(yīng)的method,將其存入偏移后的內(nèi)存中。從上述代碼可以看出,函數(shù)是連續(xù)存放在當(dāng)前的地址空間中。
extension中聲明的函數(shù),是通過V-table進(jìn)行調(diào)度嗎?
class LGTeacher {
func test1() {}
func test2() {}
func test3() {}
@objc deinit{}
init() {}
}
extension LGTeacher{
func test4() {}
}
通過斷點(diǎn),查看匯編代碼進(jìn)行驗(yàn)證:
extension中的函數(shù)調(diào)用extension中的函數(shù),并不是通過V-table函數(shù)表進(jìn)行調(diào)度,而是直接地址調(diào)用
子類繼承父類,函數(shù)表會(huì)變成什么樣?
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函數(shù)表
- 在
sil_vtable LGChild中,由子類聲明的函數(shù),被追加到父類函數(shù)下面。- 被子類重寫的父類函數(shù),位置不變,但被記錄為子類函數(shù)。
- 未被子類重寫的父類函數(shù),位置不變,依舊記錄為父類函數(shù)。
extension中的函數(shù),并不是通過V-table函數(shù)表進(jìn)行調(diào)度,也不能被子類重寫,只能被子類調(diào)用。
extension中的函數(shù),不通過V-table函數(shù)表調(diào)度而是直接地址調(diào)用,其原因在于編譯時(shí)無法將extension中的函數(shù)插入到該類函數(shù)表的正確位置。例如子類將父類的函數(shù)表繼承后,如果存在子類聲明的函數(shù),會(huì)繼續(xù)在連續(xù)地址中插入,也就是剛才看到的子類聲明的函數(shù)被追加到父類函數(shù)的下面。而聲明
extension在代碼中的位置無法確定,很有可能在子類編譯后才被讀取到。這時(shí)子類中并沒有指針記錄來區(qū)分哪些函數(shù)屬于子類、哪些函數(shù)屬于父類,故此extension中的函數(shù)無法正確插入到指定位置。這也是extension中的函數(shù)不能被子類重寫,只能被子類調(diào)用的原因。
final
使用
final修飾的方法,并不是通過V-table函數(shù)表進(jìn)行調(diào)度,而是直接地址調(diào)用。不能被子類重寫,只能被子類調(diào)用。
class LGTeacher {
final func test1() {}
func test2() {}
func test3() {}
@objc deinit{}
init() {}
}
將上述代碼生成SIL文件:
swiftc -emit-sil main.swift | xcrun swift-demangle
被LGTeacher函數(shù)表final修飾的test1方法,在函數(shù)表里不見了。修飾后的test1方法不再通過V-table進(jìn)?調(diào)度,變成直接地址調(diào)用。
我們可以通過斷點(diǎn),查看匯編代碼進(jìn)行驗(yàn)證:
匯編代碼驗(yàn)證final修飾的test1方法是直接地址調(diào)用。test2、test3方法首地址+偏移,是通過V-table函數(shù)表進(jìn)行調(diào)度。
@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
函數(shù)表沒有發(fā)生任何變化,被LGTeacher函數(shù)表@objc修飾的test1方法,依然通過V-table函數(shù)表進(jìn)行調(diào)度。
@objc修飾的方法,雖然調(diào)度方式?jīng)]有改變,但方法的聲明變成了兩個(gè)。
分別出現(xiàn)了方法的聲明swift的test1方法和oc的test1方法,而oc的test1方法內(nèi)部調(diào)用的還是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中導(dǎo)入頭文件,可以直接使用swift的類和方法ocTest.m
dynamic
使用
dynamic修飾的方法具有動(dòng)態(tài)特性,可動(dòng)態(tài)修改。調(diào)度方式?jīng)]有改變,依然通過V-table函數(shù)表進(jìn)行調(diào)度。
- 使用
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()
//輸出以下內(nèi)容:
//test2
方法未使用
dynamic修飾,使用@_dynamicReplacement交換方法時(shí),編譯報(bào)錯(cuò)
未使用`dynamic`修飾方法
方法不存在,使用@_dynamicReplacement交換方法時(shí),編譯報(bào)錯(cuò)方法不存在
@objc + dynamic
使用
@objc + dynamic修飾方法,會(huì)改變方法的調(diào)度方式。
class LGTeacher {
@objc dynamic func test1() {}
func test2() {}
func test3() {}
@objc deinit{}
init() {}
}
我們可以通過斷點(diǎn),查看匯編代碼進(jìn)行驗(yàn)證:
匯編代碼驗(yàn)證test1方法的調(diào)用方式,變?yōu)橄⒄{(diào)度,使用objc_msgSend動(dòng)態(tài)消息轉(zhuǎn)發(fā)
總結(jié):
- 值類型的函數(shù)調(diào)用方式是靜態(tài)調(diào)度
- 引用類型通過
V-table函數(shù)表進(jìn)行調(diào)度,是動(dòng)態(tài)調(diào)度extension中的函數(shù)調(diào)用方式是靜態(tài)調(diào)度final修飾的函數(shù)調(diào)用方式是靜態(tài)調(diào)度@objc修飾的函數(shù)通過V-table函數(shù)表進(jìn)行調(diào)度,是動(dòng)態(tài)調(diào)度dynamic修飾的函數(shù)通過V-table函數(shù)表進(jìn)行調(diào)度,是動(dòng)態(tài)調(diào)度@objc + dynamic修飾的函數(shù)調(diào)用方式是消息調(diào)度,使用objc_msgSend動(dòng)態(tài)消息轉(zhuǎn)發(fā)













































