前言
正常狀況下,經過剖析界面以及 class-dump 出來頭文件就能對某個功用的完成猜個八九不離十。但是 Block 這種特殊的類型在頭文件中是看不出它的聲明的,一些有 Block 回調的辦法名 dump 出來是相似這樣的:
- (void)FM_GetSubscribeList:(long long)arg1 pageSize:(long long)arg2 callBack:(CDUnknownBlockType)arg3;
由於這種回調看不到它的辦法簽名,我們無法知道這個 Block 究竟有幾個參數,也不知道它函數體的詳細地址,因而在運用 lldb 停止靜態調試的時分也是困難重重。我也一度被這個困難所阻撓,以為調用到有 Block 的辦法就是進了死胡同,沒方法持續跟蹤下去了。我還因而保持過好幾次對某個功用的剖析,特別受挫。
好在,我們還有 Google 這個弱小的武器。沒有什麼問題是一次 Google 不能處理的。假如有,那就兩次。
這篇文章就來講講如何經過 Block 的內存模型來剖析出它的函數體地址,以及函數簽名。
Block 的內存構造
在 LLVM 文檔中,可以看到 Block 的完成標准,其中最關鍵的中央是關於 Block 內存構造的定義:
struct Block_literal_1 { void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock int flags; int reserved; void (*invoke)(void *, ...); struct Block_descriptor_1 { unsigned long int reserved; // NULL unsigned long int size; // sizeof(struct Block_literal_1) // optional helper functions void (*copy_helper)(void *dst, void *src); // IFF (1<<25) void (*dispose_helper)(void *src); // IFF (1<<25) // required ABI.2010.3.16 const char *signature; // IFF (1<<30) } *descriptor; // imported variables };
可以看到第一個成員是 isa,闡明了 Block 在 Objective-C 當中也是一個對象。我們重點要關注的就是 void (*invode)(void *, ...);
和 descriptor 中的 const char *signature
,前者指向了 Block 詳細完成的地址,後者是表示 Block 函數簽名的字符串。
實戰
注:本篇文章都是在 64 位零碎下停止剖析,假如是 32 位零碎,整型與指針類型的大小都是與 64 位不分歧的,請自行停止修正。
知道了 Block 的內存模型後,就可以直接翻開 hopper 和 lldb 停止調試了。
我這裡運用了邏輯思想的失掉 APP 作為剖析的例子。特地說一句,失掉下面的內容都相當不錯,很多付費專欄的內容都是很贊的,值得一看。
預備
設備:iPhone 5s IOS 8.2 越獄
usbmuxd
$ tcprelay -t 22:2222 1234:1234 Forwarding local port 2222 to remote port 22 Forwarding local port 1234 to remote port 1234 ......
ssh 到 IOS 設備並啟動 debugserver:
$ ssh root@localhost -p 2222 iPhone $ debugserver *:1234 -a "LuoJiFM-IOS" ebugserver-@(#)PROGRAM:debugserver PROJECT:debugserver-320.2.89 for arm64. Attaching to process LuoJiFM-IOS... Listening to port 1234 for a connection from *...
本地翻開 lldb 並近程附加進程,停止靜態調試:
$ lldb (lldb) process connect connect://localhost:1234
找到偏移地址:
(lldb) image list -o -f [ 0] 0x0000000000074000 /private/var/mobile/Containers/Bundle/Application/D106C0E3-D874-4534-AED6-A7104131B31D/LuoJiFM-IOS.app/LuoJiFM-IOS(0x0000000100074000) [ 1] 0x000000000002c000 /Users/wordbeyond/Library/Developer/Xcode/iOS DeviceSupport/8.2 (12D508)/Symbols/usr/lib/dyld
在 Hopper 下找到需求斷點的地址:
下斷點:
(lldb) br s -a 0x0000000000074000+0x0000000100069700 Breakpoint 2: where = LuoJiFM-IOS`_mh_execute_header + 407504, address = 0x00000001000dd700
然後在使用中點擊訂閱 Tab ,此時會命中綴點(假如沒有命中,手動下拉刷新下)。
眾所周知,Objective-C 辦法的調用都會轉化成 objc_msgSend 調用,因而單步的時分看到 objc_msgSend 就可以停上去了:
-> 0x1000dd71c <+431900>: bl 0x100daa2bc ; symbol stub for: objc_msgSend 0x1000dd720 <+431904>: mov x0, x20 0x1000dd724 <+431908>: bl 0x100daa2ec ; symbol stub for: objc_release 0x1000dd728 <+431912>: mov x0, x21 (lldb) po $x0 <DataServiceV2: 0x17400cea0> (lldb) po (char *)$x1 "FM_GetSubscribeList:pageSize:callBack:" (lldb) po $x4 <__NSStackBlock__: 0x16fd88f88>
可以看到,第四個參數是個 StackBlock 對象,但是 lldb 只為我們打印出了它的地址。接上去,就靠我們自己來找出它的函數體地址和函數簽名了。
找出 Block 的函數體地址
要找出 Block 的函數體地址很復雜,依據下面的內存模型,我們只到找到 invoke 這個函數指針的地址,它指向的就是這個 Block 的完成。
在 64 位零碎上,指針類型的大小是 8 個字節,而 int 是 4 個字節,如下:
因而,invoke 函數指針的地址就是在第 16 個字節之後。我們可以經過 lldb 的 memory 命令來打印出指定地址的內存,我們下面曾經失掉了 block 的地址,如今就打印出它的內存內容:
(lldb) memory read --size 8 --format x 0x16fd88f88 0x16fd88f88: 0x000000019b4d8088 0x00000000c2000000 0x16fd88f98: 0x00000001000dd770 0x0000000100fc6610 0x16fd88fa8: 0x000000017444c510 0x0000000000000001 0x16fd88fb8: 0x000000017444c510 0x0000000000000008
如前所述,函數指針的地址是在第 16 個字節之後,並占用 8 個字節,所以可以失掉函數的地址是 0x00000001000dd770。
有了函數地址之後,就可以對這個地址停止反匯編:
(lldb) disassemble --start-address 0x00000001000dd770 LuoJiFM-IOS`_mh_execute_header: -> 0x1000dd770 <+431984>: stp x28, x27, [sp, #-96]! 0x1000dd774 <+431988>: stp x26, x25, [sp, #16] 0x1000dd778 <+431992>: stp x24, x23, [sp, #32] 0x1000dd77c <+431996>: stp x22, x21, [sp, #48] 0x1000dd780 <+432000>: stp x20, x19, [sp, #64] 0x1000dd784 <+432004>: stp x29, x30, [sp, #80] 0x1000dd788 <+432008>: add x29, sp, #80 ; =80 0x1000dd78c <+432012>: mov x22, x3
也可以直接在 lldb 當中下斷點:
(lldb) br s -a 0x00000001000dd770 Breakpoint 3: where = LuoJiFM-IOS`_mh_execute_header + 407616, address = 0x00000001000dd770
再次運轉函數,就可以進到回調的 Block 函數體內了。
但是,大少數狀況下,我們並不需求進到 Block 函數體內。在寫 tweak 的時分,我們更需求的是知道這個 Block 回調給了我們哪些參數。
接上去,我們持續停止探究。
找出 Block 的函數簽名
要找出 Block 的函數簽名,需求經過 descriptor 構造體中的 signature 成員,然後經過它失掉一個 NSMethodSignature 對象。
首先,需求找到 descriptor 構造體。這個構造體在 Block 中是經過指針持有的,它的地位正好在 invoke 成員前面,占用 8 個字節。可以從下面的內存打印中看到 descriptor 指針的地址是 0x0000000100fc6610。
接上去,就可以經過 descriptor 的地址找到 signature 了。但是,文檔指出並不是每個 Block 都是無方法簽名的,我們需求經過 flags 與 block 中定義的枚舉掩碼停止與判別。還是在剛剛的 llvm 文檔中,我們可以看到掩碼的定義如下:
enum { BLOCK_HAS_COPY_DISPOSE = (1 << 25), BLOCK_HAS_CTOR = (1 << 26), // helpers have C++ code BLOCK_IS_GLOBAL = (1 << 28), BLOCK_HAS_STRET = (1 << 29), // IFF BLOCK_HAS_SIGNATURE BLOCK_HAS_SIGNATURE = (1 << 30), };
再次運用 memory 命令打印出 flags 的值:
(lldb) memory read --size 4 --format x 0x16fd8a958 0x16fd8a958: 0x9b4d8088 0x00000001 0xc2000000 0x00000000 0x16fd8a968: 0x000dd770 0x00000001 0x00fc6610 0x00000001
由於 ((0xc2000000 & (1 << 30)) != 0),因而我們可以確定這個 Block 是有簽名的。
雖然在文檔中指出並不是每個 Block 都有函數簽名的。但是我們可以在 Clang 源碼 中的 CGBlocks.cpp 檢查 CodeGenFunction::EmitBlockLiteral
與 buildGlobalBlock 辦法,可以看到每個 Block 的 flags 成員都是被默許設置了 BLOCK_HAS_SIGNATURE。因而,我們可以推斷,一切運用 Clang 編譯的代碼中的 Block 都是有簽名的。
為了找出 signature 的地址,我們還需求確認這個 Block 能否擁有 copy_helper 和 disponse_helper 這兩個可選的函數指針。由於 ((0xc2000000 & (1 << 25)) != 0)
,因而我們可以確認這個 Block 擁有剛剛提到的兩個函數指針。
如今可以總結下:signature 的地址是在 descriptor 下偏移兩個 unsiged long 和兩個指針後的地址,即 32 個字節後。如今讓我們找出它的地址,並打印出它的字符串內容:
(lldb) memory read --size 8 --format x 0x0000000100fc6610 0x100fc6610: 0x0000000000000000 0x0000000000000029 0x100fc6620: 0x00000001000ddb64 0x00000001000ddb70 0x100fc6630: 0x0000000100dfec18 0x0000000000000001 0x100fc6640: 0x0000000000000000 0x0000000000000048 (lldb) p (char *)0x0000000100dfec18 (char *) $4 = 0x0000000100dfec18 "v28@?0q8@"NSDictionary"16B24"
看到這一串亂碼是不是覺得有點解體,折騰了半天,怎樣打印出這麼一串鬼東西,雖然外面有一個熟習的 NSDictionary,但是其它的東西完全看不懂啊。
不要慌,這的確就是一個函數簽名,只是我們需求經過 NSMethodSignature 找出它的參數類型:
(lldb) po [NSMethodSignature signatureWithObjCTypes:"v28@?0q8@\"NSDictionary\"16B24"] <NSMethodSignature: 0x174672940> number of arguments = 4 frame size = 224 is special struct return? NO return value: -------- -------- -------- -------- type encoding (v) 'v' flags {} modifiers {} frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0} memory {offset = 0, size = 0} argument 0: -------- -------- -------- -------- type encoding (@) '@?' flags {isObject, isBlock} modifiers {} frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0} memory {offset = 0, size = 8} argument 1: -------- -------- -------- -------- type encoding (q) 'q' flags {isSigned} modifiers {} frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0} memory {offset = 0, size = 8} argument 2: -------- -------- -------- -------- type encoding (@) '@"NSDictionary"' flags {isObject} modifiers {} frame {offset = 16, offset adjust = 0, size = 8, size adjust = 0} memory {offset = 0, size = 8} class 'NSDictionary' argument 3: -------- -------- -------- -------- type encoding (B) 'B' flags {} modifiers {} frame {offset = 24, offset adjust = 0, size = 8, size adjust = -7} memory {offset = 0, size = 1}
留意,字符串中的雙引號需求對其停止本義。
對我們最有用的 type encoding 字段,這些符號對應的解釋可以參考 Type Encoding 官方文檔。
所以,總結來講就是:這個辦法沒有前往值,它承受四個參數,第一個是 block (即我們自己的 block 的援用),第二個是 (long long) 類型的,第三個是一個 NSDictionary 對象,第四個是一個 BOOL 值。
最終,我們失掉了這個 Block 的函數參數。最初提到的那個辦法簽名的完好版就是:
- (void)FM_GetSubscribeList:(long long)arg1 pageSize:(long long)arg2 callBack:(void (^)(long long, NSDictionary *, BOOL)arg3;
小結
由於想運用真實的例子停止演示,所以本文直接運用逆向的靜態剖析停止闡明。其實下面提到的一切進程,都可以直接在 Xcode 經過自己寫的代碼停止操作。經過自己入手剖析一遍,比看十篇文章來得更無效果。下次假如面試再有人問到 Block 的完成和內存模型,你就可以跟它侃侃而談了。
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或許任務能帶來一定的協助,假如有疑問大家可以留言交流。
【iOS經過逆向了解Block的內存模型】的相關資料介紹到這裡,希望對您有所幫助! 提示:不會對讀者因本文所帶來的任何損失負責。如果您支持就請把本站添加至收藏夾哦!