靜態(tài)派發(fā)
值類型對(duì)象的函數(shù)的調(diào)用方式是靜態(tài)調(diào)用,即直接地址調(diào)用,調(diào)用函數(shù)指針,這個(gè)函數(shù)指針在編譯、鏈接完成之后就已經(jīng)確定了,存放在代碼段,而結(jié)構(gòu)體內(nèi)部并不存放方法。因此可以通過地址直接調(diào)用
- 結(jié)構(gòu)體函數(shù)符號(hào)調(diào)試如下:

- 打開
Mach-O可執(zhí)行文件,其中的__text段,就是所謂的代碼段,需要執(zhí)行的匯編指令都在這里

對(duì)于上面的分析,有個(gè)疑問:直接地址調(diào)用后面是符號(hào),這個(gè)符號(hào)是怎么來(lái)的?

- 是從
Mach-O文件的符號(hào)表 Symbol Table,但是符號(hào)表中并不存儲(chǔ)字符串,字符串存儲(chǔ)在字符串表 String Table(存放所有的變量名和函數(shù)名,以字符串形式存儲(chǔ)),然后根據(jù)符號(hào)表中的偏移值到字符串表中查找對(duì)應(yīng)的字符,然后進(jìn)行命名重整

-
Symbol Table: 存儲(chǔ)符號(hào)位于字符串表的位置 -
Dynamic Symbol Table:動(dòng)態(tài)庫(kù)函數(shù)位于符號(hào)表的偏移位置
還可以通過終端命令nm,獲取項(xiàng)目中的符號(hào)表
查看符號(hào)表:
nm mach-o文件路徑通過命令還原符號(hào)名稱:
xcrun swift-demangle 符號(hào)將
Edit Scheme中的Debug改成Release,編譯后查看,在可執(zhí)行文件目錄下,多了一個(gè)后綴為dSYM的文件,此時(shí),再去Mach-o文件中查找teach符號(hào),發(fā)現(xiàn)是找不到的。 其主要原因是因?yàn)?code>靜態(tài)鏈接的函數(shù),實(shí)際上是不需要符號(hào)的,一旦編譯完成,其地址確定后,當(dāng)前的符號(hào)表就會(huì)刪除當(dāng)前函數(shù)對(duì)應(yīng)的符號(hào),在release環(huán)境下,符號(hào)表中存儲(chǔ)的只是不能確定地址的符號(hào)對(duì)于不能確定地址的符號(hào),是在
運(yùn)行時(shí)確定的,即函數(shù)第一次調(diào)用時(shí)(相當(dāng)于懶加載),例如print,是通過dyld_stub_bind確定地址的
函數(shù)符號(hào)命名規(guī)則
- 對(duì)于
C函數(shù)來(lái)說(shuō),命名的重整規(guī)則就是在函數(shù)名之前加_(注意:C中不允許函數(shù)重載,因?yàn)闆]有辦法區(qū)分)
#include <stdio.h>
void test(){}

- 對(duì)于OC來(lái)說(shuō),也不支持函數(shù)重載,其符號(hào)命名規(guī)則是
-[類名 函數(shù)名] - 對(duì)于Swift來(lái)說(shuō),是允許函數(shù)重載,主要是因?yàn)閟wift中的
重整命名規(guī)則比較復(fù)雜,可以確保函數(shù)符號(hào)的唯一性
ASLR(隨機(jī)地址偏移)
新建一個(gè)iOS項(xiàng)目,在 ViewController中定義一下代碼
struct HTStack {
func teacher() {
print("teacher")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let t = HTStack()
t.teacher()
print("end")
}
}
- 運(yùn)行上述代碼,查看
mach-o文件,發(fā)現(xiàn)mach-o文件中的地址 與 函數(shù)調(diào)用的地址不一致,主要原因是實(shí)際調(diào)用時(shí)地址多了一個(gè)ASLR(地址空間布局隨機(jī)化 address space layout randomizes)

- 在
mach-o文件中查看,程序運(yùn)行靜態(tài)基地址(VM address) 是0x0000000100000000

- 可以通過
image list查看,其中0x100000000程序運(yùn)行的首地址,后八位是隨機(jī)地址偏移0x20ef000(即 ASLR)

