自己搞一个 MemoryGraph 工具行不行?

iOS成长之路

共 3577字,需浏览 8分钟

 · 2021-11-28

大家好,我是脑洞TF,目前就职于阿里巴巴。从事 iOS 开发多年。这是我刚开的公众号。未来我会持续不定期更新,欢迎大家关注我。一起学习,一起探讨,一起成长。
老铁,关注我不迷路,咱也双击一个 666~

本篇文字有点多,大家耐心看,有些东西大家自己去实验一下,收获的会更多。
话不多说,下面进入正题~~~(由于内容需要脱敏,导致有些东西不能说的太透,原谅我~有兴趣的同学可以私信我)



一、前言:

在 Xcode 里关于 malloc stack logging 这个工具可以看到 2 个选项,如下图。

一个是只查看当前存活的对象,一个是记录内存 alloc 和 free 的行为(下面简称内存行为日志)。
这 2 个均可以帮助我们分析内存问题。选择 Live Allocations Only 然后导出 memGraph 文件,还可以用 malloc_history、vmmap 等命令对其进行解析,根据每个内存节点的栈信息再结合 linkmap,以类、动态库、.a 的维度对内存数据进行聚合。
但是!最大的缺陷就是无法自动化,且强依赖 Xcode 工具,Xcode 联调导出 memGraph 文件非常不稳定。经常出现断联、卡死、内存数据异常、导出 memGraph 文件失败等情况。

二、业内方案:


方案

流程

优点

缺点

hook malloc方法

hook 内存分配释放的方法

->栈回溯

->记录内存行为日志

->分配释放成对数据过滤

->符号化

->数据挖掘

难度较高、成本较高

性能较差,且覆盖case不全。

实现系统的malloc logger的勾子

实现malloc_logger方法

->栈回溯

->记录内存行为日志

->分配释放成对数据过滤

->符号化

->数据挖掘

难度较高、覆盖case

性能较差

遍历内存所有live的节点

遍历所有节点

->符号化

难度较低

性能高、无法抓堆栈。

以上方案思路均没有什么问题,但是实际实现中有2个问题:

1、性能问题,卡顿。

2、需要自定义 mallocZone,否则会出现自身逻辑开辟的内存也会被统计上,影响真实数据。


三、那么 Malloc Stack Logging 是如何实现的?

那有没有什么办法能够利用系统的一些能力,然后达到我们拿到内存行为日志的呢?最开始的思路就是看看系统的 Malloc Stack Logging 是如何实现的。


1、我始终坚信 Malloc Stack Logging 工具是有写文件的,猜测一定是把内存行为日志以及堆栈记录下来了(虽然后面发现不是完全正确哈)。

就着这个思路,通过逆向系统的工具和一些动态库,也是几经周折终于找到了日志存放的路径。虽然找到文件的路径,但是总不能所有打的包都打开 Malloc Stack Logging 这个配置项吧,而且日志文件非常大。所以还是要想想办法看看有没有代码的方式控制是否输出日志。(日志存放在 tmp 目录下)


2、如何用代码控制日志开关?

通过看 libmalloc 的源码找到2个比较可疑的方法:
extern boolean_t turn_on_stack_logging(stack_logging_mode_type mode);extern void turn_off_stack_logging(void);

一尝试果真是控制内存行为日志的开关。一打开日志发现里面存的都是 2 进制。如何解析呢?


3、如何解析日志?

先看看二进制文件的规律吧,如下图:


从上图可以很规律的看出,每 64 位表示一个数,感觉每 4 个 64 位像是一组数。

改了下解析二进制的脚本将数据解析成四元素数据结构,这次感觉应该没啥问题了。数据如下:

alloc size:296 stackid:0x00000000035944 add:0x08829C30 free size:0 stackid:0x0000000001B793 add:0x08821270 free size:0 stackid:0x00000000035D92 add:0x08821270 alloc size:296 stackid:0x000000000157A2 add:0x08821270 alloc size:24 stackid:0x00000000030A4C add:0x06DF6F88 alloc size:296 stackid:0x00000000035944 add:0x08829080 free size:0 stackid:0x0000000001B793 add:0x08821270 free size:0 stackid:0x00000000035D92 add:0x08821270 alloc size:28 stackid:0x000000000277EE add:0x08820AF0


