内核堆概述

前述

linux kernel对堆的内存管理器有两种,buddy system和slab allocater,前者以内存页为操作单位,后者则是将内存页划分为多个较小的对象进行粒度颗粒管理

linux kernel将内存分为页-区-节点三级结构,下面是一张十分经典的 Overview ,自顶向下是

  • 节点(node,对应结构体 pgdata_list)
  • 区(zone,对应结构体 zone,图上展示了三种类型的 zone)
  • 页(page,对应结构体 page)

image.png

image.png

将节点的不同用途的内存区域划分为不同的zone

image.png

节点

一个MC对应一个节点;对于同一个内存控制器下的CPU而言其对应的节点称为本地内存image.png

Buddy system

以内存页为粒度管理所有的物理内存,其存在于区这一级别

每个 zone 结构体中都有一个 free_area 结构体数组,用以存储 buddy system 按照 order 管理的页面:

1
2
3
4
struct zone {
//...
struct free_area free_area[MAX_ORDER=11];
//...

order指的是连续的空闲页面的大小 2^order (参照区的那张图片)

分配:

  • 首先会将请求的内存大小向 2 的幂次方张内存页大小对齐,之后从对应的下标取出连续内存页。
  • 若对应下标链表为空,则会从下一个 order 中取出内存页,一分为二,装载到当前下标对应链表中,之后再返还给上层调用,若下一个 order 也为空则会继续向更高的 order 进行该请求过程。

释放:

  • 将对应的连续内存页释放到对应的链表上。
  • 检索是否有可以合并的内存页,若有,则进行合成,放入更高 order 的链表中。

Slub allocator

只涉及目前通用版本slub

buddy system 请求单张或多张连续内存页(slub)后再分割成同等大小的对象用以更小粒度调用

slub结构图:

image.png

page 结构体与物理内存间存在线性对应关系,我们可以直接通过 object 地址找到其对应的 page 结构体。

一个 kmem_cache 主要由两个模块组成:

  1. kemem_cache_cpu:一个percpu变量(每一个cpu都有一个独立的副本),用以表示当前正在使用的slub,因此当前 CPU 在从 kmem_cache_cpu 上取 object 时不需要加锁
  2. kmem_cache_node:存放两个slub链表;partial:有一定数量的空闲object;full:该slub上object都分配出去了

分配:

  • 首先从 kmem_cache_cpu 上取对象,若有则直接返回。
  • kmem_cache_cpu 上的 slub 已经无空闲对象了,对应 slub 会被从 kmem_cache_cpu 上取下,并尝试从 partial 链表上取一个 slub 挂载到 kmem_cache_cpu 上,然后再取出空闲对象返回。
  • kmem_cache_node 的 partial 链表也空了,那就向 buddy system 请求分配新的内存页,划分为多个 object 之后再给到 kmem_cache_cpu,取空闲对象返回上层调用。

释放:

  • 若被释放 object 属于 kmem_cache_cpu 的 slub,直接使用头插法插入当前 CPU slub 的 freelist。
  • 若被释放 object 属于 kmem_cache_node 的 partial 链表上的 slub,直接使用头插法插入对应 slub 的 freelist。
  • 若被释放 object 为 full slub,则其会成为对应 slub 的 freelist 头节点,且该 slub 会被放置到 partial 链表

slub allocator

内核堆利用与绑核

成因:

slub allocator在申请分配时会优先从kmem_cache_cpu上分配,这也就导致所利用的object可能来自不同的kmem_cache_cpu,不好触发漏洞

解决:

将当前进程绑定到某个cpu核心上,从而辨别object

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sched.h>

/* to run the exp on the specific core only */
void bind_cpu(int core)
{
cpu_set_t cpu_set;//定义一个cpu数据集合

CPU_ZERO(&cpu_set);//清空cpu集合
CPU_SET(core, &cpu_set);//将core添加到集合中
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);//提高命中率

printf("\033[34m\033[1m[*] Process binded to core \033[0m%d\n", core);
}

通用kmalloc flag

kmalloc函数,size是分配的内核对象的大小,gfp_t flags是 Get-Free-Page flag ,相当于“怎么分配”

1
2
3
static __always_inline __alloc_size(1) void *kmalloc_noprof(size_t size, gfp_t flags)
//...
#define kmalloc(...) alloc_hooks(kmalloc_noprof(__VA_ARGS__))

不同bit所对应的flag:

  • GFP_KERNEL:0xCC0

  • GFP_KERNEL | __GFP_ZERO: 0xDC0

  • GFP_KERNEL_ACCOUNT:0x400CC0

  • GFP_KERNEL_ACCOUNT | __GFP_ZERO: 0x400DC0

    GFP_KERNEL_ACCOUNTGFP_KERNEL 多启用了一个 bit ___GFP_ACCOUNT_BIT ,表示 使用 MEMCG 机制进行数据记录 ,用于追踪,在内核启用了 CONFIG_MEMCG_KMEM=y 时生效 ,没有则二者相同; __GFP_ZERO字面意思,表示清除内存在申请之前

___GFP_KSWAPD_RECLAIM_BIT 启用时意味着在内存不足时唤醒 kswapd 进行内存回收。

kmalloc编译优化

kmalloc的size如果是固定的,就能知道从哪一个kmem_cache进行分配, 因此内核会将对 kmalloc() 的调用优化为对 kmem_cache_alloc_noprof() 的调用

