关于glibc中的内存分配管理

    技术2022-05-20  30

    关于glibc中的内存分配管理

    在glibc中,使用malloc分配内存时,实际上glibc自己做了相应的堆管理,它先使用brk系统调用,扩展了内存空间,一次最少一个页面4k。进程的堆,并不是直接建立在Linux的内核的内存分配策略上的,而是建立在glibc的堆管理策略上的(也就是glibc的动态内存分配策略上),堆的管理是由glibc进行的。所以我们调用free对malloc得到的内存进行释放的时候,并不是直接释放给操作系统,而是还给了glibc的堆管理实体,而glibc会在把实际的物理内存归还给系统的策略上做一些优化,以便优化用户任务的动态内存分配过程。malloc/calloc/realloc/free这几个函数,是用来分配或释放动态内存的.目前很多Linux系统所用的malloc实现(包括libc5和glibc)都是由Doug Lea完成的.我们下面所讲的,都是指这一版本的实现.(需要注意的是目前高版本的glibc中的malloc实现是用了ptmalloc,比如RH8.0以上的版本中的glibc而不是我现在说的Doug Lea的版本,其中具体的区别等会再说,因为原理还是差不多的)从Linux的Man手册MALLOC(3)中看到这些函数原型如下:void *calloc(size_t nmemb, size_t size);void *malloc(size_t size);void free(void *ptr);void *realloc(void *ptr, size_t size);calloc()用来分配nmemb个size大小的内存块,并返回一个可用内存地址.它会自动将得到的内存块全部清零.

    malloc()用来分配size大小的内存块,并返回一个可用内存地址.free()释放ptr所指向的内存.

    realloc()用来将ptr指向的一块内存的大小改变为size.我们需要注意的是free()和realloc()函数.如果所提供的地址指针ptr所指向的内存是已经释放的,或者不是由malloc类函数分配的话,就可能发生不可预料的情况.我们要利用的,也就是这些"不可预料"的情况.我们可以通过种种手段使其变为我们自己可以"预料"的,当然我们的目的还是为了拿Shell.由于calloc()和malloc()差别不大,实际上都是调用的chunk_alloc()函数来进行分配的,区别只是calloc()在最后调用了一个宏 MALLOC_ZERO来将分配的内存块清零.因此后面除非特别指出,我们就只以malloc()为例.malloc()定义了一个内部结构malloc_chunk来定义malloc分配或释放的内存块.struct malloc_chunk{INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */struct malloc_chunk* fd; /* double links -- used only if free. */struct malloc_chunk* bk;};prev_size是上一个块的大小,只在上一个块空闲的情况下才被填充size是当前块的大小,它包括prev_size和size成员的大小(8字节)fd是双向链表的向前指针,指向下一个块.这个成员只在空闲块中使用bk是双向链表的向后指针,指向上一个块.这个成员只在空闲块中使用对于已分配的内存,除了分配用户指定大小的内存空间外,还在前面增加了malloc_chunk结构的前两个成员(8字节).一段已分配的内存结构如下图所示:0 16 32chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| 上一个块的字节数(如果上一个块空闲的话) | |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| 当前块的字节数 (size) |M|P|mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| 用户数据开始... .. .. (用户可以用空间大小) .. |nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+这里chunk指针是malloc()在内部使用的,而返回给用户的是mem指针(chunk + 8),实际上向用户隐藏了一个内部结构.也就是说,如果用户要求分配size字节内存,实际上至少分配size+8字节,只是用户可用的就是size字节(这里先不考虑对齐问题).nextchunk指向下一个内存块.对于空闲(或者说已经释放的)块,是存放在一个双向循环链表(参见上面的malloc_chunk结构)中的.在内存中的分布基本如下图所示:0 16 32chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| 上一个块的字节数(prev_size) |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+`head:' | 当前块的字节数 (size) |M|P|mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| 前指针(指向链表中的下一个块) |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| 后指针(指向链表中的上一个块) |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| 未被双向链表使用的空间(也可能是0字节长) .. .. |nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+`foot:' | 上一个块的字节数 (等于chunk->size) |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+大家可能主要到两个表中都有一个"P"标志,它是"当前块字节数"(chunk->size)中的最低一位,表示是否上一块正在被使用.如果P位置一,则表示上一块正在被使用,这时chunk->prev_size通常为零;如果P位清零,则表示上一块是空闲块,这是chunk->prev_size就会填充上一块的长度."M"位是表示此内存块是不是由mmap()分配的,如果置一,则是由mmap()分配的,那么在释放时会由munmap_chunk()去释放;否则,释放时由chunk_free()完成.这两位标志相关定义为:#define PREV_INUSE 0x1#define IS_MMAPPED 0x2由于malloc实现中是8字节对齐的,size的低3位总是不会被使用的,所以在实际计算chunk大小时,要去掉标志位.例如:#define chunksize(p) ((p)->size & ~(SIZE_BITS))一次malloc最小分配的长度至少为16字节,例如malloc(0).(上面说的长度是指chunk的长度)了解了上面这些基本概念,我们再来看看free(mem)时做了些什么:首先将mem转换为chunk(mem-8),并调用chunk_free()来释放chunk所指的内存块.然后程序会检查其相邻(包括前后)的内存块是不是空闲的:如果是空闲块的话,就将该相邻块从链表中摘除(unlink),然后将这些相邻的空闲块合并;如果不是空闲块的话,就只是设置后一个相邻块的prev_size和size( 清PREV_INUSE标志).最后将得到的空闲块加入到双向链表中去.在进行unlink操作时,实际上就是执行了一个链表结点的删除工作.比如,如果要从链表中删除chunk结点,所要做得就是:chunk0->fd fdchunk1->bk bk如下所示: chunk0 chunk chunk1+----------------------+..+----------------------+..+----------------------+|prev_size|size|*fd|*bk| |prev_size|size|*fd|*bk| |prev_size|size|*fd|*bk|+----------------^-----+..+----------------+---+-+..+--------------------^-+|_________________________| |_________________________|malloc实现中是使用了一个unlink宏来完成这个操作的,定义如下:/* take a chunk off a list */#define unlink(P, BK, FD) /{ /BK = P->bk; /FD = P->fd; /FD->bk = BK; /BK->fd = FD; /} 这里有两个写内存的操作.如果我们能够覆盖chunk->fd和chunk->bk的话,那么chunk->fd就会写到(chunk->bk + 8)这个地址,而chunk->bk就会被写到(chunk->fd + 12)这个地址!也就是我们可以将任意4个字节写到任意一个内存地址中去!!我们就可能改变程序的流程,比如覆盖函数返回地址,覆盖GOT,.dtor结构等等,这不正是我们所要的吗 free()和realloc()中都有unlink操作,因此我们要做的就是要想办法用合适的值来覆盖空闲块结构中的*fd和*bk,并让unlink能够执行.下面我们来看free()是怎么工作的,注意下面的代码为了说明的方便做了一些简化:void fREe(Void_t* mem){...(a) if (chunk_is_mmapped(p)) /* 如果IS_MMAPPED位被设置,则调用munmap_chunk() */{munmap_chunk(p);return;}...p = mem2chunk(mem); /* 将用户地址转换成内部地址: p = mem - 8 */...chunk_free(ar_ptr, p);}

    static voidinternal_functionchunk_free(arena *ar_ptr, mchunkptr p) {INTERNAL_SIZE_T hd = p->size; /* hd是当前块地址 */INTERNAL_SIZE_T sz; /* 当前块大小 */INTERNAL_SIZE_T nextsz; /* 下一个块大小 */INTERNAL_SIZE_T prevsz; /* 上一个块大小 */

    ...

    check_inuse_chunk(ar_ptr, p);sz = hd & ~PREV_INUSE; /* 取得当前块的真实大小 */next = chunk_at_offset(p, sz); /* 得到下一个块的地址 */nextsz = chunksize(next); /* 得到下一个块的真实大小 * #define chunksize(p) ((p)->size & ~(SIZE_BITS))*/if (next == top(ar_ptr)) /* 如果下一个块是头结点,则与之合并 */{sz += nextsz;(b) if (!(hd & PREV_INUSE)) /* 如果上一个块是空闲的,则与之合并*/{prevsz = p->prev_size;p = chunk_at_offset(p, -prevsz);sz += prevsz;unlink(p, bck, fwd); /* 从链表中删除上一个结点 */}set_head(p, sz | PREV_INUSE);top(ar_ptr) = p;..... }/* 如果下一个块不是头结点 */ (b) if (!(hd & PREV_INUSE)) /* 如果上一个块是空闲的,则与之合并*/{prevsz = p->prev_size;p = chunk_at_offset(p, -prevsz);sz += prevsz;if (p->fd == last_remainder(ar_ptr)) /* keep as last_remainder */islr = 1;elseunlink(p, bck, fwd); /* 从链表中删除上一个结点 */}/* 根据我的判断,刚才的程序,是在进行这个检查时发生段错误的 */(c)if (!(inuse_bit_at_offset(next, nextsz)))/* 如果下一个块是空闲的,则与之合并*/{sz += nextsz;if (!islr && next->fd == last_remainder(ar_ptr))/* re-insert last_remainder */{islr = 1;link_last_remainder(ar_ptr, p);}elseunlink(next, bck, fwd);/* 从链表中删除下一个结点 */next = chunk_at_offset(p, sz);}elseset_head(next, nextsz); /* 如果前后两个块都不是空闲的,则将下一个块的size 中的PREV_INUSE位清零 */ set_head(p, sz | PREV_INUSE);next->prev_size = sz; /* 将下一个块的prev_size部分填成当前块的大小 */if (!islr)frontlink(ar_ptr, p, sz, idx, bck, fwd); /* 将当前这个块插入空闲块链表中 */.....}我们看到这里面有3个地方调用了unlink.如果想要执行它们,需要满足下列条件:1. (a) 当前块的IS_MMAPPED位必须被清零,否则不会执行chunk_free()2. (b) 上一个块是个空闲块 (当前块size的PREV_INUSE位清零)或者(c) 下一个块是个空闲块(下下一个块(p->next->next)size的PREV_INUSE位清零)我们的弱点程序发生溢出时,可以覆盖下一个块的内部结构,但是并不能修改当前块的内部结构,因此条件(b)是满足不了的.我们只能寄希望于条件(c).所谓下下一个块的地址其实是由下一个块的数据来推算出来的.

    RH8.0上却不行,是因为在新版本的glibc库中堆内存管理采用了Wolfram Gloger的ptmalloc/ptmalloc2代码.ptmalloc2代码是从Doug Lea的代码移植过来的,主要目的是增加对多线程(尤其是SMP系统)环境的支持,同时进一步优化了内存分配,回收的算法.由于在ptmalloc2中引入了fastbins机制,malloc()/free()溢出在某些条件下会受到更多的限制,虽然作者的本意并不是针对溢出攻击.由于fastbins是单向链表数组,每一个fastbin是一个单向链表,满足fastbins条件的内存块回收时将被放入相应的fastbin链表中,以便在以后的内存申请时能更快地再被分配出去,从而提高性能.

     

    关于fastbins理解如下:这个程序是我看malloc_consolidate时的一个知识点验证程序,并不是说有多实用,多精巧 :)fastbins的概念就是小于av->max_fast (最大为80,默认为64,可以通过mallopt指定)的小堆如果每次free时都进行和周围空块的合并工作并不能有效增大空块的大小而且很可能紧接着会再次出现小空间的malloc请求,作为折中每次free这些小堆时并不进行合并工作,而是将他们记录到av->fastbins里边,按大小组成多个等大小的单链表,每个链表头就在fastbins[]数组中;当有堆malloc请求或free后空快的size >0xffff时就调用malloc_consolidate进行一次清理工作:将fastbins里的各个链表遍历一遍,对每个链表上的块进行和周围空闲块的合并工作(如果前一个块为空闲则unlink前块,如果后一个块是空闲则unlink后一个块),合并后将整个块放到正常的unsorted_chunks中形成正常的空闲块,并从fastbins的链表里删除。

     

    详细参见:http://www.zzdnyy.com/cyclopedia/memory/21632.shtml


    最新回复(0)