前言
正常情況下,通過分析界面以及 class-dump 出來頭文件就能對某個功能的實現猜個八九不離十。但是 block 這種特殊的類型在頭文件中是看不出它的聲明的,一些有 block 回調的方法名 dump 出來是類似這樣的:
1
|
- ( void )fm_getsubscribelist:( long long )arg1 pagesize:( long long )arg2 callback:(cdunknownblocktype)arg3; |
因為這種回調看不到它的方法簽名,我們無法知道這個 block 到底有幾個參數,也不知道它函數體的具體地址,因此在使用 lldb 進行動態調試的時候也是困難重重。我也一度被這個困難所阻擋,以為調用到有 block 的方法就是進了死胡同,沒辦法繼續跟蹤下去了。我還因此放棄過好幾次對某個功能的分析,特別受挫。
好在,我們還有 google 這個強大的武器。沒有什么問題是一次 google 不能解決的。如果有,那就兩次。
這篇文章就來講講如何通過 block 的內存模型來分析出它的函數體地址,以及函數簽名。
block 的內存結構
在 llvm 文檔中,可以看到 block 的實現規范,其中最關鍵的地方是對于 block 內存結構的定義:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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
1
2
3
4
|
$ 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:
1
2
3
4
5
6
|
$ 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 并遠程附加進程,進行動態調試:
1
2
|
$ lldb (lldb) process connect connect: //localhost:1234 |
找到偏移地址:
1
2
3
|
(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 下找到需要斷點的地址:
下斷點:
1
2
|
(lldb) br s -a 0x0000000000074000+0x0000000100069700 breakpoint 2: where = luojifm-ios`_mh_execute_header + 407504, address = 0x00000001000dd700 |
然后在應用中點擊訂閱 tab ,此時會命中斷點(如果沒有命中,手動下拉刷新下)。
眾所周知,objective-c 方法的調用都會轉化成 objc_msgsend 調用,因此單步的時候看到 objc_msgsend 就可以停下來了:
1
2
3
4
5
6
7
8
9
10
|
-> 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 的地址,現在就打印出它的內存內容:
1
2
3
4
5
|
(lldb) memory read --size 8 --format x 0x16fd88f88 0x16fd88f88: 0x000000019b4d8088 0x00000000c2000000 0x16fd88f98: 0x00000001000dd770 0x0000000100fc6610 0x16fd88fa8: 0x000000017444c510 0x0000000000000001 0x16fd88fb8: 0x000000017444c510 0x0000000000000008 |
如前所述,函數指針的地址是在第 16 個字節之后,并占用 8 個字節,所以可以得到函數的地址是 0x00000001000dd770。
有了函數地址之后,就可以對這個地址進行反匯編:
1
2
3
4
5
6
7
8
9
10
|
(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 當中下斷點:
1
2
|
(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 文檔中,我們可以看到掩碼的定義如下:
1
2
3
4
5
6
7
|
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 的值:
1
2
3
|
(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 個字節后。現在讓我們找出它的地址,并打印出它的字符串內容:
1
2
3
4
5
6
7
|
(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 找出它的參數類型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
(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 的函數參數。最初提到的那個方法簽名的完整版就是:
1
|
- ( void )fm_getsubscribelist:( long long )arg1 pagesize:( long long )arg2 callback:( void (^)( long long , nsdictionary *, bool )arg3; |
小結
因為想使用真實的例子進行演示,所以本文直接使用逆向的動態分析進行說明。其實上面提到的所有過程,都可以直接在 xcode 通過自己寫的代碼進行操作。通過自己動手分析一遍,比看十篇文章來得更有效果。下次如果面試再有人問到 block 的實現和內存模型,你就可以跟它侃侃而談了。
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作能帶來一定的幫助,如果有疑問大家可以留言交流。
原文鏈接:http://www.swiftyper.com/2016/12/16/debuging-objective-c-blocks-in-lldb/