本文记录了笔者调试一个简单的由C语言编写的嵌入式应用,在main函数执行之后,第一次调用内存分配函数时,glibc的执行过程。对于普通的二进制应用,Linux内加载应用并执行,首先运行的是动态链接器;在应用的main函数被调用之前,动态链接器也会分配内存:不过动态链接器没用使用到libc.so中的内存分配功能模块,据笔者当前的调试经历,动态链接器是过mmap来分配私有的匿名内存的。
笔者调试的应用,在main函数中间接地调用了fgets(…)这个函数,在fgets()函数调用的另一些函数中,最终会尝试分配1024字节的内存空间大小。有兴趣的读者可以自行编写简单的应用来调试。
在glibc源码中,malloc/malloc.c文件中包含了大量的注释说明信息,为我们理解、调试ptmalloc功能有很大的帮助;笔者在此就不再鹦鹉学舍了,为了方便大家查看,笔者对两个重要结构体图如下:
其中mchunkptr被定义为malloc_chunk的结构体指针;对于ARMv7平台,NFASTBINS为10,NBINS为128,BINMAPSIZE为4。结构体malloc_state的指针通常被定义为mstate;详细的定义请查看glibc源码。
在笔者的博客文章《GDB内存调试初探一》中,提到了__libc_malloc函数会通过一个钩子来初始化内存状态结构体静态变量main_arena;但经笔者调试发现ptmalloc_init()函数并未修改此变量,而仅仅将其地址写入指针变量thread_arena中。首先,在执行ptmalloc_init函数前后加入断点:
之后,在两个断点触发时查看main_arena变量:
经比较可知,执行函数ptmalloc_init前后静态结构体变量main_arena没有被修改。注意到main_arena结构体中的next指针指向该结构体本身。不过,函数ptmalloc_init对进程的环境变量进行了一些处理,即检测有没有与内存分配相关的环境变量;如果有,就会对内存分配的功能进行一些调整。当前调试的应用使用的是默认的环境变量,没有与内存分配相关的环境变量。
此时main_arena中的max_system变量和max_system_mem均为零,说明内存分配状态结构体不能分配内存;如果要为应用分配内存,那么须先向Linux内核发起分配内存的系统调用。
在函数ptmalloc_init执行完成之后,会跳转回到__libc_malloc()函数;后者会调用到_int_malloc函数进一步基于main_arena结构体变量来分配内存。本次调试的应用请求的内存分配大小为1024个字节,不过最终分配的内存比1024字节稍大一些,这是通过宏定义checked_request2size来实现的:
如上图,函数_int_malloc的第二个参数bytes为1024,经过checked_request2size宏处理,函数局部变量nb被赋值成为1032,即最终会尝试分配1032个字节;不过对于应用而言,仅使用1024个字节是安全的:ptmalloc内存分配模块会使用1032字节的前几个字节保存一些内存分配相关的信息。
接下来函数_int_malloc会根据请求分配的内存大小进行一些特殊的处理。不过此次调试的应用请求分配的内存大小为1032个字节(即nb = 1032),均不满足条件,下图中的两个条件分支均没有执行:
现在这两个分支均未执行,并不是很重要:此时内存分配状态结构体中没用空闲的内存供以分配:最终函数_int_malloc不得不调用sysmalloc以向内核请求更多的内存。
上面提到,ptmalloc_init并未修改main_arena结构体变量;实际上,main_arena的初始化函数为malloc_init_state()。不过,该函数也不会为main_arena向Linux内核申请可用的内存,它仅仅是将其初始化;初始化完成后,main_arena结构体会发生一些变化:
上面的调试流程为:_int_malloc(…) -> malloc_consolidate(…) -> malloc_init_state(…);上图的调试结果为返回到_int_malloc(…)的结果。函数malloc_init_state将global_max_fast赋值为64;该变量的作用留待以后考察。注意,main_arena结构体指针top被初始化为top指针所在的地址;结构体中的bins指针数组也指向临近的内存空间。这是很有趣的设计,其具体的功能特点尚待我们去发掘。
以上的分析结果说明,尽管main_arena被函数malloc_init_state()初始化,但其没有可用的内存空间分配给应用。它必须向内核申请内存;那么就直接给分配/释放内存的系统调用brk加上捕捉断点(catch):
上图给出了完整的调用栈回溯信息,不过下图给出了该系统调用的前后信息:
此次系统调用的参数为0,即系统调用为brk(NULL),并没有向Linux内核申请内存。内核返回的值应当被视为当前堆(Heap)的起始地址。之后,brk系统调用再次被触发:
上图的调试结果表明,ptmalloc内存分配模块为main_arena向Linux内核申请了0x21000个字节的内存空间,即132K(或135168字节),这远远大于应用第一次请求分配的1024个字节的空间大小。那么可推测,剩余的空间会被记录在main_arena结构体中,留待下次应用分配内存时使用。
应用只要求libc分配1K大小的内存,但libc却一次性向内核请求了132K大小的内存。main_arena中需要记录这些信息:
如上图,top指向了堆空间的起始地址;在堆空间的起始地址偏移4字节处,写入了堆空间的大小与PREV_INUSE相与,即0x21001(上图中的135169)。
首先让我们根据代码先计算一下为了向应用分配1024字节的空间,需要对main_arena进行哪些修改,以及向应用返回的内存地址:
上图的计算结显示,分配的内存地址应该为0x2a013008。然后加入一个新的断点,让上图的代码执行完成并返回至_int_malloc函数中,查看内存分配状态结构体main_arena:
由此可以确定依据C代码手动计算的结果与实际运行的结果一致。最后在函数__libc_malloc的返回地址加上断点,查看其返回值,与我们的预测结果也是一致的:
至此,我们对简单应用第一次分配内存的基本的流程,就有了一个基本的了解了。
对于简单的C语言编写的应用,在main函数执行之后才会对glibc中的内存分配模块ptmalloc进行初始化。该模块通过brk系统调用向Linux内存申请堆空间,并将其存储于main_arena结构体中。该结构体的用于记录主线程的内存的分配与释放信息。具体地来说,本次调试案例分配的堆内存地址空间起始地址为0x2a013000,而应用获得的内存地址为0x2a013008,共偏移了8个字节,即两个4字节。该两个4字节分别对应结构体malloc_chunk中的mchunk_prev_size和mchun_size。笔者已将本次调试的简单应用及源码上传至下载区,有需要的可以下载自己调试分析。