[TOC]
GLIBC 版本:
commit 36f2487f13e3540be9ee0fb51876b1da72176d3f (grafted, HEAD, tag: glibc-2.38)
Author: Andreas K. Hüttel <dilfridge@gentoo.org>
Date: Mon Jul 31 19:54:16 2023 +0200
NEWS: Fix typos
Signed-off-by: Andreas K. Hüttel <dilfridge@gentoo.org>
__libc_malloc 走马观花
一个运行中程序中的 bins
pwndbg> bins
tcachebins
0x20 [ 1]: 0xba6ff66bac80 ◂— 0x0
0x30 [ 1]: 0xba6ff66cc990 ◂— 0x0
0x60 [ 1]: 0xba6ff66bdde0 ◂— 0x0
0x70 [ 2]: 0xba6ff66cebc0 —▸ 0xba6ff66a4c00 ◂— 0x0
0x80 [ 1]: 0xba6ff66a6ff0 ◂— 0x0
0xa0 [ 1]: 0xba6ff66bfd30 ◂— 0x0
0xb0 [ 2]: 0xba6ff66a34f0 —▸ 0xba6ff66d0390 ◂— 0x0
0xe0 [ 1]: 0xba6ff66a3410 ◂— 0x0
0xf0 [ 1]: 0xba6ff66ad7a0 ◂— 0x0
0x100 [ 1]: 0xba6ff66d0460 ◂— 0x0
0x110 [ 1]: 0xba6ff66c88c0 ◂— 0x0
0x120 [ 1]: 0xba6ff66a8bf0 ◂— 0x0
0x140 [ 1]: 0xba6ff66a26b0 ◂— 0x0
0x1d0 [ 1]: 0xba6ff66a27f0 ◂— 0x0
0x1e0 [ 1]: 0xba6ff66ad5c0 ◂— 0x0
0x230 [ 1]: 0xba6ff66b64f0 ◂— 0x0
0x260 [ 2]: 0xba6ff66c6630 —▸ 0xba6ff66a8910 ◂— 0x0
0x270 [ 1]: 0xba6ff66a29c0 ◂— 0x0
0x280 [ 1]: 0xba6ff66c6920 ◂— 0x0
0x290 [ 1]: 0xba6ff66c1b40 ◂— 0x0
0x2a0 [ 2]: 0xba6ff66b6720 —▸ 0xba6ff66a6960 ◂— 0x0
0x2c0 [ 1]: 0xba6ff66ce7d0 ◂— 0x0
0x2d0 [ 1]: 0xba6ff66bdb10 ◂— 0x0
0x300 [ 1]: 0xba6ff66b8570 ◂— 0x0
0x310 [ 1]: 0xba6ff66d8650 ◂— 0x0
0x340 [ 1]: 0xba6ff66a48a0 ◂— 0x0
0x370 [ 1]: 0xba6ff66bc1c0 ◂— 0x0
0x380 [ 1]: 0xba6ff66a4520 ◂— 0x0
0x3d0 [ 1]: 0xba6ff66ba8b0 ◂— 0x0
0x3f0 [ 1]: 0xba6ff66a6c00 ◂— 0x0
0x410 [ 1]: 0xba6ff66d9960 ◂— 0x0
fastbins
empty
unsortedbin
all: 0xba6ff66b9690 —▸ 0xe1b9d1800ab0 (main_arena+96) ◂— 0xba6ff66b9690
smallbins
0x20: 0xba6ff66c8890 —▸ 0xba6ff66d0430 —▸ 0xba6ff66a4bd0 —▸ 0xe1b9d1800ac0 (main_arena+112) ◂— 0xba6ff66c8890
0x40: 0xba6ff66a44d0 —▸ 0xba6ff66bc170 —▸ 0xe1b9d1800ae0 (main_arena+144) ◂— 0xba6ff66a44d0
0x50: 0xba6ff66bdab0 —▸ 0xba6ff66a3590 —▸ 0xe1b9d1800af0 (main_arena+160) ◂— 0xba6ff66bdab0
0x60: 0xba6ff66a88a0 —▸ 0xba6ff66ce760 —▸ 0xe1b9d1800b00 (main_arena+176) ◂— 0xba6ff66a88a0
0x70: 0xba6ff66a3390 —▸ 0xba6ff66bc520 —▸ 0xe1b9d1800b10 (main_arena+192) ◂— 0xba6ff66a3390
0x80: 0xba6ff66a8b60 —▸ 0xe1b9d1800b20 (main_arena+208) ◂— 0xba6ff66a8b60
0x90: 0xba6ff66c6880 —▸ 0xe1b9d1800b30 (main_arena+224) ◂— 0xba6ff66c6880
0xa0: 0xba6ff66c1a90 —▸ 0xe1b9d1800b40 (main_arena+240) ◂— 0xba6ff66c1a90
0xe0: 0xba6ff66d9870 —▸ 0xe1b9d1800b80 (main_arena+304) ◂— 0xba6ff66d9870
0x130: 0xba6ff66cea80 —▸ 0xe1b9d1800bd0 (main_arena+384) ◂— 0xba6ff66cea80
0x170: 0xba6ff66a67e0 —▸ 0xe1b9d1800c10 (main_arena+448) ◂— 0xba6ff66a67e0
0x180: 0xba6ff66b83e0 —▸ 0xe1b9d1800c20 (main_arena+464) ◂— 0xba6ff66b83e0
0x1f0: 0xba6ff66bd0c0 —▸ 0xe1b9d1800c90 (main_arena+576) ◂— 0xba6ff66bd0c0
0x2a0: 0xba6ff66d00e0 —▸ 0xe1b9d1800d40 (main_arena+752) ◂— 0xba6ff66d00e0
0x2d0: 0xba6ff66a6220 —▸ 0xe1b9d1800d70 (main_arena+800) ◂— 0xba6ff66a6220
0x2e0: 0xba6ff66aeaf0 —▸ 0xe1b9d1800d80 (main_arena+816) ◂— 0xba6ff66aeaf0
largebins
0x980-0x9b0: 0xba6ff66bf390 —▸ 0xe1b9d1801000 (main_arena+1456) ◂— 0xba6ff66bf390
0x3000-0x3ff0: 0xba6ff66c89c0 —▸ 0xe1b9d18011b0 (main_arena+1888) ◂— 0xba6ff66c89c0
0x4000-0x4ff0: 0xba6ff66a8d00 —▸ 0xba6ff66c1dc0 —▸ 0xe1b9d18011c0 (main_arena+1904) ◂— 0xba6ff66a8d00
0x7000-0x7ff0: 0xba6ff66aeff0 —▸ 0xe1b9d18011f0 (main_arena+1952) ◂— 0xba6ff66aeff0
0x8000-0x8ff0: 0xba6ff66d0550 —▸ 0xe1b9d1801200 (main_arena+1968) ◂— 0xba6ff66d0550
pwndbg>
// 这tcache 中的某条 bins 链表,由大小为 0x260 的 chunks 组成,链表头地址为 0xba6ff66c6630
0x260 [ 2] : 0xba6ff66c6630 —▸ 0xba6ff66a8910 ◂— 0x0
chunk 大小 tcache.counts 头节点
这是 smallbins 中的第一条 bins 链表,由大小为 0x20 的 chunks 组成,头节点的地址为 0xba6ff66c8890,末尾节点指向了头节点,表示这是一条循环链表。
smallbins
0x20: 0xba6ff66c8890 —▸ 0xba6ff66d0430 —▸ 0xba6ff66a4bd0 —▸ 0xe1b9d1800ac0 (main_arena+112) ◂— 0xba6ff66c8890
0xba6ff66c8890 地址并不是 chunk 的开始地址,而是用户数据地址,假如这条链表的叫 node1,那么 node1->next 指向 0xba6ff66d0430,ode1->next
的地址为 0xba6ff66c8890 + 0x10
pwndbg> x 0xba6ff66c8890 + 0x10
0xba6ff66c88a0: 0x0000ba6ff66d0430
malloc(535)
首先对 535 进行对齐,然后 csize2tidx 计算 tc_idx 的值:
// malloc/malloc.c
3293 size_t tbytes = checked_request2size (bytes);
3299 size_t tc_idx = csize2tidx (tbytes);
checked_request2size:对用户请求的大小进行对齐,达到chunk 要求的标准大小。而 tc_idx 是 tcache 链表的索引,这个索引通过 csize2tidx 计算得到。
checked_request2size:
1317 static inline size_t
1318 checked_request2size (size_t req) __nonnull (1)
1319 {
1320 if (__glibc_unlikely (req > PTRDIFF_MAX))
1321 return 0;
1322
1323 /* When using tagged memory, we cannot share the end of the user
1324 block with the header for the next chunk, so ensure that we
1325 allocate blocks that are rounded up to the granule size. Take
1326 care not to overflow from close to MAX_SIZE_T to a small
1327 number. Ideally, this would be part of request2size(), but that
1328 must be a macro that produces a compile time constant if passed
1329 a constant literal. */
1330 if (__glibc_unlikely (mtag_enabled))
1331 {
1332 /* Ensure this is not evaluated if !mtag_enabled, see gcc PR 99551. */
1333 asm ("");
1334
1335 req = (req + (__MTAG_GRANULE_SIZE - 1)) &
1336 ~(size_t)(__MTAG_GRANULE_SIZE - 1);
1337 }
1338
1339 return request2size (req);
1340 }
request2size 是一个宏:
1 macro request2size
2
3 #define request2size(req) \
4 (((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) \
5 ? MINSIZE \
6 : ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)
csize2tidx 也是一个宏:
macro csize2tidx
Type: size_t (aka unsigned long)
#define csize2tidx(x) \
(((x) -MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)
checked_request2size 是 inline 函数,request2size 是 macro,这就导致难以调试,csize2tidx 也是如此。
观察输出的最好方法就是使用 gcc 完全展开这两个宏,包裹进函数里,这样就可以迅速得到一张 checked_request2size & csize2tidx 的返回值表,具体看 main 函数:
#include "includd.h"
#include <stdio.h>
size_t csize2tidx(size_t tbytes) {
return (((tbytes) -
(unsigned long)((
((__builtin_offsetof(struct malloc_chunk, fd_nextsize)) +
((2 * (sizeof(size_t)) < __alignof__(long double)
? __alignof__(long double)
: 2 * (sizeof(size_t))) -
1)) &
~((2 * (sizeof(size_t)) < __alignof__(long double)
? __alignof__(long double)
: 2 * (sizeof(size_t))) -
1))) +
(2 * (sizeof(size_t)) < __alignof__(long double)
? __alignof__(long double)
: 2 * (sizeof(size_t))) -
1) /
(2 * (sizeof(size_t)) < __alignof__(long double)
? __alignof__(long double)
: 2 * (sizeof(size_t))));
}
size_t checked_request2size(size_t bytes) {
return (((bytes) + (sizeof(size_t)) +
((2 * (sizeof(size_t)) < __alignof__(long double)
? __alignof__(long double)
: 2 * (sizeof(size_t))) -
1) <
(unsigned long)((
((__builtin_offsetof(struct malloc_chunk, fd_nextsize)) +
((2 * (sizeof(size_t)) < __alignof__(long double)
? __alignof__(long double)
: 2 * (sizeof(size_t))) -
1)) &
~((2 * (sizeof(size_t)) < __alignof__(long double)
? __alignof__(long double)
: 2 * (sizeof(size_t))) -
1))))
? (unsigned long)((
((__builtin_offsetof(struct malloc_chunk, fd_nextsize)) +
((2 * (sizeof(size_t)) < __alignof__(long double)
? __alignof__(long double)
: 2 * (sizeof(size_t))) -
1)) &
~((2 * (sizeof(size_t)) < __alignof__(long double)
? __alignof__(long double)
: 2 * (sizeof(size_t))) -
1)))
: ((bytes) + (sizeof(size_t)) +
((2 * (sizeof(size_t)) < __alignof__(long double)
? __alignof__(long double)
: 2 * (sizeof(size_t))) -
1)) &
~((2 * (sizeof(size_t)) < __alignof__(long double)
? __alignof__(long double)
: 2 * (sizeof(size_t))) -
1));
}
int main(void){
for(size_t bytes=0;bytes!=4096;bytes++){
size_t tbytes = checked_request2size (bytes);
size_t tc_idx = csize2tidx(tbytes);
printf("bytes = %ld,tbytes = %ld, tc_idx = %ld\n",bytes,tbytes,tc_idx);
}
}
这个程序用于计算 bytes 从 0-4096 的 checked_request2size & csize2tidx 的返回值。
bytes = 1,tbytes = 32, tc_idx = 0
bytes = 25,tbytes = 48, tc_idx = 1
bytes = 40,tbytes = 48, tc_idx = 1
bytes = 41,tbytes = 64, tc_idx = 2
bytes = 56,tbytes = 64, tc_idx = 2
bytes = 57,tbytes = 80, tc_idx = 3
bytes = 71,tbytes = 80, tc_idx = 3
bytes = 72,tbytes = 80, tc_idx = 3
bytes = 73,tbytes = 96, tc_idx = 4
bytes = 75,tbytes = 96, tc_idx = 4
....
bytes = 535,tbytes = 544, tc_idx = 32
....
当 bytes = 535 时,tc_idx = 32:
进到 malloc/malloc.c:3304
对 tc_idx 和 tcache 进行检查:
► 3304 if (tc_idx < mp_.tcache_bins // mp_.tcache_bins = 64
3305 && tcache != NULL
3306 && tcache->counts[tc_idx] > 0)
...
...
3314 if (SINGLE_THREAD_P)
3315 {
3316 victim = tag_new_usable (_int_malloc (&main_arena, bytes));
3317 assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
3318 &main_arena == arena_for_chunk (mem2chunk (victim)));
3319 return victim;
3320 }
在 glibc 中,tcache_bins 是用于缓存小型内存块的线程本地缓存(Thread Local Cache,简称 tcache)的大小。在 glibc 2.33 及之后的版本中,tcache_bins 默认为 64,即每个线程的 tcache 最多可以缓存 64 种不同大小的内存块。
因为没有任何 free 操作,所以 tcache 是空的,tcache != NULL
检查失败,自然不会到 TCACHE 中取出 chunk,但在真实的世界中,tcache 的命中率非常高。
继续跳转到 3314 行执行,进入 _int_malloc (&main_arena, bytes)
中去。
它在 glibc 的 malloc 实现中用于处理大于 128KB 的内存分配请求,这个函数接收两个参数:&main_arena
是一个指向主内存区域(main arena)的指针,bytes
是要分配的内存块的字节数。
_int_malloc 分析
根据用户请求的大小,在堆上找到合适大小的可用内存块,并返回指向该内存块的指针。
// glibc-2.38/malloc/malloc.c
3832 static void *
3833 _int_malloc (mstate av, size_t bytes)
传入的参数:
av = 0xffffbb2c0a50 <main_arena>
bytes = 535
这里又出现了 nb = checked_request2size (bytes);
计算得到 nb = 544
。
glibc-2.38/malloc/malloc.c:3902
检查 nb 是否超过了 get_max_fast ()
返回的值 :
// get_max_fast 计算 fastbin chunk 的最大尺寸。
3902 if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ())){
....
}
对应的汇编代码:
► 0xffffbb159ce0 <_int_malloc+56> cmp x21, x0
pwndbg> p $x21 // nb == 544
$110 = 544
pwndbg> p $x0 // get_max_fast() == 128
$111 = 128
在 aarch64 架构,glibc 2.38 中 get_max_fast
返回的最大值为 128。
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
判断不成立。
继续走到 3965 if (in_smallbin_range (nb))
:
3965 if (in_smallbin_range (nb))
3966 {
3967 idx = smallbin_index (nb);
3968 bin = bin_at (av, idx);
3969
...
3970 if ((victim = last (bin)) != bin)
3971 {
汇编代码:
► 0xffffbb159ce8 <_int_malloc+64> cmp x21, #0x3ff
所以当 bytes > 1023 的时候,就会进入到 Largebin 中取出 chunk 吗? 也就是说 nb 小于 0x3ff(1023),尝试进入 smallbin 中取出 chunk,计算出 idx=34,bin_at 计算出对应的 smallbin 链表为
0xffffbb2c0cc0
。
last(bin) 是一个宏:
((bin)->bk)
但由于没有任何 free 操作,smallbins 此时是空的,显然 bin->bk == bin
,3970 行的判断语句会失败。继续向下走:
4055 for (;; )
4056 {
4057 int iters = 0;
► 4058 while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
中的 unsorted_chunks() 是一个宏:
macro unsorted_chunks
#define unsorted_chunks(M) (bin_at (M, 1))
// Expands to
((mbinptr) (((char *) &((av)->bins[((1) - 1) * 2])) \
- __builtin_offsetof (struct malloc_chunk, fd)))
根据这段宏的意思打印 fd 和 bk 的值
pwndbg> p /x *((av)->bins[((1) - 1) * 2])
$125 = {
mchunk_prev_size = 0xaaaae50376a0,
mchunk_size = 0x0,
fd = 0xffffbb2c0ab0,
bk = 0xffffbb2c0ab0,
fd_nextsize = 0xffffbb2c0ac0,
bk_nextsize = 0xffffbb2c0ac0
}
这里也可以看出 unsorted bin 实际上是 main_arean 中 最开的第一个 bin 链表。
fd == bk,unsorted bins 为空,所以不会进到 while 循环里执行。
最好走到 4318 行:
► 4318 ++idx;
4319 bin = bin_at (av, idx);
4320 block = idx2block (idx);
4321 map = av->binmap[block];
4322 bit = idx2bit (idx);
4323
此时 ++idx = 35,而 bin=bin_at (av, 35);
pwndbg> p *(av->bins[((35) -1) * 2])
$151 = {
mchunk_prev_size = 281473821969600,
mchunk_size = 281473821969600,
fd = 0xffffbb2c0cd0 <main_arena+640>, <---- !!!!
bk = 0xffffbb2c0cd0 <main_arena+640>,
fd_nextsize = 0xffffbb2c0ce0 <main_arena+656>,
bk_nextsize = 0xffffbb2c0ce0 <main_arena+656>
}
pwndbg> p bin
$152 = (mbinptr) 0xffffbb2c0cd0 <main_arena+640>
根据 &main_arean->binmap 去 largbins 中取出 chunks:
4327 if (bit > map || bit == 0)
4328 {
4329 do
4330 {
► 4331 if (++block >= BINMAPSIZE) /* out of bins */
4332 goto use_top;
4333 }
4334 while ((map = av->binmap[block]) == 0);
4335
4336 bin = bin_at (av, (block << BINMAPSHIFT));
4337 bit = 1;
4338 }
这里会循环 4 次,因为 av.binmap 为0,同样 largbins 是空的:
pwndbg> p av.binmap
$157 = {0, 0, 0, 0}
binmap一共 128 bits,16字节,4个int大小,binmap 按 int 分成 4 个 block,每 个block有 32 个 bit。
紧接着走到了这个循环里:
victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)
判断 unsorted_chunks 的第一个元素地址是否等于最后一个元素的地址,换句话说就是判断 unsorted_chunks 是否为空,当没有 free 操作时,unsorted_chunks 显然是空的,所以这里的代码块也不会执行。
最好走到了这里:
注释:
idx2block 是一个宏:
- ++idx == 58
- bin = 0xffff85eb0e40
- 经过 idx2block 位移操作后得到 block=1。
- map = av->binmap[block]; 得到 map = 0。 binmap 在 main_arean 中:
- bit = idx2bit (58),计算得到 bit == 67108864
#define idx2bit(i) ((1U << ((i) & ((1U << BINMAPSHIFT) - 1))))
((1U << ((idx) & ((1U << 5) - 1))))
在没有任何 free 操作下,就根本没有 free chunk,所以并不会进入 4336 行执行:
这里涉及到一个概念就是 binmap:
如果该large bin中最大的chunk的size小于用户请求的size的话,那么就依次查看后续的large bin中是否有满足需求的chunk,不过需要注意的是鉴于 bin 的个数较多(不同bin中的chunk极有可能在不同的内存页中),如果按照上一段中介绍的方法进行遍历的话(即遍历每个bin中的chunk),就可能会发生多次内存页中断操作,进而严重影响检索速度,所以glibc malloc设计了 Binmap 结构体来帮助提高bin-by-bin检索的速度。Binmap记录了各个bin中是否为空,通过bitmap可以避免检索一些空的bin。如果通过binmap找到了下一个非空的large bin的话,就按照上一段中的方法分配chunk,否则就使用top chunk来分配合适的内存:
► 4431 victim = av->top;
4432 size = chunksize (victim);
4433
4434 if (__glibc_unlikely (size > av->system_mem))
4435 malloc_printerr ("malloc(): corrupted top size");
4436
这段代码的大概意思就是向上移动 av.top,并使用 set_heap 来更新 mchunk_size 的值,最后返回 victim 指针。
当 size = chunksize (victim);
得到 size=0 的时候,也就是 top 的 size =0时,就需要使用系统调用 brk 申请新的内存空间,这就转入到 sysmalloc() 中运行:
pwndbg> bt
#0 _int_malloc (av=av@entry=0xf82cf43a0a50 <main_arena>, bytes=bytes@entry=640) at malloc.c:4469
#1 0x0000f82cf423aba8 in tcache_init () at malloc.c:3240
#2 0x0000f82cf423b604 in __GI___libc_malloc (bytes=bytes@entry=1024) at malloc.c:3301
#3 0x0000f82cf421648c in __GI__IO_file_doallocate (fp=0xf82cf43a1518 <_IO_2_1_stdout_>) at filedoalloc.c:101
#4 0x0000f82cf4225728 in __GI__IO_doallocbuf (fp=fp@entry=0xf82cf43a1518 <_IO_2_1_stdout_>) at /data/local/tmp/gentoo/home/glibc-2.38/libio/libioP.h:1030
#5 0x0000f82cf42239e8 in _IO_new_file_overflow (f=0xf82cf43a1518 <_IO_2_1_stdout_>, ch=-1) at fileops.c:745
#6 0x0000f82cf4224664 in _IO_new_file_xsputn (f=0xf82cf43a1518 <_IO_2_1_stdout_>, data=<optimized out>, n=8) at /data/local/tmp/gentoo/home/glibc-2.38/libio/libioP.h:1030
#7 0x0000f82cf41fbcb4 in __printf_buffer_flush_to_file (buf=buf@entry=0xffffd9018b90) at ../libio/libioP.h:1030
#8 0x0000f82cf41fbd9c in __printf_buffer_to_file_done (buf=buf@entry=0xffffd9018b90) at printf_buffer_to_file.c:120
#9 0x0000f82cf4204324 in __vfprintf_internal (s=<optimized out>, format=0xb63aab9a0cb8 "Loop %d:\n", ap=..., mode_flags=mode_flags@entry=0) at vfprintf-internal.c:1524
#10 0x0000f82cf41fb4b8 in __printf (format=<optimized out>) at printf.c:33
#11 0x0000b63aab9a0b34 in main () at malloc_test.c:14
#12 0x0000f82cf41d6f78 in __libc_start_call_main (main=main@entry=0xb63aab9a0ad4 <main>, argc=argc@entry=1, argv=argv@entry=0xffffd9019208) at ../sysdeps/nptl/libc_start_call_main.h:58
#13 0x0000f82cf41d7040 in __libc_start_main_impl (main=0xb63aab9a0ad4 <main>, argc=1, argv=0xffffd9019208, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=<optimized out>) at ../csu/libc-start.c:360
#14 0x0000b63aab9a09f0 in _start ()
这里一个 backtrace 显示当第一次调用 printf 的时候,会初始化 tcache,然后开始申请内存池空间,此时 av.top 为 0,调用 sysmalloc 申请内存。
经过几轮 Free 操作后的 __libc_malloc(430)
3299 size_t tc_idx = csize2tidx (tbytes); // tc_idx = 26
// mp_.tcache_bins == 64
3304 if (tc_idx < mp_.tcache_bins
3305 && tcache != NULL
3306 && tcache->counts[tc_idx] > 0)
3307 {
3308 victim = tcache_get (tc_idx);
3309 return tag_new_usable (victim);
3310 }
此时 csize2tidx (430)
计算得到 tc_idx == 26
。
mp_
结构体是 malloc_par
。在 glibc 2.33 及之后的版本中,mp_.tcache_bins 的大小通常由环境变量 TCACHE_MAX_BINS 控制,默认情况下为 64。这意味着每个线程的 tcache 可以缓存 64 种不同大小的内存块。malloc_par 结构体在这里被赋值:
// malloc/malloc.c
1909 static struct malloc_par mp_ =
1910 {
1911 .top_pad = DEFAULT_TOP_PAD,
1912 .n_mmaps_max = DEFAULT_MMAP_MAX,
1913 .mmap_threshold = DEFAULT_MMAP_THRESHOLD,
1914 .trim_threshold = DEFAULT_TRIM_THRESHOLD,
1915 #define NARENAS_FROM_NCORES(n) ((n) * (sizeof (long) == 4 ? 2 : 8))
1916 .arena_test = NARENAS_FROM_NCORES (1)
1917 #if USE_TCACHE
1918 ,
1919 .tcache_count = TCACHE_FILL_COUNT,
1920 .tcache_bins = TCACHE_MAX_BINS, // 64
1921 .tcache_max_bytes = tidx2usize (TCACHE_MAX_BINS-1),
1922 .tcache_unsorted_limit = 0 /* No limit. */ 1923 #endif
1924 };
此时 tcache != NULL
,tcache->counts[tc_idx] > 0
都满足,直接进入 TCACHE 中取出chunks:
► 3308 victim = tcache_get (tc_idx); // victim == 0xaaaae50461b0
3309 return tag_new_usable (victim);
tcache_get
3161 static __always_inline void *
3162 tcache_get_n (size_t tc_idx, tcache_entry **ep)
3163 {
3164 tcache_entry *e;
3165 if (ep == &(tcache->entries[tc_idx]))
3166 e = *ep;
3167 else
3168 e = REVEAL_PTR (*ep);
3169
3170 if (__glibc_unlikely (!aligned_OK (e)))
3171 malloc_printerr ("malloc(): unaligned tcache chunk detected");
3172
3173 if (ep == &(tcache->entries[tc_idx]))
3174 *ep = REVEAL_PTR (e->next);
3175 else
3176 *ep = PROTECT_PTR (ep, REVEAL_PTR (e->next));
3177
3178 --(tcache->counts[tc_idx]);
3179 e->key = 0;
3180 return (void *) e;
3181 }
ptmalloc_init ()
ptmalloc_init ()
可能在任何需要的时候被调用,这个 backtrace 显示了 __printf
调用了 ptmalloc_init ()
:
pwndbg> bt
#0 tcache_init () at malloc.c:3236
#1 0x0000e5501c9eb604 in __GI___libc_malloc (bytes=bytes@entry=1024) at malloc.c:3301
#2 0x0000e5501c9c648c in __GI__IO_file_doallocate (fp=0xe5501cb51518 <_IO_2_1_stdout_>) at filedoalloc.c:101
#3 0x0000e5501c9d5728 in __GI__IO_doallocbuf (fp=fp@entry=0xe5501cb51518 <_IO_2_1_stdout_>) at /data/local/tmp/gentoo/home/glibc-2.38/libio/libioP.h:1030
#4 0x0000e5501c9d39e8 in _IO_new_file_overflow (f=0xe5501cb51518 <_IO_2_1_stdout_>, ch=-1) at fileops.c:745
#5 0x0000e5501c9d4664 in _IO_new_file_xsputn (f=0xe5501cb51518 <_IO_2_1_stdout_>, data=<optimized out>, n=8)
at /data/local/tmp/gentoo/home/glibc-2.38/libio/libioP.h:1030
#6 0x0000e5501c9abcb4 in __printf_buffer_flush_to_file (buf=buf@entry=0xfffffb6f4490) at ../libio/libioP.h:1030
#7 0x0000e5501c9abd9c in __printf_buffer_to_file_done (buf=buf@entry=0xfffffb6f4490) at printf_buffer_to_file.c:120
#8 0x0000e5501c9b4324 in __vfprintf_internal (s=<optimized out>, format=0xc3ced19f0cb8 "Loop %d:\n", ap=..., mode_flags=mode_flags@entry=0)
at vfprintf-internal.c:1524
#9 0x0000e5501c9ab4b8 in __printf (format=<optimized out>) at printf.c:33
#10 0x0000c3ced19f0b34 in main () at malloc_test.c:14
#11 0x0000e5501c986f78 in __libc_start_call_main (main=main@entry=0xc3ced19f0ad4 <main>, argc=argc@entry=1, argv=argv@entry=0xfffffb6f4b08)
at ../sysdeps/nptl/libc_start_call_main.h:58
#12 0x0000e5501c987040 in __libc_start_main_impl (main=0xc3ced19f0ad4 <main>, argc=1, argv=0xfffffb6f4b08, init=<optimized out>, fini=<optimized out>,
rtld_fini=<optimized out>, stack_end=<optimized out>) at ../csu/libc-start.c:360
#13 0x0000c3ced19f09f0 in _start ()
tcache_key_initialize
使用 getrandom 系统调用填充 tcache_key。
// glibc-2.38/malloc/malloc.c
3132 __getrandom_nocancel (&tcache_key, sizeof(tcache_key), GRND_NONBLOCK)
// /glibc-2.38/sysdeps/unix/sysv/linux/not-cancel.h
86 static inline ssize_t
87 __getrandom_nocancel (void *buf, size_t buflen, unsigned int flags)
89 return INTERNAL_SYSCALL_CALL (getrandom, buf, buflen, flags);
90 }
INTERNAL_SYSCALL_CALL 是一个宏,陷入 getrandom
系统调用:
1 macro INTERNAL_SYSCALL_CALL 2 provided by <sysdep.h>
3
4 #define INTERNAL_SYSCALL_CALL(...) \
5 __INTERNAL_SYSCALL_DISP (__INTERNAL_SYSCALL, __VA_ARGS__)
6
7 // Expands to
8 ({
9 long _sys_result;
10 {
11 long _x2tmp = (long) (flags);
12 long _x1tmp = (long) (buflen);
13 long _x0tmp = (long) (buf);
14 register long _x0 asm ("x0");
15 _x0 = _x0tmp;
16 register long _x1 asm ("x1") = _x1tmp;
17 register long _x2 asm ("x2") = _x2tmp;
18 register long _x8 asm ("x8") = ((278));
19 asm volatile ("svc 0 // syscall "
20 "SYS_ify(getrandom)"
21 : "=r"(_x0)
22 : "r"(_x8), "r"(_x0), "r"(_x1), "r"(_x2)
23 : "memory");
24 _sys_result = _x0;
25 }
26 _sys_result;
27 })
thread_arena = &main_arena;
main_arena 结构体此时是控的:
pwndbg> p main_arena
$2 = {
mutex = 0,
flags = 0,
have_fastchunks = 0,
fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
top = 0x0,
last_remainder = 0x0,
bins = {0x0 <repeats 254 times>},
binmap = {0, 0, 0, 0},
next = 0xf1bebe7b0a50 <main_arena>,
next_free = 0x0,
attached_threads = 1,
system_mem = 0,
max_system_mem = 0
}
malloc_init_state
用于初始化堆状态。
for (i = 1; i < NBINS; ++i)
{
bin = bin_at (av, i);
bin->fd = bin->bk = bin;
}
将 bins 链表首尾相连。
在 glibc 中,bin_at 函数用于计算给定索引位置的 bin(即线程本地缓存中的缓存链表)的地址。但是,这个地址指向的并不是用户数据本身,而是指向缓存链表头部的指针。详见 常见的宏分析 章节
TUNABLE_GET
这是一组可调节参数,你可以设置 TUNABLE 环境变量来改变 glibc 的运行时参数如:
GLIBC_TUNABLES=glibc.malloc.trim_threshold=128:glibc.malloc.check=3
export GLIBC_TUNABLES
使用 ld-linux-aarch64.so.2 --list-tunables
列举所有可调参数和默认值:
ihexon@virto ~> /lib/ld-linux-aarch64.so.1 --list-tunables
glibc.rtld.nns: 0x4 (min: 0x1, max: 0x10)
glibc.elision.skip_lock_after_retries: 3 (min: 0, max: 2147483647)
glibc.malloc.trim_threshold: 0x0 (min: 0x0, max: 0xffffffffffffffff)
glibc.malloc.perturb: 0 (min: 0, max: 255)
glibc.pthread.rseq: 1 (min: 0, max: 1)
glibc.cpu.name:
glibc.mem.tagging: 0 (min: 0, max: 255)
glibc.elision.tries: 3 (min: 0, max: 2147483647)
glibc.elision.enable: 0 (min: 0, max: 1)
glibc.malloc.hugetlb: 0x0 (min: 0x0, max: 0xffffffffffffffff)
glibc.malloc.mxfast: 0x0 (min: 0x0, max: 0xffffffffffffffff)
glibc.rtld.dynamic_sort: 2 (min: 1, max: 2)
....
tcache_init
一个完整的 tcache_init 被触发的情况:
pwndbg> bt
#0 tcache_init () at malloc.c:3236
#1 0x0000e0306be9b604 in __GI___libc_malloc (bytes=bytes@entry=1024) at malloc.c:3301
#2 0x0000e0306be7648c in __GI__IO_file_doallocate (fp=0xe0306c001518 <_IO_2_1_stdout_>) at filedoalloc.c:101
#3 0x0000e0306be85728 in __GI__IO_doallocbuf (fp=fp@entry=0xe0306c001518 <_IO_2_1_stdout_>) at /data/local/tmp/gentoo/home/glibc-2.38/libio/libioP.h:1030
#4 0x0000e0306be839e8 in _IO_new_file_overflow (f=0xe0306c001518 <_IO_2_1_stdout_>, ch=-1) at fileops.c:745
#5 0x0000e0306be84664 in _IO_new_file_xsputn (f=0xe0306c001518 <_IO_2_1_stdout_>, data=<optimized out>, n=8) at /data/local/tmp/gentoo/home/glibc-2.38/libio/libioP.h:1030
#6 0x0000e0306be5bcb4 in __printf_buffer_flush_to_file (buf=buf@entry=0xffffd089eab0) at ../libio/libioP.h:1030
#7 0x0000e0306be5bd9c in __printf_buffer_to_file_done (buf=buf@entry=0xffffd089eab0) at printf_buffer_to_file.c:120
#8 0x0000e0306be64324 in __vfprintf_internal (s=<optimized out>, format=0xb3a11f9f0cb8 "Loop %d:\n", ap=..., mode_flags=mode_flags@entry=0) at vfprintf-internal.c:1524
#9 0x0000e0306be5b4b8 in __printf (format=<optimized out>) at printf.c:33
#10 0x0000b3a11f9f0b34 in main () at malloc_test.c:14
#11 0x0000e0306be36f78 in __libc_start_call_main (main=main@entry=0xb3a11f9f0ad4 <main>, argc=argc@entry=1, argv=argv@entry=0xffffd089f128)
at ../sysdeps/nptl/libc_start_call_main.h:58
#12 0x0000e0306be37040 in __libc_start_main_impl (main=0xb3a11f9f0ad4 <main>, argc=1, argv=0xffffd089f128, init=<optimized out>, fini=<optimized out>,
rtld_fini=<optimized out>, stack_end=<optimized out>) at ../csu/libc-start.c:360
#13 0x0000b3a11f9f09f0 in _start ()
尝试使用 arena_get (ar_ptr, bytes);
获取一个 arena 但显然 ar_ptr 是 null,最终会通过 arena_get2 创建一个新的 arena 作为 main_arena。
第一次 tcache_init 时,会进入 _int_malloc
创建大小为 640 的arena 区域,这块区域最终会由 sysmalloc (nb, av);
来申请
ar_ptr = arena_get_retry (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes); // 跳进 sysmalloc
4468 {
► 4469 void *p = sysmalloc (nb, av);
4470 if (p != NULL)
4471 alloc_perturb (p, bytes);
4472 return p;
从现在开始,vmmap显示进程的内存空间会多出一块 Heap 区域:
$ vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0xb3a11f9f0000 0xb3a11f9f1000 r-xp 1000 0 /home/ihexon/cpp/malloc_test
0xb3a11fa0f000 0xb3a11fa10000 r--p 1000 f000 /home/ihexon/cpp/malloc_test
0xb3a11fa10000 0xb3a11fa11000 rw-p 1000 10000 /home/ihexon/cpp/malloc_test
0xb3a13d699000 0xb3a13d6ba000 rw-p 21000 0 [heap]
_int_malloc 根据 bytes 返回一个 victim 指针,这就是 tcache 所在的起始位置:
tcache = (tcache_perthread_struct *) victim;
memset (tcache, 0, sizeof (tcache_perthread_struct));
sysmalloc 分析
sysmalloc 使用系统调用 __sbrk
来分配内存,常见的当 main_arena.top < MINSIZE 或者 main_arena == NULL 的时候(用人话来说就是 glibc 初始化 heap 的时候),glibc 会使用 sysmalloc 来申请更多的内存。
当请求的大小到达 mmap 的阀值,glibc 尝试使用 mmap 分配:
2546 /*
2547 If have mmap, and the request size meets the mmap threshold, and
2548 the system supports mmap, and there are few enough currently
2549 allocated mmapped regions, try to directly map this request
2550 rather than expanding top.
2551 */
2552
2553 if (av == NULL
2554 || ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
2555 && (mp_.n_mmaps < mp_.n_mmaps_max)))
在 aarch64 glibc 2.38 中:
mp_.n_mmaps_max
的值为 65536 (0x10000)。mp_.mmap_threshold
的值为 131072 (0x20000)。
old_top、old_size和old_end分别保存了top chunk的指针,大小以及尾部的地址:
2523 mchunk // In sysmalloc /* incoming value of av->top */
2524 INTERN char *old_end ; /* its size */
2525 char *old_end; /* its end address */
► 2578 old_top = av->top;
2579 old_size = chunksize (old_top);
2580 old_end = (char *) (chunk_at_offset (old_top, old_size));
将 brk 和 snd_brk 指针悬空:
brk = snd_brk = (char *) (MORECORE_FAILURE);
2659 size = nb + mp_.top_pad + MINSIZE;
// size= 135168
brk = (char *) (MORECORE (size));
// brk = 0xb7dc0c694000
brk 是起始地址
MORECORE 宏:
MORECORE 在编译时会被替换为 __glibc_morecore
函数,使用 __sbrk
向操作系统申请大小为 increment 的内存。
// malloc/malloc.c
368 #include "morecore.c"
370 #define MORECORE (*__glibc_morecore)
// malloc/morecore.c
23 void *
>> 24 __glibc_morecore (ptrdiff_t increment)
25 {
26 if (__always_fail_morecore)
>> 27 return NULL;
28
>> 29 void *result = (void *) __sbrk (increment);
30 if (result == (void *) -1)
>> 31 return NULL;
32
33 return result;
34 }
► 2734 if (mp_.sbrk_base == 0)
2735 mp_.sbrk_base = brk;
2736 av->system_mem += size;
mp_.sbrk_base 是一个全局变量,用于存储堆(heap)的起始地址。 av->system_mem 记录了该 arena 实例当前映射的所有可写内存的总大小。此时
av.system_mem = 135168
mp_.sbrk_base = 0xb7dc0c694000
end_misalign = (INTERNAL_SIZE_T) (brk + size + correction);
// end_misalign = 0xb7dc0c6b5000
2769 { size_t front_misalign
2770 front_misalign = 0;
2771 end_misalign = 0;
2772 correction = 0;
2773 aligned_brk = brk;
snd_brk= 0xb7dc0c6b5000
aligned_brk = 0xb7dc0c694000
2865 av->top = (mchunkptr) aligned_brk;
2866 set_head (av->top, (snd_brk - aligned_brk + correction) | PREV_INUSE);
2867 av->system_mem += correction;
2915 /* finally, do the allocation */
2916 p = av->top;
2917 size = chunksize (p);
2919 /* check that one of the above allocation paths succeeded */
2920 if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
2921 {
2922 remainder_size = size - nb;
2923 remainder = chunk_at_offset (p, nb);
2924 av->top = remainder;
2925 set_head (p, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0));
2926 set_head (remainder, remainder_size | PREV_INUSE);
2927 check_malloced_chunk (av, p, nb);
► 2928 return chunk2mem (p);
2929 }
free() 分析
3346 __libc_free (void *mem)
3361 p = mem2chunk (mem); // 将 mem 地址转换chunk 开始的地址
mem2chunk 展开后: ((mchunkptr)tag_at (((char*)(p) - (2 * (sizeof (size_t))))))
一个 chunk 结构体如下:
$17 = {
mchunk_prev_size = 0,
mchunk_size = 321,
fd = 0x9f4e676ebcb24fd6,
bk = 0x9491939aa2edf5dc,
fd_nextsize = 0x3619a269d7022118,
bk_nextsize = 0xc6be861b52824684
}
也就是指针下移2个 size_t 长度,所以 mem 指向的是 p.fd
,向下移懂 2 个 size_t 就指向了 mchunk_prev_size。
3385 ar_ptr = arena_for_chunk (p);
通过 p 计算得到为 main_arena 的地址 并赋值给 ar_ptr。
3386 _int_free (ar_ptr, p, 0);
进入正式的 free 步骤:
4493 size = chunksize (p); // 计算 这个 chunk 的大小
// 展开为 (((p)->mchunk_size) & ~((0x1 | 0x2 | 0x4)))
通常情况下,chunks 都会被链接进 tcache 中,这也说明了 tcache 优先级非常高。
4512 if (tcache != NULL && tc_idx < mp_.tcache_bins)
► 4515 tcache_entry *e = (tcache_entry *) chunk2mem (p);
归还到 tcache 中:
► 4541 if (tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (p, tc_idx);
return
}
tcache 结构:
tcache is pointing to: 0xba6ff66a2010 for thread 1
{
counts = {0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 2, 0, 0, 1, 0, 0, 1, 0 <repeats 12 times>, 1, 0, 1, 0, 0},
entries = {0x0, 0xba6ff66cc990, 0x0, 0x0, 0xba6ff66bdde0, 0xba6ff66a4c00, 0xba6ff66a6ff0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xba6ff66c88c0, 0x0, 0x0, 0xba6ff66a26b0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xba6ff66bc5a0, 0x0, 0x0, 0xba6ff66aede0, 0xba6ff66b64f0, 0x0, 0x0, 0x0, 0xba6ff66a29c0, 0xba6ff66c6920, 0xba6ff66c1b40, 0xba6ff66b6720, 0x0, 0x0, 0xba6ff66bdb10, 0x0, 0x0, 0xba6ff66b8570, 0x0 <repeats 12 times>, 0xba6ff66ba8b0, 0x0, 0xba6ff66a6c00, 0x0, 0x0}
}
tcache.counts = 64,而 tc_idx 经过计算为 13,处在 64 的范围内:
size_t tc_idx = csize2tidx (nb);
- mp_.tcache_count 是记录当前线程已经从 tcache 中成功分配的内存块总数的变量。它用于监控和控制当前线程从 tcache 中分配内存的情况,mp_.tcache_count 的最大值为 65535。
- do_set_tcache_count 函数用于设置线程的 tcache 的容量限制。通过调用这个函数,可以设置当前线程的 tcache 的容量限制,从而控制当前线程能够缓存的小内存分配的数量。
```c
5612 static _always_inline int
5613 do_set_tcache_count (size_t value)
5614 {
5615 if (value <= MAX_TCACHE_COUNT)
5616 {
5617 LIBC_PROBE (memory_tunable_tcache_count, 2, value, mp.tcache_count); 5618 mp_.tcache_count = value; 5619 return 1; 5620 } 5621 return 0; 5622 }
## tcache_put (p, tc_idx);
```c
3144 static __always_inline void
3145 tcache_put (mchunkptr chunk, size_t tc_idx)
3146 {
3147 tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
3148
3149 /* Mark this chunk as "in the tcache" so the test in _int_free will
3150 detect a double free. */
3151 e->key = tcache_key;
3152
3153 e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
3154 tcache->entries[tc_idx] = e;
3155 ++(tcache->counts[tc_idx]);
3156 }
tcache_entry *e = (tcache_entry *) chunk2mem (chunk); 从 chunk 得到 tcache_entry,tcache_entry 是每个 tcache.counts[N] 链表的入口地址,还是注意这个结构:
tcache is pointing to: 0xba6ff66a2010 for thread 1
{
counts = {0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 2, 0, 0, 1, 0, 0, 1, 0 <repeats 12 times>, 1, 0, 1, 0, 0},
entries = {0x0, 0xba6ff66cc990, 0x0, 0x0, 0xba6ff66bdde0, 0xba6ff66a4c00, 0xba6ff66a6ff0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xba6ff66c88c0, 0x0, 0x0, 0xba6ff66a26b0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xba6ff66bc5a0, 0x0, 0x0, 0xba6ff66aede0, 0xba6ff66b64f0, 0x0, 0x0, 0x0, 0xba6ff66a29c0, 0xba6ff66c6920, 0xba6ff66c1b40, 0xba6ff66b6720, 0x0, 0x0, 0xba6ff66bdb10, 0x0, 0x0, 0xba6ff66b8570, 0x0 <repeats 12 times>, 0xba6ff66ba8b0, 0x0, 0xba6ff66a6c00, 0x0, 0x0}
}
这里得到的 tcache_entrie = 0xba6ff66ad7a0
PROTECT_PTR: PROTECT_PTR() is going to protect this chunk’s fd ptr…
e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
- 将 chunk.next 指向 tcache->entries[tc_idx],比如 tc_idx=17,那么 chunk.next 就指向了 tcache->entries[17] 的开始地址。
- 将 tcache->entries[17] 的开始地址更新为 chunk 的开始地址,完成 chunk 的插入到头部的操作
- 对 tcache.counts[tc_idx] 计算器自增,代表此时 tcache->entries[17] 添加进了一块 chunk。
常用的宏分析
bin_at
#define bin_at(m, i)
(mbinptr) (((char *) &((m)->bins[((i) -1) * 2]))
- offsetof (struct malloc_chunk, fd))