- (void)FM_GetSubscribeList:(long long)arg1 pageSize:(long long)arg2 callBack:(CDUnknownBlockType)arg3;因为这种回调看不到它的方法签名,我们无法知道这个 Block 到底有几个参数,也不知道它函数体的具体地址,因此在使用 lldb 进行动态调试的时候也是困难重重。我也一度被这个困难所阻挡,以为调用到有 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 函数签名的字符串。$ tcprelay -t 22:2222 1234:1234Forwarding local port 2222 to remote port 22Forwarding local port 1234 to remote port 1234......ssh 到 iOS 设备并启动 debugserver:
$ ssh root@localhost -p 2222iPhone $ 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+0x0000000100069700Breakpoint 2: where = LuoJiFM-IOS`_mh_execute_header + 407504, address = 0x00000001000dd700然后在应用中点击订阅 Tab ,此时会命中断点(如果没有命中,手动下拉刷新下)。
-> 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 只为我们打印出了它的地址。接下来,就靠我们自己来找出它的函数体地址和函数签名了。
因此,invoke 函数指针的地址就是在第 16 个字节之后。我们可以通过 lldb 的 memory 命令来打印出指定地址的内存,我们上面已经得到了 block 的地址,现在就打印出它的内存内容:
(lldb) memory read --size 8 --format x 0x16fd88f880x16fd88f88: 0x000000019b4d8088 0x00000000c20000000x16fd88f98: 0x00000001000dd770 0x0000000100fc66100x16fd88fa8: 0x000000017444c510 0x00000000000000010x16fd88fb8: 0x000000017444c510 0x0000000000000008如前所述,函数指针的地址是在第 16 个字节之后,并占用 8 个字节,所以可以得到函数的地址是 0x00000001000dd770。
(lldb) disassemble --start-address 0x00000001000dd770LuoJiFM-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 0x00000001000dd770Breakpoint 3: where = LuoJiFM-IOS`_mh_execute_header + 407616, address = 0x00000001000dd770再次运行函数,就可以进到回调的 Block 函数体内了。
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 0x16fd8a9580x16fd8a958: 0x9b4d8088 0x00000001 0xc2000000 0x000000000x16fd8a968: 0x000dd770 0x00000001 0x00fc6610 0x00000001由于 ((0xc2000000 & (1 << 30)) != 0),因此我们可以确定这个 Block 是有签名的。
CodeGenFunction::EmitBlockLiteral
与 buildGlobalBlock 方法,可以看到每个 Block 的 flags 成员都是被默认设置了 BLOCK_HAS_SIGNATURE。因此,我们可以推断,所有使用 Clang 编译的代码中的 Block 都是有签名的。((0xc2000000 & (1 << 25)) != 0)
,因此我们可以确认这个 Block 拥有刚刚提到的两个函数指针。(lldb) memory read --size 8 --format x 0x0000000100fc66100x100fc6610: 0x0000000000000000 0x00000000000000290x100fc6620: 0x00000001000ddb64 0x00000001000ddb700x100fc6630: 0x0000000100dfec18 0x00000000000000010x100fc6640: 0x0000000000000000 0x0000000000000048(lldb) p (char *)0x0000000100dfec18(char *) $4 = 0x0000000100dfec18 "v28@?0q8@"NSDictionary"16B24"看到这一串乱码是不是觉得有点崩溃,折腾了半天,怎么打印出这么一串鬼东西,虽然里面有一个熟悉的 NSDictionary,但是其它的东西完全看不懂啊。
(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}注意,字符串中的双引号需要对其进行转义。
- (void)FM_GetSubscribeList:(long long)arg1 pageSize:(long long)arg2 callBack:(void (^)(long long, NSDictionary *, BOOL)arg3;小结