iOS通过逆向理解Block的内存模型

发布时间 - 2026-01-10 22:39:15    点击率:

前言

正常情况下,通过分析界面以及 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使用  # block的原理  # 分析IOS RunLoop的事件循环机制  # IOS开发之多线程NSThiread GCD NSOperation Runloop  # 详解iOS应用UI开发中的九宫格坐标计算与字典转换模型  # iOS中的应用启动原理以及嵌套模型开发示例详解  # EvenLoop模型在iOS的RunLoop应用示例  # 都是  # 我们可以  # 可以看到  # 是在  # 回调  # 文档  # 就可以  # 是一个  # 第一个  # 是有  # 可以直接  # 要找  # 这篇文章  # 进到  # 自己的  # 的是  # 体内  # 掩码  # 得到了  # 情况下 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: HTML透明颜色代码怎么让下拉菜单透明_下拉菜单透明背景指南【技巧】  桂林网站制作公司有哪些,桂林马拉松怎么报名?  怎么制作网站设计模板图片,有电商商品详情页面的免费模板素材网站推荐吗?  怎么用AI帮你为初创公司进行市场定位分析?  香港服务器如何优化才能显著提升网站加载速度?  Laravel怎么实现搜索高亮功能_Laravel结合Scout与Algolia全文检索【实战】  如何快速搭建高效香港服务器网站?  猎豹浏览器开发者工具怎么打开 猎豹浏览器F12调试工具使用【前端必备】  音响网站制作视频教程,隆霸音响官方网站?  javascript中的数组方法有哪些_如何利用数组方法简化数据处理  详解免费开源的DotNet二维码操作组件ThoughtWorks.QRCode(.NET组件介绍之四)  jquery插件bootstrapValidator表单验证详解  网站优化排名时,需要考虑哪些问题呢?  免费制作统计图的网站有哪些,如何看待现如今年轻人买房难的情况?  新三国志曹操传主线渭水交兵攻略  Laravel怎么生成二维码图片_Laravel集成Simple-QrCode扩展包与参数设置【实战】  网站建设整体流程解析,建站其实很容易!  如何在阿里云完成域名注册与建站?  html5如何设置样式_HTML5样式设置方法与CSS应用技巧【教程】  简单实现Android文件上传  🚀拖拽式CMS建站能否实现高效与个性化并存?  夸克浏览器网页跳转延迟怎么办 夸克浏览器跳转优化  成都网站制作公司哪家好,四川省职工服务网是做什么用?  黑客入侵网站服务器的常见手法有哪些?  北京网站制作的公司有哪些,北京白云观官方网站?  如何用AI帮你把自己的生活经历写成一个有趣的故事?  详解ASP.NET 生成二维码实例(采用ThoughtWorks.QRCode和QrCode.Net两种方式)  Gemini怎么用新功能实时问答_Gemini实时问答使用【步骤】  JavaScript实现Fly Bird小游戏  Laravel如何创建自定义Artisan命令?(代码示例)  关于BootStrap modal 在IOS9中不能弹出的解决方法(IOS 9 bootstrap modal ios 9 noticework)  jimdo怎样用html5做选项卡_jimdo选项卡html5实现与切换效果【指南】  公司门户网站制作公司有哪些,怎样使用wordpress制作一个企业网站?  详解Android中Activity的四大启动模式实验简述  Laravel如何使用Service Container和依赖注入?(代码示例)  Laravel控制器是什么_Laravel MVC架构中Controller的作用与实践  如何实现建站之星域名转发设置?  中山网站推广排名,中山信息港登录入口?  在线教育网站制作平台,山西立德教育官网?  Laravel怎么配置不同环境的数据库_Laravel本地测试与生产环境动态切换【方法】  教你用AI将一段旋律扩展成一首完整的曲子  Laravel如何自定义分页视图?(Pagination示例)  Laravel如何配置.env文件管理环境变量_Laravel环境变量使用与安全管理  如何挑选优质建站一级代理提升网站排名?  Laravel怎么多语言本地化设置_Laravel语言包翻译与Locale动态切换【手册】  网站制作价目表怎么做,珍爱网婚介费用多少?  iOS验证手机号的正则表达式  国美网站制作流程,国美电器蒸汽鍋怎么用官方网站?  如何在阿里云虚拟服务器快速搭建网站?  Python自然语言搜索引擎项目教程_倒排索引查询优化案例