- 函數(shù)地址等于
0x100000000(程序運(yùn)行首地址)+0x20ef000(ASLR) +0x3A30(符號(hào)表地址偏移)=0x1020F2A30

動(dòng)態(tài)派發(fā)
匯編指令補(bǔ)充
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)到某地址
探索class的調(diào)度方式
首先介紹下V_Table在SIL文件中的格式
//聲明sil vtable關(guān)鍵字
decl ::= sil-vtable
//sil vtable中包含 關(guān)鍵字、標(biāo)識(shí)(即類名)、所有的方法
2 sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'
//方法中包含了聲明以及函數(shù)名稱
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:關(guān)鍵字 -
HTTeacher:表示是 HTTeacher類的函數(shù)表 - 其次就是當(dāng)前方法的聲明對(duì)應(yīng)著方法的名稱
- 函數(shù)表 可以理解為
數(shù)組,聲明在 class內(nèi)部的方法在不加任何關(guān)鍵字修飾的過程中,是連續(xù)存放在我們當(dāng)前的地址空間中的。這一點(diǎn),可以通過斷點(diǎn)來(lái)印證
函數(shù)表源碼探索
下面來(lái)進(jìn)行函數(shù)表底層的源碼探索
- 源碼中搜索
initClassVTable,并加上斷點(diǎn),然后寫上源碼進(jìn)行調(diào)試
image - 其內(nèi)部是通過 for循環(huán)編碼,然后
offset+index偏移,然后獲取method,將其存入到偏移后的內(nèi)存中,從這里可以驗(yàn)證函數(shù)是連續(xù)存放的 - 對(duì)于class中函數(shù)來(lái)說(shuō),類的方法調(diào)度是通過
V-Taable,其本質(zhì)就是一個(gè)連續(xù)的內(nèi)存空間(數(shù)組結(jié)構(gòu))。
問題:如果更改方法聲明的位置呢?例如 extension中的函數(shù),此時(shí)的函數(shù)調(diào)度方式還是函數(shù)表調(diào)度嗎?
- 定義一個(gè) HTTeacher的
extension
extension HTTeacher {
func teacher4() { print("teacher4") }
}
- 再定義一個(gè)子類
HTStudent繼承自HTTeacher,查看SIL中的V-Table
class HTStudent: HTTeacher {}
- 查看 SIL文件,發(fā)現(xiàn)子類只繼承了class中定義的函數(shù),即函數(shù)表中的函數(shù)
image
其原因是因?yàn)?code>子類將父類的函數(shù)表全部繼承了,如果此時(shí)子類增加函數(shù),會(huì)繼續(xù)在連續(xù)的地址中插入,假設(shè)extension函數(shù)也是在函數(shù)表中,則意味著子類也有,但是子類無(wú)法并沒有相關(guān)的指針記錄函數(shù) 是父類方法 還是 子類方法,所以不知道方法該從哪里插入,導(dǎo)致extension中的函數(shù)無(wú)法安全的放入子類中。所以在這里可以側(cè)面證明extension中的方法是直接調(diào)用的,且只屬于類,子類是無(wú)法繼承的
開發(fā)注意點(diǎn):
- 需要繼承的
方法和屬性,不能寫在extension中。 - 而
extension中創(chuàng)建的函數(shù),一定是只屬于自己類,但是其子類也有其訪問權(quán)限,只是不能繼承和重寫
final、@objc、dynamic修飾函數(shù)
final 修飾
-
final修飾的方法是直接調(diào)度的,可以通過SIL驗(yàn)證 + 斷點(diǎn)驗(yàn)證
class HTTeacher {
final func teacher() { print("teacher") }
func teacher1() { print("teacher1") }
func teacher2() { print("teacher2") }
func teacher3() { print("teacher3") }
}

@objc 修飾
- 使用
@objc關(guān)鍵字是將swift中的方法暴露給OC
class HTTeacher {
@objc func teacher() { print("teacher") }
func teacher1() { print("teacher1") }
func teacher2() { print("teacher2") }
func teacher3() { print("teacher3") }
}
- 通過SIL+斷點(diǎn)調(diào)試,發(fā)現(xiàn)
@objc修飾的方法是函數(shù)表調(diào)度
image
【小技巧】:混編頭文件查看方式:查看項(xiàng)目名-Swift.h頭文件

