基本概念
Debug版本的musl-libc库为用户提供内存泄漏检测、堆内存统计、踩内存分析以及backtrace功能等维测手段,可以提高用户态内存相关问题的定位效率。
采用了对malloc/free接口进行插桩,保存关键节点信息,然后程序在申请和释放内存时进行内存节点完整性校验,最后在程序结束时通过统计节点信息得到内存统计信息并根据统计信息判断内存是否泄漏的设计思想。
运行机制
内存泄漏检查
对于每个进程,内存调测模块维护了128个链表(当前系统的线程最大数量为128个),每个链表的索引为线程ID。
申请内存时:保存关键信息到内存节点控制块,根据当前线程ID将内存节点控制块挂到对应链表;
释放内存时:根据需要释放的内存地址匹配内存节点控制块并将该控制块删除。
图1 堆内存节点信息链表
申请内存时,返回地址会被保存到LR寄存器中。进程运行过程中,系统会在内存节点控制块中添加疑似泄漏点对应的lr等信息。如下图所示:
图2 堆内存节点信息
其中,TID表示线程ID;PID表示进程ID;ptr表示申请的内存地址;size表示申请的内存大小;lr[n]表示函数调用栈地址,变量n的大小可以根据具体场景的需要进行配置。
释放内存时,将free等接口的入参指针与node的ptr字段进行匹配,如果相同则删除该内存节点控制块信息。
用户通过串口或文件等方式,将各个进程内存调测信息导出,利用addr2line工具将导出的信息转换成导致内存泄漏的代码行,便可以解决内存泄露问题。
图3 泄漏点代码行定位流程
堆内存统计
用户态线程堆内存使用统计具有一定的实际意义,统计线程申请的堆内存占比,为用户程序的内存使用优化提供数据支持。用户态堆内存统计模块主要涉及的接口为malloc和free。如上图所示,每个进程维护128个链表,链表索引即线程ID,申请内存时系统将ptr和size信息记录在内存节点控制块中并将节点控制块挂在以线程ID为头信息的链表上,堆内存释放时根据ptr从对应的链表上移除相应的堆内存块信息;同时计算出当前线程所持有的堆内存总的使用量,并更新当前进程的堆内存使用量和堆内存使用峰值。
内存完整性检查
- 使用malloc申请内存(小于等于0x1c000 bytes时通过堆分配算法分配) 用户程序申请堆内存时,在堆内存节点处添加校验值等信息,如果校验值异常,则很有可能是前一块堆内存使用越界导致的(目前无法识别校验值被野指针破坏的场景)。在内存申请、释放时校验内存节点校验值的正确性,若内存节点被破坏,校验失败时则输出tid、pid及当前被踩节点前一块堆内存申请时保存的调用栈信息,通过addr2line工具可获得具体的代码行信息,辅助用户解决问题。
图4 node节点头信息添加校验值
free堆内存时,不会立即把该内存块释放掉,而是在内存中写入魔术数字0xFE,并放到free队列中(保证在一定时间内不会再被malloc函数分配),当有野指针或use-after-free的情况对该内存进行读取的操作时,能够发现数据异常,但是对于写操作则无法判断出来。
图5 free流程图
- 使用malloc申请内存(大于0x1c000 bytes时通过mmap申请) 当malloc通过mmap申请大块内存时,在返回给用户使用的内存区间头和尾分别多申请一个页,一个页PAGE_SIZE当前为0x1000,这两个页分别通过mprotect接口设置权限为PROT_NONE(无可读可写权限),可以有效防止内存越界读写问题:越界读写数据时由于无读写权限而导致用户程序异常,根据异常调用栈信息可找到相应的代码逻辑。
图6 malloc通过mmap机制申请内存的内存布局
使用指导
接口说明
表1 内存调测功能
接口名 | 描述 |
---|---|
mem_check_init | 初始化内存检测模块。 |
watch_mem | 获取线程级堆内存使用信息。 |
check_leak | 检查是否有堆内存泄漏。 |
check_heap_integrity | 检查堆内存的完整性。 |
backtrace | 获取调用栈地址信息。 |
backtrace_symbols | 根据地址信息获取符号信息。 |
print_trace | 输出函数调用栈信息。 |
表2 调用栈回溯功能
接口名 | 描述 |
---|---|
backtrace | 获取调用栈地址信息。 |
backtrace_symbols | 根据地址信息获取符号信息。 |
print_trace | 输出函数调用栈信息。 |
使用说明
编译OpenHarmony工程时默认编译的是debug版本,即libc库已经集成内存调测相关的接口实现,用户可以根据具体需要决定是否使能内存调测功能。
堆内存调测功能提供两种方式供用户使用:接口调用及命令行参数。
-
接口调用:优点是可以较精确的检查某一段代码逻辑的堆内存相关信息,缺点是需要修改用户代码。
-
命令行参数:优点是无需修改用户代码,缺点是无法精确的校验某一段逻辑的堆内存信息。