ptmalloc - 小小内存的分配和申请
glibc中malloc的代码包括了线程同步,平台兼容性等问题,但是本系列文章主要的研究对象ptmalloc。所以提供的代码都是经过简化,部分宏也会展开,能够说清楚ptmalloc的运行流程就可以了。运行glibc代码的环境是x86_64,有些根据平台进行选择的宏就直接使用x86_64的。
要点
- 用例
- 申请内存
- 释放小内存
- 释放后的重新获取
- 最后的清除
用例
int main(void)
{
void *m1 = malloc(0x20);
void *m2 = malloc(0x20);
void *m3 = malloc(0x20);
free(m2);
m2 = malloc(0x20);
free(m1);
free(m2);
free(m3);
return -1;
}
申请内存
这次的用例里面居然出现了4个malloc,会不会很慌。
淡定,只要看过前面三章的都懂,一开始申请的时候,malloc的内存还是很纯净的。只用sbrk申请了一块DEFAULT_TOP_PAD + x + MIN_SIZE(详细解释请看第一次申请小内存)大小的内存,基本上,三次申请0x20都是直接从那里划分出来的。那块内存可以叫为top。
释放小内存
好了,我们来看第7行的free了,这次的free挺重要的,带领我们去认识一个在malloc_state里面一个重要的成员,fastbinY。具体用来干嘛的,直接上代码解释。
void __libc_free(void *mem)
{
/* other code */
...
mchunkptr p = ((mchunkptr)((char*)(mem) - 2 * SIZE_SZ));
_int_free(&main_arena, p, 0);
}
__libc_free基本已经过了一遍了,除了一开始直接调用munmap释放mmap申请的内存外,剩余的作用便是调用_int_free了。
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
INTERNAL_SIZE_T size = chunksize (p);
const char *errstr = NULL;
static int check_action = DEFAULT_CHECK_ACTION;
/* [1] */
if ((uintptr_t) p > (uintptr_t) -size) {
errstr = "free(): invalid pointer";
errout:
malloc_printerr (check_action, errstr, chunk2mem (p), av);
return;
}
#define MALLOC_ALIGNMENT (2 * sizeof(size_t))
#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)
#define aligned_OK(m) (((unsigned long)(m) & MALLOC_ALIGN_MASK) == 0)
/* [2] */
if (size < MINSIZE || !aligned_OK (size))) {
errstr = "free(): invalid size";
goto errout;
}
先看上面的代码,两段检查代码,[1]检查指针地址的合法性,[2]首先检查需要释放的内存的大小是否符合最小内存块大小,接着检查内存块是否对齐,在我本机MALLOC_ALIGNMENT是16,那么,aligned_OK检查的就是size是否是16的倍数。
#define chunk_at_offset(p, s) ((mchunkptr) (((char *) (p)) + (s)))
#define chunksize_nomask(p) ((p)->mchunk_size)
#define chunksize(p) (chunksize_nomask (p) & ~(SIZE_BITS))
/* [3] */
static int global_max_fast = 128;
mfastbinptr *fb;
if ((unsigned long)(size) <= (unsigned long)global_max_fast) {
if (chunksize_nomask (chunk_at_offset (p, size)) <= MINSIZE ||
chunksize(chunk_at_offset (p, size)) >= av->system_mem, 0)) {
errstr = "free(): invalid next size (fast)";
goto errout;
}
}
#define FASTCHUNKS_BIT (1U)
#define set_fastchunks(M) ((M)->flags &= ~FASTCHUNKS_BIT);
set_fastchunks(av);
/* offset 2 to use otherwise unindexable first 2 bins */
#define fastbin_index(sz) \
((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
unsigned int idx = fastbin_index(size);
#define fastbin(ar_ptr, idx) ((ar_ptr)->fastbinsY[idx])
fb = &fastbin (av, idx);
当你看到global_max_fast的时候,你就知道我为何选0x20这个值了,小于128就可以成为fastbin的一员了。
然后通过两个检查,一个检查内存块大小是否小于最低大小和内存大小是否超过已申请的内存大小,顺便也检查了大小值是否被覆写为负数。
接下来通过set_fastchunks设置标志位,使用fastbin_index根据大小定位存在哪条链表当中,最后就是根据idx取地址值。
/* [4] */
mchunkptr old = *fb, old2;
do {
/* 检查bin的顶部等不等于准备加入的值
(检查二次释放(double free)). */
if (old == p) {
errstr = "double free or corruption (fasttop)";
goto errout;
}
p->fd = old2 = old;
/* 如果fb的值等于old2,就用p重写,否则不改动,
返回值为fb改动前的值 */
old = __sync_val_compare_and_swap(fb, old2, p);
} while (old != old2);
}
[4]就是将当前需要操作的内存块链在链表上,细节注释也说清楚了。
释放后的重新获取
看过我前面章节的应该知道,m2的重新申请肯定是经过_int_malloc这个函数,直接上代码了,挺简单的一段
static void *
_int_malloc(mstate av, size_t bytes)
{
size_t nb;
unsigned int idx;
/* 第一次申请小内存已经解释过了 */
checked_request2size(bytes, nb);
/* 0x20小于0x40,所以代码走这条分支 */
if ((unsigned long)nb <= (unsigned long)global_max_fast))
{
idx = fastbin_index (nb);
mfastbinptr *fb = &fastbin (av, idx);
mchunkptr pp = *fb;
do {
victim = pp;
if (victim == NULL)
break;
/* 如果*fb等于victim,就用victim->fd重写,
否则不改动,
返回值为fb改动前的值 */
pp = __sync_val_compare_and_swap(fb, victim, victim->fd);
} while (pp != victim);
/* 如果存在有用的fastbin块,就返回 */
if (victim != 0)
{
/* 用于检查这段时间窗口,fastbin是否被破坏 */
if (fastbin_index (chunksize (victim)) != idx)
{
errstr = "malloc(): memory corruption (fast)";
malloc_printerr (check_action, errstr, chunk2mem (victim), av);
return NULL;
}
return chunk2mem (victim);
}
}
}
基本上,流程到了这里,这篇文章基本就快完结了,malloc / free的代码第一眼看下去还是挺恐怖的。不过,一块一块的去看,带着目的性去看,还是能够啃下来的。
最后的清除
代码最后是三个free,走的流程你也知道的,就是放在fastbin里面。
好了,该完结了。
应该完结么?
这三次申请的内存是从top划分出来的。
top的内存是从哪里来的,sbrk。
使用sbrk申请是sbrk(正数),释放是sbrk(负数)。这么多代码里面都没见到再次调用sbrk。泄露了?
在这里,先放着几个猜想,那块内存太小了,不影响什么,内核自己收回去便是。或者说,挂了钩子在atexit,最终退出的时候会调用。
完结撒花