- 如果只是通過 @objc修飾函數(shù),OC還是無(wú)法調(diào)用swift方法的,因此如果想要
OC訪問swift,class需要繼承NSObject
<!--swift類-->
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文件發(fā)現(xiàn)被 @objc修飾的函數(shù)聲明有兩個(gè):swift + OC(內(nèi)部調(diào)用的swift中的teach函數(shù))

即在SIL文件中生成了兩個(gè)方法
- swift原有的函數(shù)
-
@objc標(biāo)記暴露給OC來(lái)使用的函數(shù): 內(nèi)部調(diào)用swift原有函數(shù)
dynamic 修飾
以下面代碼為例,查看 dynamic修飾的函數(shù)的調(diào)度方式
class HTTeacher: NSObject {
dynamic func teacher() { print("teacher") }
func teacher1() { print("teacher1") }
func teacher2() { print("teacher2") }
func teacher3() { print("teacher3") }
}
- 其中 teach函數(shù)的調(diào)度還是
函數(shù)表調(diào)度,可以通過斷點(diǎn)調(diào)試驗(yàn)證,使用dynamic的意思是可以動(dòng)態(tài)修改,意味著當(dāng)類繼承自NSObject時(shí),可以使用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") }
}
- 通過斷點(diǎn)調(diào)試,走的是
objc_msgSend流程,即動(dòng)態(tài)消息轉(zhuǎn)發(fā)
image
swift中實(shí)現(xiàn)方法交換
在swift中的需要交換的函數(shù)前,使用dynamic修飾,然后通過: @_dynamicReplacement(for: 函數(shù)符號(hào))進(jìn)行交換,如下所示
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()方法沒有實(shí)現(xiàn) 或者沒有dynamic修飾符,會(huì)報(bào)錯(cuò)
image
方法調(diào)度總結(jié)
-
struct是值類型,其中函數(shù)的調(diào)度屬于直接調(diào)用地址,即靜態(tài)調(diào)度 -
class是引用類型,其中函數(shù)的調(diào)度是通過V-Table函數(shù)表來(lái)進(jìn)行調(diào)度的,即動(dòng)態(tài)調(diào)度 -
extension中的函數(shù)調(diào)度方式是直接調(diào)度 -
final修飾的函數(shù)調(diào)度方式是直接調(diào)度 -
@objc修飾的函數(shù)調(diào)度方式是函數(shù)表調(diào)度,如果OC中需要使用,class還必須繼承NSObject -
dynamic修飾的函數(shù)的調(diào)度方式是函數(shù)表調(diào)度,使函數(shù)具有動(dòng)態(tài)性 -
@objc + dynamic組合修飾的函數(shù)調(diào)度,是執(zhí)行的是objc_msgSend流程,即動(dòng)態(tài)消息轉(zhuǎn)發(fā)
內(nèi)存插件 libfooplugin.dylib的使用
安裝和使用
方式一
- 在根目錄下創(chuàng)建 .lldbinit ??????文件:
vim /.lldbinit - 然后輸入
plugin load libfooplugin.dylib路徑
方式二
- 在通過lldb調(diào)試的時(shí)候,直接輸入
plugin load libfooplugin.dylib路徑
使用
- 在
lldb環(huán)境下,通過cat address 地址使用
內(nèi)存分區(qū)調(diào)試實(shí)踐
堆區(qū)
對(duì)于堆區(qū)的內(nèi)存來(lái)說(shuō),就是通過 new & malloc 關(guān)鍵字來(lái)申請(qǐng)的內(nèi)存空間,不連續(xù),類似鏈表的結(jié)構(gòu),最直觀的就是類的實(shí)例對(duì)象。
定義代碼如下,通過cat查看類實(shí)例的內(nèi)存分區(qū)
class HTTeahcer {
func teacher() {
print("teacher")
}
}
var t = HTTeahcer()