但是通过数据结构发现并没有栈信息,有的只是一个 stackid。有这玩意没有用啊,那如何能获取到 frames。

4、如何将 stackid 转为 frames:

结合系统源码,找到了一个可以将 stackid 转为 frames 的方式。
到了这一步感觉好像这条路可以走通了。
到目前为止:有内存行为日志 > 日志可以解析 > 获取到栈信息。有了以上数据,核心链路就通了。


方案工程化验证:

日志体量:1 小时 6G 左右

解析耗时:6G 日志 stackid 转成 frames 耗时也大概1小时

解析后日志体量:大约 30G 左右

结论:将 stackid 转成 frames 存在手机上,磁盘会爆炸。这个方式不行,必须想办法进行离线日志解析。



5、如何离线解析日志和获取栈信息

感觉一定是有一个 map 存储这个 stackid 和 frames 的映射关系。只要把 map 持久化到本地,就可以做离线解析了。

看了下相关源码,发现原来系统并不是仅仅对 stackid 和 frames 做了一个 map,而是用一个树来存储栈。并且是放在内存中的。这样一来是大大地减少了内存的占用,二来是通过 stackid 和树之间建立一种映射关系,查找速度也非常快!简直绝了!

然后对这棵树进行序列化和反序列化,验证了下,真是一点毛病没有啊!如此离线解析日志就做到了。而且解析 6G 数据耗时大概 7min 左右。


6、数据验证:

同一份数据,通过上面提到过的运行时解析离线解析两种解析方式进行数据对比,数据结果是一致的。(在这个过程中 type_flags 内存分配类型这里也有点小问题,就不展开了)

如此,基本的路已经通了,那么自动化分析内存工具是可行的。剩下的工作就是根据地址将 alloc 和 free 成对的数据过滤掉。然后符号化再做数据挖掘了。

其实在方案工程化的过程中,会遇到各种各样的问题,每走一步都比较难,这一篇就不展开介绍了。


四、方案优缺点

优点:

  • 实现成本低

  • 直接使用系统原始日志,自定义工具空间大,想怎么处理聚类都可以。

  • 性能特别好,虽然打印了很多的日志,但是还是特别丝滑,毫无卡顿。

  • 无需像 MemGraph 那样连接 Mac 设备,任何时间,任何环境都可以打开开关记录内存行为。

缺点:

  • 长时间记录内存行为日志,占用磁盘空间(不过我觉得这不叫事,这些外力都可以解决~)


五、整体总结

调研过程比上述过程要坎坷,不过大概了解了 memgraph 的原理和 Instrument 的原理。在逆向很多系统工具的时候,发现如果设备和 Xcode 或者 Instrument 工具建立的socket 通道,数据会通过通道发送到 Mac 上。
memgraph 的 Live Allocations Only 不是拿的内存行为日志,应该是遍历的所有的内存节点,再通过 stack tree 获取到的每个内存节点的栈信息。

Instrument 工具应该是对内存行为日志做了包装,二者的核心都是类似的。

该方案的应用场景和想象空间还是非常大的,想象力往往是前进的源头和动力。而且方案的灵活度也比较高,毕竟拿到的都是原始的日志和数据啥的,想怎么搞怎么搞,还不是任由咱们摆布,哈哈哈哈哈~


这就是本片的全部内容,如果有什么不对的地方,欢迎各位大佬私信我。

六、写在最后

最后回答一下标题的问题,能不能自己做一个 MemoryGraph 工具?

答案是能!

那还有没有性能更好的方案

答案是有。


关注我,后面我会继续分享~

(下一篇打算分享一点逆向相关的内容。先把下一篇的方向定好,然后写在文章里,这样能倒逼自己,一定要坚持~~)



浏览 41
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报