查看原文
其他

丁增贤: glibc堆探秘系列之fastbin ——上集

丁增贤 Linux阅码场 2018-06-20


作者:丁增贤  笔名:林江川  id:ljcnaix


毕业于电子科技大学信息与软件工程学院,工作刚满1年零3天。本科期间对底层很感兴趣,技能树一路点歪,目前在做安全相关的工作。对二进制漏洞的挖掘、分析和利用有一些实践。

欢迎给Linuxer投稿,获得精美礼品和其他福利:

 在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总线、设备、驱动模型》直播PPT分享

让天堂的归天堂,让尘土的归尘土——谈Linux的总线、设备、驱动模型

宋宝华: 论一个程序员问问题的自我修养

...


公众号:linuxer

ID:LinuxDev

长按二维码关注我们




    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存