從上圖可以看出,類的實(shí)例對(duì)象存儲(chǔ)在堆區(qū),即
heap pointer
棧區(qū)
查看以下代碼的 age內(nèi)存地址位于哪個(gè)區(qū)?
func test() {
// 我們?cè)诤瘮?shù)內(nèi)部聲明的age變量就是一個(gè)局部變量
var age: Int = 18
print(age)
}
test()

從結(jié)果來(lái)看,
age位于棧區(qū),即 stack address,此處的age如果用 let修飾,取不到地址
全局區(qū)
對(duì)于C的分析
下面是C語(yǔ)言的部分代碼,查看其變量的內(nèi)存地址
//全局已初始化變量
int a = 10;
//全局未初始化變量
int age;
//全局靜態(tài)變量
static int age2 = 30;
int main(int argc, const char * argv[]) {
char *p = "CJLTeacher";
printf("%d", a);
printf("%d", age2);
return 0;
}
-
查看
a(全局已初始化變量)的內(nèi)存地址
image
其中__DATA.__data表示segment.section,這里的位置和全局區(qū)并不沖突,因?yàn)橐粋€(gè)是人為的內(nèi)存分配(內(nèi)存布局分區(qū)),一個(gè)是 Mach-O的segment.section段中,是文件的格式劃分
image -
查看
age(全局未初始化變量)的內(nèi)存地址
image
age在Mach-O文件中,放在了__DATA.__common段,主要放的就是未初始化的符號(hào)聲明(mach-o相比內(nèi)存劃分更細(xì),主要是為了更好的定位符號(hào)),當(dāng)然此時(shí)的age在內(nèi)存中依然在全局區(qū) -
查看
age2(全局已初始化靜態(tài)變量)的內(nèi)存地址(其中需要注意:age2必須使用才能找到,否則會(huì)報(bào)錯(cuò))
image -
觀察3個(gè)變量的地址,其地址都是相鄰的,因?yàn)樵趦?nèi)存中都放在了
全局區(qū),觀察其內(nèi)存地址,可以發(fā)現(xiàn),在全局區(qū)中,未初始化變量地址比已初始化變量地址高
image -
如果定義了一個(gè)
char *p = "CJLTeacher",查看*p,Mach-O存儲(chǔ)在__TEXT.cstring段,內(nèi)存中存儲(chǔ)在常量區(qū)
image -
如果是
const修飾的變量呢?存放在Mach-O文件中的__TEXT.__const段
image -
如果使用static + const修飾變量,此時(shí)變量在哪?
image
查看age4的內(nèi)存地址,地址特別大,而且使用cat查看不了,因?yàn)閙ach-o沒有記錄,age4就是50,即使用static+const修飾的變量就相當(dāng)于直接替換
對(duì)于Swift的分析
let age = 10
-
對(duì)于
let修飾的變量,由于是不可變的,所以不能通過po+cat查看內(nèi)存,通過匯編 首地址+偏移來(lái)獲取age的內(nèi)存,發(fā)現(xiàn)是在Mach-O的__DATA.__common段
image
從這里可以發(fā)現(xiàn),這與C中是有所區(qū)別的。swift的不同之處:已經(jīng)初始化的全局變量放在__DATA.__common段,猜測(cè)是因?yàn)?age開始是被標(biāo)記為未初始化的,當(dāng)我們執(zhí)行代碼之后才將10存儲(chǔ)到對(duì)應(yīng)的內(nèi)存地址中 如果是
var修飾的變量呢?可以發(fā)現(xiàn)與let是一致的,還是__DATA.__common段
var age2 = 20

總結(jié)
- 對(duì)于C語(yǔ)言中全局變量,根據(jù)是否已經(jīng)初始化,存儲(chǔ)在Mach-O中存儲(chǔ)位置是不同的
-
已初始化的全局變量:__DATA.__data -
未初始化的全局變量:__DATA.__common -
已初始化的全局靜態(tài)變量,即static修飾:__DATA.__data - 對(duì)于
char *p類型的字符:__TEXT.cstring -
const修飾的全局變量:__TEXT.__const -
static+const修飾的全局變量:Mach-O中沒有記錄
-
- 對(duì)于 Swift中的全局變量
-
let修飾的全局變量:__DATA.__common -
var修飾的全局變量:__DATA.__common
-













