靜態派發
值類型對象的函數的調用方式是靜態調用
,即直接地址調用
,調用函數指針,這個函數指針在編譯、鏈接完成之后就已經確定了
,存放在代碼段
,而結構體內部并不存放方法。因此可以通過地址直接調用
- 結構體函數符號調試如下:
- 打開
Mach-O
可執行文件,其中的__text
段,就是所謂的代碼段,需要執行的匯編指令都在這里
對于上面的分析,有個疑問:直接地址調用后面是符號
,這個符號是怎么來的?
- 是從
Mach-O
文件的符號表 Symbol Table
,但是符號表中并不存儲字符串
,字符串存儲在字符串表 String Table
(存放所有的變量名和函數名,以字符串形式存儲),然后根據符號表中的偏移值
到字符串表中
查找對應的字符,然后進行命名重整
-
Symbol Table
: 存儲符號位于字符串表的位置 -
Dynamic Symbol Table
:動態庫函數
位于符號表的偏移位置
還可以通過終端命令nm
,獲取項目中的符號表
查看符號表:
nm mach-o文件路徑
通過命令還原符號名稱:
xcrun swift-demangle 符號
將
Edit Scheme
中的Debug
改成Release
,編譯后查看,在可執行文件目錄下,多了一個后綴為dSYM
的文件,此時,再去Mach-o
文件中查找teach
符號,發現是找不到的。 其主要原因是因為靜態鏈接的函數,實際上是不需要符號的
,一旦編譯完成,其地址確定后,當前的符號表就會刪除當前函數對應的符號,在release環境下,符號表
中存儲的只是不能確定地址的符號
對于不能確定地址的符號,是在
運行時確定的
,即函數第一次調用時(相當于懶加載
),例如print
,是通過dyld_stub_bind
確定地址的
函數符號命名規則
- 對于
C函數
來說,命名的重整規則就是在函數名之前加_
(注意:C中不允許函數重載
,因為沒有辦法區分)
#include <stdio.h>
void test(){}
- 對于OC來說,也不支持函數重載,其符號命名規則是
-[類名 函數名]
- 對于Swift來說,是允許函數重載,主要是因為swift中的
重整命名規則
比較復雜,可以確保函數符號的唯一性
ASLR(隨機地址偏移)
新建一個iOS項目,在 ViewController
中定義一下代碼
struct HTStack {
func teacher() {
print("teacher")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let t = HTStack()
t.teacher()
print("end")
}
}
- 運行上述代碼,查看
mach-o
文件,發現mach-o
文件中的地址 與 函數調用的地址不一致,主要原因是實際調用時地址多了一個ASLR
(地址空間布局隨機化 address space layout randomizes)
- 在
mach-o
文件中查看,程序運行靜態基地址
(VM address) 是0x0000000100000000
- 可以通過
image list
查看,其中0x100000000
程序運行的首地址,后八位是隨機地址偏移0x20ef000
(即 ASLR)
- 函數地址等于
0x100000000
(程序運行首地址)+0x20ef000
(ASLR) +0x3A30
(符號表地址偏移)=0x1020F2A30
動態派發
匯編指令補充
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
:跳轉到某地址
探索class的調度方式
首先介紹下V_Table在SIL文件中的格式
//聲明sil vtable關鍵字
decl ::= sil-vtable
//sil vtable中包含 關鍵字、標識(即類名)、所有的方法
2 sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'
//方法中包含了聲明以及函數名稱
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
:關鍵字 -
HTTeacher
:表示是 HTTeacher類的函數表 - 其次就是當前方法的聲明對應著方法的名稱
- 函數表 可以理解為
數組
,聲明在 class內部的方法在不加任何關鍵字修飾的過程中,是連續存放
在我們當前的地址空間中的。這一點,可以通過斷點來印證
函數表源碼探索
下面來進行函數表底層
的源碼探索
- 源碼中搜索
initClassVTable
,并加上斷點,然后寫上源碼進行調試
image - 其內部是通過 for循環編碼,然后
offset+index
偏移,然后獲取method
,將其存入到偏移后的內存中,從這里可以驗證函數是連續存放的
- 對于class中函數來說,類的方法調度是通過
V-Taable
,其本質就是一個連續的內存空間(數組結構)。
問題:如果更改方法聲明的位置呢?例如 extension
中的函數,此時的函數調度方式還是函數表調度嗎?
- 定義一個 HTTeacher的
extension
extension HTTeacher {
func teacher4() { print("teacher4") }
}
- 再定義一個子類
HTStudent
繼承自HTTeacher
,查看SIL中的V-Table
class HTStudent: HTTeacher {}
- 查看 SIL文件,發現子類只繼承了class中定義的函數,即函數表中的函數
image
其原因是因為子類將父類的函數表全部繼承
了,如果此時子類增加函數,會繼續在連續的地址中插入,假設extension函數也是在函數表中
,則意味著子類也有,但是子類無法并沒有相關的指針記錄函數 是父類方法 還是 子類方法,所以不知道方法該從哪里插入
,導致extension中的函數無法安全的放入子類中。所以在這里可以側面證明extension中的方法是直接調用的,且只屬于類,子類是無法繼承的
開發注意點:
- 需要繼承的
方法和屬性
,不能寫在extension
中。 - 而
extension
中創建的函數,一定是只屬于自己類
,但是其子類也有其訪問權限
,只是不能繼承和重寫
final、@objc、dynamic修飾函數
final 修飾
-
final
修飾的方法是直接調度的
,可以通過SIL驗證 + 斷點驗證
class HTTeacher {
final func teacher() { print("teacher") }
func teacher1() { print("teacher1") }
func teacher2() { print("teacher2") }
func teacher3() { print("teacher3") }
}
@objc 修飾
- 使用
@objc
關鍵字是將swift
中的方法暴露給OC
class HTTeacher {
@objc func teacher() { print("teacher") }
func teacher1() { print("teacher1") }
func teacher2() { print("teacher2") }
func teacher3() { print("teacher3") }
}
- 通過SIL+斷點調試,發現
@objc
修飾的方法是函數表調度
image
【小技巧】:混編
頭文件查看方式:查看項目名-Swift.h
頭文件
- 如果只是通過 @objc修飾函數,OC還是無法調用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
文件發現被 @objc
修飾的函數聲明有兩個:swift + OC(內部調用的swift中的teach函數)
即在SIL文件中生成了兩個方法
- swift原有的函數
-
@objc
標記暴露給OC來使用的函數: 內部調用swift原有函數
dynamic 修飾
以下面代碼為例,查看 dynamic
修飾的函數的調度方式
class HTTeacher: NSObject {
dynamic func teacher() { print("teacher") }
func teacher1() { print("teacher1") }
func teacher2() { print("teacher2") }
func teacher3() { print("teacher3") }
}
- 其中 teach函數的調度還是
函數表調度
,可以通過斷點調試驗證,使用dynamic
的意思是可以動態修改
,意味著當類繼承自NSObject時,可以使用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") }
}
- 通過斷點調試,走的是
objc_msgSend
流程,即動態消息轉發
image
swift中實現方法交換
在swift中的需要交換的函數前,使用dynamic修飾,然后通過: @_dynamicReplacement(for: 函數符號)
進行交換,如下所示
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()
方法沒有實現 或者沒有dynamic
修飾符,會報錯
image
方法調度總結
-
struct
是值類型
,其中函數的調度屬于直接調用地址
,即靜態調度
-
class
是引用
類型,其中函數的調度是通過V-Table函數表
來進行調度的,即動態調度
-
extension
中的函數調度方式是直接調度
-
final
修飾的函數調度方式是直接調度
-
@objc
修飾的函數調度方式是函數表調度
,如果OC中需要使用,class還必須繼承NSObject
-
dynamic
修飾的函數的調度方式是函數表調度
,使函數具有動態性
-
@objc + dynamic
組合修飾的函數調度,是執行的是objc_msgSend
流程,即動態消息轉發
內存插件 libfooplugin.dylib的使用
安裝和使用
方式一
- 在根目錄下創建 .lldbinit ??????文件:
vim /.lldbinit
- 然后輸入
plugin load libfooplugin.dylib路徑
方式二
- 在通過lldb調試的時候,直接輸入
plugin load libfooplugin.dylib路徑
使用
- 在
lldb
環境下,通過cat address 地址
使用
內存分區調試實踐
堆區
對于堆區的內存來說,就是通過 new & malloc 關鍵字來申請的內存空間,不連續,類似鏈表的結構,最直觀的就是類的實例對象。
定義代碼如下,通過cat
查看類實例的內存分區
class HTTeahcer {
func teacher() {
print("teacher")
}
}
var t = HTTeahcer()
從上圖可以看出,類的實例對象存儲在堆區,即
heap pointer
棧區
查看以下代碼的 age
內存地址位于哪個區?
func test() {
// 我們在函數內部聲明的age變量就是一個局部變量
var age: Int = 18
print(age)
}
test()
從結果來看,
age
位于棧區,即 stack address
,此處的age
如果用 let
修飾,取不到地址
全局區
對于C的分析
下面是C語言的部分代碼,查看其變量的內存地址
//全局已初始化變量
int a = 10;
//全局未初始化變量
int age;
//全局靜態變量
static int age2 = 30;
int main(int argc, const char * argv[]) {
char *p = "CJLTeacher";
printf("%d", a);
printf("%d", age2);
return 0;
}
-
查看
a(全局已初始化變量)
的內存地址
image
其中__DATA.__data
表示segment.section
,這里的位置和全局區并不沖突,因為一個是人為的內存分配(內存布局分區)
,一個是 Mach-O的segment.section
段中,是文件的格式劃分
image -
查看
age(全局未初始化變量)
的內存地址
image
age
在Mach-O文件中,放在了__DATA.__common
段,主要放的就是未初始化的符號聲明(mach-o相比內存劃分更細,主要是為了更好的定位符號),當然此時的age
在內存中依然在全局區
-
查看
age2(全局已初始化靜態變量)
的內存地址(其中需要注意:age2必須使用才能找到,否則會報錯)
image -
觀察3個變量的地址,其地址都是相鄰的,因為在內存中都放在了
全局區
,觀察其內存地址,可以發現,在全局區中,未初始化變量地址
比已初始化變量地址
高
image -
如果定義了一個
char *p = "CJLTeacher"
,查看*p
,Mach-O
存儲在__TEXT.cstring
段,內存中存儲在常量區
image -
如果是
const
修飾的變量呢?存放在Mach-O文件中的__TEXT.__const
段
image -
如果使用static + const修飾變量,此時變量在哪?
image
查看age4
的內存地址,地址特別大,而且使用cat
查看不了,因為mach-o沒有記錄,age4
就是50,即使用static+const
修飾的變量就相當于直接替換
對于Swift的分析
let age = 10
-
對于
let
修飾的變量,由于是不可變的
,所以不能通過po+cat
查看內存,通過匯編 首地址+偏移
來獲取age
的內存,發現是在Mach-O的__DATA.__common
段
image
從這里可以發現,這與C中是有所區別的。swift的不同之處:已經初始化的全局變量放在__DATA.__common
段,猜測是因為age
開始是被標記為未初始化
的,當我們執行代碼之后才將10
存儲到對應的內存地址中
如果是
var
修飾的變量呢?可以發現與let
是一致的,還是__DATA.__common
段
var age2 = 20
總結
- 對于C語言中全局變量,根據是否已經初始化,存儲在Mach-O中存儲位置是不同的
-
已
初始化的全局變量:__DATA.__data
-
未
初始化的全局變量:__DATA.__common
-
已
初始化的全局靜態
變量,即static
修飾:__DATA.__data
- 對于
char *p
類型的字符:__TEXT.cstring
-
const
修飾的全局變量:__TEXT.__const
-
static+const
修飾的全局變量:Mach-O中沒有記錄
-
- 對于 Swift中的全局變量
-
let
修飾的全局變量:__DATA.__common -
var
修飾的全局變量:__DATA.__common
-