1
2
3
void *kmem_cache_alloc_noprof(struct kmem_cache *cachep,
gfp_t flags) __assume_slab_alignment __malloc;
#define kmem_cache_alloc(...) alloc_hooks(kmem_cache_alloc_noprof(__VA_ARGS__))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#define INIT_KMALLOC_INFO(__size, __short_size)         \
{ \
.name[KMALLOC_NORMAL] = "kmalloc-" #__short_size, \
KMALLOC_RCL_NAME(__short_size) \
KMALLOC_CGROUP_NAME(__short_size) \
KMALLOC_DMA_NAME(__short_size) \
KMALLOC_RANDOM_NAME(RANDOM_KMALLOC_CACHES_NR, __short_size) \
.size = __size, \
}

/*
* kmalloc_info[] is to make slab_debug=,kmalloc-xx option work at boot time.
* kmalloc_index() supports up to 2^21=2MB, so the final entry of the table is
* kmalloc-2M.
*/
const struct kmalloc_info_struct kmalloc_info[] __initconst = {
INIT_KMALLOC_INFO(0, 0),
INIT_KMALLOC_INFO(96, 96),
INIT_KMALLOC_INFO(192, 192),
INIT_KMALLOC_INFO(8, 8),
INIT_KMALLOC_INFO(16, 16),
INIT_KMALLOC_INFO(32, 32),
INIT_KMALLOC_INFO(64, 64),
INIT_KMALLOC_INFO(128, 128),
INIT_KMALLOC_INFO(256, 256),
INIT_KMALLOC_INFO(512, 512),
INIT_KMALLOC_INFO(1024, 1k),
INIT_KMALLOC_INFO(2048, 2k),
INIT_KMALLOC_INFO(4096, 4k),
INIT_KMALLOC_INFO(8192, 8k),
INIT_KMALLOC_INFO(16384, 16k),
INIT_KMALLOC_INFO(32768, 32k),
INIT_KMALLOC_INFO(65536, 64k),
INIT_KMALLOC_INFO(131072, 128k),
INIT_KMALLOC_INFO(262144, 256k),
INIT_KMALLOC_INFO(524288, 512k),
INIT_KMALLOC_INFO(1048576, 1M),
INIT_KMALLOC_INFO(2097152, 2M)
};

enum kmalloc_cache_type {
KMALLOC_NORMAL = 0,
#ifndef CONFIG_ZONE_DMA
KMALLOC_DMA = KMALLOC_NORMAL,
#endif
#ifndef CONFIG_MEMCG
KMALLOC_CGROUP = KMALLOC_NORMAL,
#endif
KMALLOC_RANDOM_START = KMALLOC_NORMAL,
KMALLOC_RANDOM_END = KMALLOC_RANDOM_START + RANDOM_KMALLOC_CACHES_NR,
#ifdef CONFIG_SLUB_TINY
KMALLOC_RECLAIM = KMALLOC_NORMAL,
#else
KMALLOC_RECLAIM,
#endif
#ifdef CONFIG_ZONE_DMA
KMALLOC_DMA,
#endif
#ifdef CONFIG_MEMCG
KMALLOC_CGROUP,
#endif
NR_KMALLOC_TYPES
};

在内核源码中对 kmalloc_caches 的使用形式为 kmalloc_caches[type][index]但实际上的使用是一维指针数组 ,每个 type 占据一段连续的 index, 且大于 8k 的大小通常不启用 ,因此每个 type 一般会有 14kmem_cache ,根据这个规则我们便能计算 kmalloc_caches[N] 对应的大小,以下是两个例子:

  • kmalloc_caches[12]: 大小范围位于 KMALLOC_NORMAL 对应的 index 0~13 ,取 kmalloc_info[12 - 0] ,得到大小 4k,对应 kmem_cachekmalloc-4k,对应 flag 为 GFP_KERNEL
  • kmalloc_caches[54]: 大小范围位于 KMALLOC_CGROUP 对应的 index 42~55 ,取 kmalloc_info[54 - 42] ,得到大小 4k,对应 kmem_cachekmalloc-cg-4k,对应 flag 为 GFP_KERNEL_ACCOUNT

参考(由源码分析):

  • KMALLOC_NORMAL → 缓存名称为 "kmalloc-*" → 对应 GFP_KERNEL
  • KMALLOC_CGROUP → 缓存名称为 "kmalloc-cg-*" → 对应 GFP_KERNEL_ACCOUNT
  • KMALLOC_RECLAIM → 缓存名称为 "kmalloc-rcl-*" → 对应 GFP_KERNEL | __GFP_RECLAIM
  • KMALLOC_DMA → 缓存名称为 "kmalloc-dma-*" → 对应 GFP_KERNEL | __GFP_DMA

slub 合并 & 隔离

slab alias 机制是一种对同等 / 相近大小 object 的 kmem_cache 进行复用的一种机制:

  • 当一个 kmem_cache 在创建时,若已经存在能分配相等 / 近似大小的 object 的 kmem_cache ,则不会创建新的 kmem_cache,而是为原有的 kmem_cache 起一个 alias,作为 “新的” kmem_cache 返回
  • 对于初始化时设置了 SLAB_ACCOUNT 这一 flag 的 kmem_cache 而言,则会新建一个新的 kmem_cache 而非为原有的建立 alias