丁增贤: glibc堆探秘系列之fastbin ——上集
作者:丁增贤 笔名:林江川 id:ljcnaix
毕业于电子科技大学信息与软件工程学院,工作刚满1年零3天。本科期间对底层很感兴趣,技能树一路点歪,目前在做安全相关的工作。对二进制漏洞的挖掘、分析和利用有一些实践。
欢迎给Linuxer投稿,获得精美礼品和其他福利:
或许你和我一样,经常需要和内存打交道,尤其是动态分配释放的堆内存。渐渐的就萌生出到glibc内部去看看的想法。但是直接读ptmalloc(glibc的堆内存管理是基于ptmalloc2实现的)的源码有点枯燥而且没有头绪。如果是这样,那么请你往下读,这一系列文章或许不能一步到位替你讲清楚ptmalloc,但它会围绕一个个有意思的示例程序,一步步向你展现ptmalloc的实现,为你阅读源码穿针引线。今天我们将一起来了解fastbin,fastbin是处理小内存(64位小于120比特,32位小于60比特)分配释放的重要数据结构。读完今天的文章,你将了解当申请释放一块小内存时,glibc内部会发生什么。让我们从一个有意思的示例程序开始吧。
1. 一个奇怪的栗子
// 本示例程序修改自shellphish how2heap项目中的fastbin_dup.c
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *a = malloc(8);
int *b = malloc(8);
int *c = malloc(8);
printf("1st malloc(8): %p\n", a);
printf("2nd malloc(8): %p\n", b);
printf("3rd malloc(8): %p\n", c);
free(a);
// free(a);
free(b);
free(a);
printf("1st malloc(8): %p\n", malloc(8));
printf("2nd malloc(8): %p\n", malloc(8));
printf("3rd malloc(8): %p\n", malloc(8));
这段程序首先申请了三块8字节的内存,内存的首地址保存在栈上的变量a、b和c中。然后依次释放a指向的内存、b指向的内存,最后再次释放a指向的内存。最后再次申请三次内存,并打印malloc函数的返回结果。你应该已经发现了,这是一段有问题的程序,变量a被释放了两次。我们知道在调用free函数释放一块内存后,马上再次释放同一块内存会导致程序崩溃,如果将示例中的注释去掉,编译运行程序就会崩溃。那么我们的示例程序是不是也会崩溃呢?会不会出现一些非预期的问题呢?我们将示例程序编译运行,下面是我在64位ubuntu 16.04上的运行结果。
$ ./fastbin_dup
1st malloc(8): 0x55e6271c7010
2nd malloc(8): 0x55e6271c7030
3rd malloc(8): 0x55e6271c7050
1st malloc(8): 0x55e6271c7010
2nd malloc(8): 0x55e6271c7030
3rd malloc(8): 0x55e6271c7010
可以看到程序顺利执行,并没有触发崩溃。仔细观察一下程序输出,会发现最后三次malloc函数的调用发生了异常,地址0x55e6271c7010被返回了两次。而且仔细观察一下会发现,malloc返回的地址,正好是程序刚刚释放的内存。可能你在实际的项目中已经遇到多次malloc返回相同地址的问题,一般有经验的程序员会去找什么地方多次free了。今天我想把这个问题讲讲清楚,并且介绍一下glibc堆内存管理中的fastbin。
2. 使用调试器寻找答案
要弄清楚上一节提到的问题,我们不能停留在free、malloc这些库函数的层面,需要深入glibc进行调试,在这之前你需要做一点准备工作。首先你需要下载glibc的源码,ubuntu 16.04下只要执行如下命令:
sudo apt install glibc-source
glibc源码的压缩文件会被下载到“/usr/src/glibc/”目录下,我们自行解压到工作目录即可。然后我们需要下载glibc的符号表,使用下面的命令:
sudo apt install libc6-dbg
最后在启动gdb调试后,需要设置源码搜索路径,由于我们调试的free、malloc这些库函数在“malloc/malloc.c”文件中实现,所以使用如下命令:
directory ${your_glibc_source_path}/malloc
下面我们正式开始调试。在14行下断点,使用“s”命令,单步进入free函数内部。我们会发现进入了一个名为__libc_free函数的内部。实际上我们调用的free函数是__libc_free函数的别名,在malloc.c中有如下代码,设置函数别名:
strong_alias (__libc_free, free)
strong_alias (__libc_malloc, malloc)
所以我们调用malloc、free函数时,真正执行的是__libc_free和__libc_malloc函数。
3. __libc_free函数
事实上对于__libc_free函数,我们需要关注的代码只有很少的几行,因为大部分是超大内存分配以及ptmalloc初始化相关的代码。简化的__libc_free函数,如下所示:
void __libc_free(void *mem) {
mstate ar_ptr;
mchunkptr p;
if (mem == 0)
return;
p = mem2chunk(mem); // 根据分配的内存地址获取chunk地址
ar_ptr = arena_for_chunk(p); // 根据chunk地址获取arena地址
_int_free(ar_ptr, p, 0);
}
首先判断传入的指针是否为空,然后使用两个宏对地址做了简单的处理,最后调用_int_free函数。这两个宏分别和两个重要概念相关,一个是chunk,一个是arena。arena的原意是运动场、舞台,我们可以理解为场地,顾名思义arena为内存的分配管理等一系列活动提供最基本的场地。chunk的原意是块,chunk是内存分配的基本单位。ptmalloc就是在arena这片场地上,把内存分块进行分配和管理的。
不管内存是在哪里被分配的,用什么方法分配,用户请求分配的空间在ptmalloc中都使用一个chunk来表示。用户调用free()函数释放掉的内存也并不是立即就归还给操作系统,相反,它们也会被表示为一个chunk,ptmalloc使用特定的数据结构来管理这些空闲的chunk。
——《glibc内存管理ptmalloc2源代码分析》 by 华庭
返回给用户的内存和chunk、arena之间的关系,可以粗略的用下图 3 1表示。
图 3.1 arena、chunk和分配给用户程序的内存之间的关系
_libc_free函数就是获取要释放的内存对应的chunk和arena的地址,然后传给_int_free函数进行进一步的操作,可见对于ptmalloc内存管理的基本单位还是chunk。arena的结构稍微复杂一些,我们不去关注它的细节,chunk的结构比较简单,也是后续代码要操作的,我们简单来介绍一下。chunk的首部通过结构体malloc_chunk定义:
typedef struct malloc_chunk *mchunk_ptr;
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; // 如果前一个块是空闲的,该域表示前一个块的大小
INTERNAL_SIZE_T size; // 当前块的大小,并且记录了当前chunk和前一个chu-
// nk的一些属性,包括前一个chunk是否正在使用,当
// 前chunk是否是通过mmap分配的内存,当前chunk是
// 否属于main_arena
struct malloc_chunk *fd; // fd和bk指针只有在当前块空闲时才存在,其作用是
struct malloc_chunk *bk; // 将当前块加入到空闲chunk块链表中统一管理,如
// 果当前chunk块已经被分配给应用程序使用,那么
// 这两个域就没有用了,将作为分配给应用程序的内
// 存使用
/* 下面两个域只在large bin中使用 */
struct malloc_chunk *fd_nextsize;
struct malloc_chunk *bk_nextsize;
}
其中INTERNAL_SIZE_T是一个为了跨平台支持而定义的宏。
#ifndef INTERNAL_SIZE_T
#define INTERNAL_SIZE_T size_t
#endif
我们今天的示例分配的是小内存,所以malloc_chunk中最后两个域就不用考虑了。需要指出的是,返回给用户的内存地址从malloc_chunk的域fd开始。可以从宏mem2chunk的实现来验证这一点。
/* 根据返回给用户的内存地址获得chunk地址*/
#define mem2chunk(mem) ((mchunk_ptr)((char *)mem - 2 * SIZE_SZ))
可以看到传入返回给用户程序的内存地址后,减去两个SIZE_SZ,就得到chunk的起始地址。SIZE_SZ也是为跨平台支持而定义的宏,定义如下所示:
/* SIZE_SZ为屏蔽平台的差异,定义为size_t的大小 */
#define SIZE_SZ (size(INTERNAL_SIZE_T))
SIZE_SZ就是域size/prev_size的大小,所以返回给用户程序的内存地址是从域fd开始的。此外prev_size域仅当前一个块是空闲时才有效,否则会作为前一个chunk的可用内存的一部分。
下集敬请期待....
往期精彩回顾>>>
CSDN直播:深入探究Linux/VxWorks的设备树(Device Tree)
考试:Linux/ARM 设备树(Device Tree)的知识自测题
让天堂的归天堂,让尘土的归尘土——谈Linux的总线、设备、驱动模型
...
公众号:linuxer
ID:LinuxDev
长按二维码关注我们