以前逆向部分的文章基礎知識和所需工具已經講述的差很少了 , 後續準備好實戰項目以及彙編和越獄部份內容繼續更新, 敬請關注 .git
目前準備更新一個底層系列文章 , 從
dyld
加載可執行文件到入口main
函數 , 到類 , 分類 , 協議等等的加載爲主線 . 一步步探索底層原理 .github本篇文章從方法的本質開始講述 , 前面少幾篇文章 , 後續補上 , 而後會準備一個目錄 .objective-c
說到任何關於 OC
本質的東西 , 咱們不得不提一下 Runtime
這個東西 .算法
官方文檔緩存
這裏只是簡單瞭解一下 Runtime
, 爲咱們探索方法本質提供一些幫助 , 後續更新詳細的 Runtime
機制和具體使用 . 安全
◈ Objective-C
擴展了 C
語言,並加入了面向對象特性和 Smalltalk
式的消息傳遞機制。而這個擴展的核心是一個用 C
和 編譯語言 寫的 Runtime
庫。它是 Objective-C
面向對象和動態機制的基石和根本。bash
◈ Objective-C
是一個動態語言,這意味着它不只須要一個編譯器,也須要一個運行時系統來動態得建立類和對象、進行消息傳遞和轉發。架構
◈ 理解 Objective-C
的 Runtime
機制能夠幫咱們更好的瞭解這個語言,適當的時候還能對語言進行擴展,從系統層面解決項目中的一些設計或技術問題。app
Runtime
其實有兩個版本:'modern'
和'legacy'
。咱們如今用的Objective-C 2.0
採用的是現行 ( Modern ) 版的Runtime
系統,只能運行在iOS
和macOS 10.5
以後的 64 位程序中。而macOS
較老的 32 位程序仍採用Objective-C 1
中的(早期) ( Legacy ) 版本的Runtime
系統。函數這兩個版本最大的區別在於 當你更改一個類的實例變量的佈局時,在早期版本中你須要從新編譯它的子類,而現行版就不須要。
Runtime
基本是用 C
和彙編寫的。你能夠在 這裏 下到蘋果維護的開源代碼。Apple
和 GNU
各自維護一個開源的 runtime 版本,這兩個版本之間都在努力的保持一致。
建議多多閱讀 .
Runtime
對於咱們普通開發者來講主要是根據其動態的機制 , 來實現各類各樣的需求 / 效果 . 簡單列舉一下 :
KVO
實現NSCoding
的自動歸檔和自動解檔MJExtension
)實際上根據 Runtime
的機制和其提供的 API
, 咱們能夠自由的運用 從而生成不一樣的功能 .
在 C
語言中,將代碼轉換爲可執行程序,通常要經歷三個步驟,即編譯、連接、運行。在連接的時候,對象的類型、方法的實現就已經肯定好了。
而在 Objective-C
中 , 因爲 LLVM
將一些在編譯和連接過程當中的工做,放到了運行階段。也就是說,就算是一個編譯好的 .ipa
包,在程序沒運行的時候,也不知道調用一個方法會發生什麼。這也爲後來大行其道的「熱修復」提供了可能 。
這樣的設計使 Objective-C
變得靈活,甚至可讓咱們在程序運行的時候,去動態修改一個方法的實現。
由此引出咱們今天方法本質的探索 -- 消息發送機制
關於這三種對象以前有篇文章裏面有較爲詳細的講述 , 本篇就很少贅述了 , 本系列文章中會繼續更新 類 / 對象的本質 . OC類對象/實例對象/元類解析
說了這麼多 , 下面咱們去除上帝視角 , 來從零開始一步步探索 OC
方法的完整流程 .
新建一個 Command Line
項目 , 代碼以下:
// main.m
#import <Foundation/Foundation.h>
@interface LBObject : NSObject
- (void)eat;
@end
@implementation LBObject
- (void)eat{
NSLog(@"eat");
}
@end
void run(){
NSLog(@"%s",__func__);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
LBObject * obj = [LBObject alloc];
[obj eat]; // OC 方法
run(); // C 函數
}
return 0;
}
複製代碼
clang -rewrite-objc main.m -o main.cpp
因爲 LLVM
自己就是內置的 clang
, 所以經過該命令咱們便可查看編譯後,運行前源碼轉換成了 C
以後的樣子 .
打開 main.cpp
, 直接拉到最底下 main
函數實現 .
上圖中咱們很清楚的看到 , run
函數在編譯期就肯定了函數調用 以及實現 . 而 OC
方法被編譯成調用objc_msgSend
函數. 這也就是咱們在 Runtime
所提到的 消息發送機制 .
LLVM
+ Runtime
使用這種作法以此來實現動態的可能 .
所以得出結論 :
OC
方法的本質就是調用 objc_msgSend
等函數 .
爲何說 '等函數' , 由於調用類方法 / 父類方法 都會有不一樣 . 例如 : objc_msgSendSuper
, objc_msgSend_stret
等等 .
經過編譯後代碼咱們看到 objc_msgSend
函數有兩個參數 id
, SEL
. id
顯然就是操做哪一個對象 . 而經過SEL
與 imp
的機制 , 以此實現了動態調用方法的本質 . 咱們稱這種機制爲 消息發送 .
LBObject *obj = [LBObject alloc];
[obj eat];
// 實例方法調用底層編譯
// 方法的本質: 消息 : 消息接受者 消息編號 ....參數 (消息體)
objc_msgSend(obj, sel_registerName("eat"));
複製代碼
objc_msgSend(objc_getClass("LBObject"), sel_registerName("eat"));
複製代碼
struct objc_super lbSuper;
lbSuper.receiver = obj;
lbSuper.super_class = [LBSuper class];
// __OBJC2__ 只需 receiver 和 super_class 便可
objc_msgSendSuper(&lbSuper, @selector(sayHello));
複製代碼
struct objc_super myClassSuper;
myClassSuper.receiver = [obj class];
myClassSuper.super_class = class_getSuperclass(object_getClass([obj class]));// 元類
objc_msgSendSuper(&myClassSuper, sel_registerName("test_classFunc"));
複製代碼
使用 objc_msgSend
函數要把校驗關閉 , 不然編譯就報錯了.
重頭戲終於來了 .
打開 objc4
源碼 . 可編譯objc4 源碼 , 密碼 r5v6 .
搜索 objc_msgSend
, 直接來到 objc-msg-arm64.s
的 ENTRY _objc_msgSend
中.
在彙編裏面,函數的入口格式是 ENTRY
+ 函數名 , 結束是 END_ENTRY
, 咱們這裏以 arm64
架構爲例
objc_msgSend
是使用匯編來寫的 , 爲何呢 ? 我的感受因爲如下緣由 :
C
語言做爲靜態語言 , 不可能經過一個函數來實現未知參數個數,類型而且跳轉到另外一個任意的函數指針的需求 .hook
, 咱們常用匯編來調用方法和實現函數.源碼以下 :
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
// person - isa - 類
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
//...
複製代碼
代碼太長我就不粘所有了 , 你們本身在源碼中看 .
整個彙編過程具體代碼就不帶着分析了 , 後續繼續更新逆向時會講彙編部分 , 到時候會好好分析一下寄存器和彙編指令.
簡單總結一下 _objc_msgSend
整個彙編代碼過程以下:
1️⃣ : 在
objc_msgSend
中分爲兩部分 ,第一部分是彙編寫的查找緩存的流程 . 直到bl __class_lookupMethodAndLoadCache3
時 , 轉到 C 函數繼續執行 後續的lookUpImpOrForward
流程.2️⃣ : 獲取對象真實的
isa
, 非taggedpointer
的isa
是一個聯合體 , 使用位域來存儲各類信息 , 這個後續筆者會詳細講述 .3️⃣ : 來到
CacheLookup
過程 , 經過指針偏移找到cache_t
,處理bucket
以及內存哈希表處理 , 經過sel
哈希算法以後的key
找到imp
, 找到則返回 , 找不到JumpMiss
.4️⃣ : 繼續來到
__objc_msgSend_uncached
->MethodTableLookup
5️⃣ : 調用
bl __class_lookupMethodAndLoadCache3
, 來到慢速查找流程 .
後續就是 C 函數實現的消息查找以及轉發流程 , 因爲篇幅問題 , 下篇文章繼續講述完整流程 .
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
複製代碼
OC
方法的本質以下 :
在編譯期由
LLVM
將方法調用編譯成調用objc_msgSend
等函數 , 而後在彙編代碼執行緩存查找sel
對應的imp
, 找到就會返回調用 , 找不到則進入消息查找和消息轉發慢速流程 .