本着学习心态,去简单实现一下mallocstacklogging日志的离线分析,同时记录一下本次遇到的问题。个人水平有限,代码仅供参考。
除了项目代码以外,MallocStackLogging文件是我从自己的越狱iPhone7提取出来的系统MallocStackLogging动态库,可以拖进Hopper分析。libmalloc-166.251.2是我加了注释的源码。
离线解析日志文件,需要知道日志文件在哪里,日志文件的数据是以什么格式保存的
其实文中有提到,日志文件是放在沙盒的tmp目录下的。不过,还是要自己探究一下。
经过测试,当选择为LiveAllocationsOnly时是不生成日志的。(原因是libmalloc源码中就是这么写的)。当选择为AllAllocationandFreeHistory的时候,
externboolean_tturn_on_stack_logging(stack_logging_mode_typemode);externvoidturn_off_stack_logging(void);我决定从两个已知的开关方法入手。翻阅源码libmalloc-317.140.5得知libmalloc都是调用的MallocStackLogging.framework的方法
LoadtheMallocStackLogginglibraryandregisteritwithlibmallocboolean_tmalloc_register_stack_logger(void);
扔到Hopper里瞅瞅。
整个库其实并不大,符号也不多。我看到了一些感兴趣的符号,比如_create_log_file。
那下一步呢?不会要真的分析汇编吧
遇事不决,github搜一波
在低版本的libmalloc库中,是有源码的。
以下的分析都是基于libmalloc-166.251.2版本的。之后libmalloc-283就不存在这些代码了。
所以,我有一个大胆的想法:
你们什么都没干,只是把代码移到了MallocStackLogging.framework中对吧?(其实是改了的)
我把加了注释的libmalloc-166.251.2源码也放入了代码文件中,里面有我的很多注释和蹩脚的翻译。如果对源码感兴趣的朋友,可以翻阅。希望对你有帮助。
比如这样的注释。
turn_on_stack_logging函数
turn_on_stack_logging __prepare_to_log_stacks(false);//初始化pre_write_buffers __create_uniquing_table//创建用于保存内存分配堆栈的哈希表uniquing_table create_log_file//创建日志文件 index_file_descriptor//静态变量日志文件句柄 __stack_log_file_path__//全局变量日志文件地址 malloc_logger=__disk_stack_logging_log_stack; __syscall_logger=__disk_stack_logging_log_stack;缩进表示调用关系。可以看到MallocStackLogging.framework也是利用两个勾子函数malloc_logger和__syscall_logger来实现内存分配日志信息的记录的。核心是__create_uniquing_table函数,这个函数会用来创建存放分配堆栈信息哈希表backtrace_uniquing_table。
假如我们有这样两个函数调用堆栈:
funcCfuncDfuncBfuncBfuncAfuncA他们在哈希表中的存放是这样的:上才艺!
为了方便理解,我把图画成这样了。其实parent存放的是父节点的hash值也就是下标。更多详细资料可以参考函数enter_frames_in_table。我在其中加了很多注释。
其实我们只需要知道一个事情,哈希表backtrace_uniquing_table存放了堆栈信息。最后返回的stack_id就是最上层调用指令(比如funcCfuncD)的hash值也是下标。有一个stack_id,我就可以一连串地拿到funcCfuncBfuncA。
一个stack_id对应一个堆栈列表信息。
在__prepare_to_log_stacks函数中,我还看到了
turn_off_stack_logging函数
turn_off_stack_logging malloc_logger=NULL; __syscall_logger=NULL; stack_logging_enable_logging=0;这个函数就没什么好讲了,把两个勾子置空。
__disk_stack_logging_log_stack函数
但是,根据我看源码的结果。树的操作只会在精简模式下进行,而且入树的是stack_id和分配的内存地址。不知道是不是因为我看的是libmalloc库老版本的缘故。
什么数据?stack_logging_index_event
typedefstruct{ uintptr_targument; uintptr_taddress; uint64_toffset_and_flags;//top8bitsareactuallytheflags!}stack_logging_index_event;address存放是分配或者释放的内存地址(经过伪装处理)argument存放是内存的大小offset_and_flags就有些复杂了。经过一堆宏的位运算。
(16个0_stack_id的低48位)|(type_flags的低8位_56个0)|(type_flags的24-32位_48个0)=type_flags的低8位_type_flags的24-32位_stack_id的低48位=共64位offset_and_flags的64位存放了stack_id和type_flags和user_tag
那么目前为止,我知道了日志文件中存放的是一个个stack_logging_index_event结构体。那么通过解析日志文件,拿到stack_logging_index_event后,我就能知道address对应的stack_id,再拿stack_id去查堆栈哈希表,就能拿到分配或释放这块内存的堆栈信息。
接下来就是写代码了。
解析出来的数据,怎么看都不对劲。
我们来看看用系统api解析出来的数据是什么样子的。
externkern_return_t__mach_stack_logging_enumerate_records(task_ttask,mach_vm_address_taddress,voidenumerator(my_mach_stack_logging_record_t,void*),void*context);voidenumerate_records_hander(my_mach_stack_logging_record_trecord,void*context){NSString*type=typeString(record.type_flags);NSLog(@"%@size:%llustackid:0x%llxaddress:%p",type,record.argument,record.stack_identifier,(void*)record.address);}-(void)test_enumerate_records{if(!_isOpen){NSLog(@"还没有打开日志开关呢");return;}__mach_stack_logging_enumerate_records(mach_task_self(),NULL,enumerate_records_hander,NULL);}结果是
这个数据,看起来就正常多了。
问题出在哪里呢?我试图去找私有库的头文件,了解真正的结构,未果。这个时候,我想起《自己搞一个MemoryGraph工具行不行?》文中提到,日志文件的内容是4个数一组的。这个跟我看源码的结果是不一致。不过不妨一试。其实第一次解析时,有一个细节。就是第一条数据看上去像是一点点正常。
修改一下结构体:
//第二次错误尝试typedefstruct{uint64_targument;uint64_taddress;uint64_toffset_and_flags;uint64_twhat;}wrong_stack_logging_index_event64;结果:
看起来address和size是对了。stack_id和type_flags不对。
这里,我也思考一下,什么情况下会需要修改结构体?1.增加新功能了2.成员的类型不够存了,需要改变结构......
所以,会不会offset_and_flags被拆出来了?那就不需要那么多位运算了。
验证一下:
好了,再次修改结构体。
//这是现在的版本了typedefstruct{uint64_targument;uint64_taddress;uint64_toffset;uint64_tflags;}test_stack_logging_index_event64;这一次尝试就不贴图了,结果和系统api解析出来的一致。
获取到stack_id。我就可以查哈希表了。
接下来,用到了很多系统库的api。都可以在libmalloc-317.140.5库中的stack_logging.h文件中找到。
到此,解析日志文件,查询哈希表的逻辑就通了。
至于离线解析,系统api中已经提供哈希表的序列化和反序列化方法。
其实在看源码的过程中,我一直在思考这个功能的使用场景。利用系统开放的两个勾子,来记录内存分配释放的堆栈信息的功能。其实在matrix-iOS和OOMDetector库中都已经见识过了。这两个库也许也是参考了官方的实现方式。甚至于,在matrix-iOS的wiki中也写到__syscall_logger勾子是有审核风险的。
我真的能通过这个功能,收集用户手机上的日志信息吗?
就算可以。这么巨量的日志文件,又如何上传到我们的服务器呢?用户的手机磁盘空间是非常宝贵的。不然iOS系统也不会用内存压缩的方式替换内存交换了。那是不是意味着,只能在开发阶段使用?
libmalloc-317.140.5的stack_logging.h中的大量哈希表的操作方法也被标记为了过期。