架构技术评论

开发MemoryGraph工具的可行性探索

大家好,我是脑洞TF,目前就职于阿里巴巴。从事iOS开发多年。这是我刚开的公众号。未来我会持续不定期更新,欢迎大家关注我。一起学习,一起探讨,一起成长。 老铁,关注我不迷路,咱也双击一个666~
本篇文字有点多,大家耐心看,有些东西大家自己去实验一下,收获的会更多。 话不多说,下面进入正题~~~(由于内容需要脱敏,导致有些东西不能说的太透,原谅我~有兴趣的同学可以私信我)


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

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

方案

流程

优点

缺点

hook malloc 方法 hook 内存分配释放的方法 -> 栈回溯 -> 记录内存行为日志 -> 分配释放成对数据过滤 -> 符号化 -> 数据挖掘

难度较高、成本较高

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

实现系统的 malloc logger 的勾子

实现 malloc_logger 方法

-> 栈回溯 -> 记录内存行为日志 -> 分配释放成对数据过滤 -> 符号化 -> 数据挖掘
难度较高、覆盖 case 全

性能较差

遍历内存所有 live 的节点

遍历所有节点

-> 符号化

难度较低

性能高、无法抓堆栈。

以上方案思路均没有什么问题,但是实际实现中有2个问题: 1、性能问题,卡顿。 2、需要自定义mallocZone,否则会出现自身逻辑开辟的内存也会被统计上,影响真实数据。
三、那么MallocStackLogging是如何实现的? 那有没有什么办法能够利用系统的一些能力,然后达到我们拿到内存行为日志的呢?最开始的思路就是看看系统的MallocStackLogging是如何实现的。
1、我始终坚信MallocStackLogging工具是有写文件的,猜测一定是把内存行为日志以及堆栈记录下来了(虽然后面发现不是完全正确哈)。 就着这个思路,通过逆向系统的工具和一些动态库,也是几经周折终于找到了日志存放的路径。虽然找到文件的路径,但是总不能所有打的包都打开MallocStackLogging这个配置项吧,而且日志文件非常大。所以还是要想想办法看看有没有代码的方式控制是否输出日志。(日志存放在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工具应该是对内存行为日志做了包装,二者的核心都是类似的。

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


这就是本片的全部内容,如果有什么不对的地方,欢迎各位大佬私信我。
六、写在最后 最后回答一下标题的问题,能不能自己做一个memGraph工具? 答案是能!

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

答案是有。

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

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