- 调试准备
笔者在上一篇《GDB内存调试初探》中提到了一个简单的调试应用(可执行文件名为memory,并已上传至CSDN的下载区),该应用会从标准输入逐行读取内存操作的相关“指令”,进行简单的内存申请、释放和读写的操作。本文仍将沿用此调试应用,侧重点在于glibc的ptmalloc内存分配模块初始化之后(即第一次申请内存之后)应用的几次内存分配的执行流程。
首先,编写简单的文本文件,写入memory的操作“指令”,保存至文件mscript.txt:
之后,使用gdb加载memory可执行文件,并在其main函数加入断点,最后以run < mscript.txt命令运行该可执行文件。之前提到过,这是一个简单的C代码编写的调试应用,它在main函数执行之后才进行第一次的内存分配请求:
如上图,可能由于glibc检测到应用的标准输入指向一个普通的文件,而非一个tty文件,它第一次请求分配的内存从1024字节转变为4096个字节(参考笔者的前一篇博客文章),相应的第一次向Linux内核请求分配的内存大小也变为136KB。
第一次应用向ptmalloc请求内存分配,是不可避免的:我们编写的应用调用了fgets函数从标准输入读取简单的操作“指令”。尽管如此,这对我们调试、分析ptmalloc模块的影响并不大。在memory进程读取了第一行“指令”之后,它会第二次请求分配内存,大小为1024个字节(由第一行“指令”确定,0m0x400):
此时我们也可以通过GDB查看一下ptmalloc的内存分配状态结构体main_arena,如上图所示。
- _int_malloc(…)的执行流程
在笔者的前一篇博客文章中提到,在main函数运行后第一次向ptmalloc申请内存,_int_malloc函数会最终调用sysmalloc函数向Linux内核申请内存资源;而ptmalloc模块向Linux内核申请的内存大小超过100KB,却只向memory应用返回了1K(或4K)大小的内存,剩余的内存就交给ptmalloc管理了,内存分配信号保存于main_arena结构体中。此时memory进程执行了标准输入的“指令”,向ptmalloc第二次申请了1024个字节,_int_malloc函数会如何处理呢?一种方法是结合_int_malloc的源码,使用GDB逐步调试;另一种方法也是结合_int_malloc源码,但是要对该函数的处理流程进行预测。这里笔者选择了第二种方法,这可以简化GDB的操作,因为我们有足够的调试信息。
此时在_int_malloc函数中,由于局部变量nb的值为1032,有两个条件分支均未执行,第一个条件分支是与get_max_fast()的比较,第二个条件分支是in_smallbin_range()宏的判定。之后,计算了idx局部变量的值:
我们手动计算的结果为idx = 72。_int_malloc函数第一个重量级的代码块是一个for循环语句,其中嵌套了while循环。经分析,while循环不会被执行,如下图:
接下来是对宏in_smallbin_range()的非判定,其中嵌套了一个条件分支,经分析可知,该条件分支中的代码也不会被执行:
之后再次进入一个for循环体,不过很不幸,该循环体中的do … while循环很快就执行了goto跳转:
至此,在_int_malloc函数中的主要执行流程就完成了:虽然没有对main_arena结构体进行写操作(对应函数中的av指针),但最终决定从top指针处分配内存了。使用top指向的内存来分配,这就简化了内存分配的操作,下面对代码的调试可以验证这一点。
- 使用top指针的剩余空间分配内存
在use_top标签之下有一长段的注释说明信息,可以帮助我们理解ptmalloc的内存分配机制。在分析、预测_int_malloc的返回值之前,笔者决定让memory进程执行到此处,查看main_arena结构体的信息,之后再分析预测:
如上图,根据main_arena结构体中的ptmalloc内存分配信息,手动计算显示_int_malloc函数应当返回的内存指针为0x2a014010;让此函数执行完成并返回,可以确认我们的计算结果是正确的:
让应用执行完毕,也可以验证这一点,memory可执行程序每分配一次内存,都会将分配的内存地址输出到终端:
- 多次分配内存的运行结果分析
上面调试用到的mscript.txt只分配了一次内存,笔者决定多次分配内存查看返回指针的规律。编写连续5次分配1024个字节的“指令”,之后释放第三次和第四次分配的内存,最后再分配一次大小为2048字节的内存。运行memory可执行文件结果如下:
由上图可知,分配1024字节的内存返回指针存在一定的规律:除了第一个返回指针与第二个返回指针相差0x810个字节外,其他的“相邻”的返回指针都相差了0x408字节,也就是1032字节。而1032是一个我们比较熟悉的数值:它是用宏checked_request2size对1024作用得到的值,也就是实际分配的内存大小。在之前的博客文章中提到,多出来的8个字节其实用于了malloc_chunk结构体的前两个成员变量,mchunk_prev_size及mchunk_size。同时也得知两次释放的内存很快被合并,作为最后一次内存分配的返回内存空间了。
但第一次和第二次内存分配返回的指针间隔让我们推测,fprintf(…)在第一次调用时,也分配了1024个字节的内存,可以再次启用gdb调用验证:
如上图,断点加入后,就可以调试了;调试结果表明,我们的推断是正确的:
- 总结
此次调试不是一个“成功”的调试案例;但笔者现在尚未找到通过gdb调试来分析glibc的内存分配模块ptmalloc的实现机制。换句话说,理解ptmalloc的机制需要通过阅读glibc源代码,而gdb只是协助我们理解ptmalloc而己。在理解之后,可能会帮助我们调试一些嵌入式应用的内存异常问题——gdb在此只是一种辅助手段而己。笔者在工作中的一些调试内存异常的案例,因可能涉及公司的一些信息,不能在此分享;此类分析类的调试,不能侧重于gdb调试手段,也不能侧重于的内存相关的问题解决了。
从本次调试过程,笔者需要总结一下:
- 对于连续多次分配内存的请求,如果没有内存的释放过程,ptmalloc通常会从main_arena结构体中的top指针处分配内存;
- 内存被释放后,ptmalloc会有一个“合并”相邻的被释放的内存的过程;
- 与前一篇博客文相同的结论,ptmalloc返回给应用的内存指针之前的8个字节对应着结构体malloc_chunk的前两个成员变量。