如何保护mmap()返回内存页中指定大小的随机内存块?
页内内存块防重复分配的可行方案
你遇到的问题是因为mprotect()仅支持页级别的内存保护,没法直接作用于页内的中间地址。针对你的需求——避免后续allocate()调用覆盖已分配块,这里有几个实用的解决思路:
方法一:用元数据链表管理空闲/已分配块
这是用户态内存分配器(比如标准malloc)的核心实现思路,完全不需要依赖系统级内存保护,纯用户态就能解决问题:
- 定义一个元数据结构体,用来记录每个内存块的状态:
typedef struct BlockMeta { size_t size; // 当前块的总大小(含元数据) int is_allocated; // 标记块是否已被分配 struct BlockMeta* next; // 链表指针,串联所有块 } BlockMeta;
- 初始化页时,把整个4KB页初始化为一个大的空闲块:
// 假设page已经通过mmap获取 BlockMeta* initial_block = (BlockMeta*)page; initial_block->size = PAGESIZE - sizeof(BlockMeta); initial_block->is_allocated = 0; initial_block->next = NULL;
- 在
allocate()函数里,遍历这个链表寻找符合大小的空闲块:
void* allocate(int size) { // 对齐size(可选,减少内存碎片) size = (size + sizeof(size_t) - 1) & ~(sizeof(size_t) - 1); BlockMeta* current = (BlockMeta*)page; while (current != NULL) { if (!current->is_allocated && current->size >= size) { // 如果块剩余空间足够拆分,生成新的空闲块 if (current->size - size > sizeof(BlockMeta)) { BlockMeta* split_block = (BlockMeta*)((char*)current + sizeof(BlockMeta) + size); split_block->size = current->size - size - sizeof(BlockMeta); split_block->is_allocated = 0; split_block->next = current->next; current->next = split_block; } // 标记当前块为已分配 current->is_allocated = 1; current->size = size; // 返回元数据后的内存地址给用户 return (char*)current + sizeof(BlockMeta); } current = current->next; } // 没有足够空闲空间,返回NULL return NULL; }
这种方式下,后续allocate()会自动跳过已标记为is_allocated的块,完全不会出现覆盖问题,同时还能高效管理内存碎片。
方法二:子页粒度映射(适合需要严格内存写保护的场景)
如果你的需求不仅是防止allocate()重复分配,还要防止用户程序越界写入已分配块,可以把4KB页拆分为多个更小的、独立映射的子页:
- 提前按固定粒度(比如256B、512B)拆分4KB页,每个子页单独用
mmap映射(用MAP_FIXED确保地址在原页范围内):
#define SUB_PAGE_SIZE 256 #define SUB_PAGE_COUNT (PAGESIZE / SUB_PAGE_SIZE) void* sub_pages[SUB_PAGE_COUNT]; int sub_page_allocated[SUB_PAGE_COUNT] = {0}; // 标记子页是否已分配 // 初始化子页映射 void init_sub_pages() { for (int i = 0; i < SUB_PAGE_COUNT; i++) { uintptr_t sub_page_addr = (uintptr_t)page + i * SUB_PAGE_SIZE; sub_pages[i] = mmap((void*)sub_page_addr, SUB_PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); } }
- 分配时,计算需要的子页数量,把对应子页标记为已分配,并可以用
mprotect()将未分配的子页设为PROT_NONE(防止误写):
void* allocate(int size) { int required_sub_pages = (size + SUB_PAGE_SIZE - 1) / SUB_PAGE_SIZE; // 寻找连续的空闲子页 int start_idx = -1; int count = 0; for (int i = 0; i < SUB_PAGE_COUNT; i++) { if (!sub_page_allocated[i]) { if (start_idx == -1) start_idx = i; count++; if (count == required_sub_pages) break; } else { start_idx = -1; count = 0; } } if (start_idx == -1) return NULL; // 标记子页为已分配,并配置内存权限 for (int i = 0; i < SUB_PAGE_COUNT; i++) { if (i >= start_idx && i < start_idx + required_sub_pages) { sub_page_allocated[i] = 1; mprotect(sub_pages[i], SUB_PAGE_SIZE, PROT_READ | PROT_WRITE); } else if (!sub_page_allocated[i]) { mprotect(sub_pages[i], SUB_PAGE_SIZE, PROT_NONE); } } return sub_pages[start_idx]; }
这种方法的缺点是会增加系统调用的开销,因为每个子页都需要单独映射和保护,适合对内存安全性要求极高的场景。
总结
如果只是想避免allocate()自身覆盖已分配块,方法一的元数据链表方案是最优选择,它是所有主流内存分配器的核心实现方式,高效且灵活;如果需要防止用户程序越界写入,再考虑方法二的子页保护方案。
内容的提问来源于stack exchange,提问作者hxdshell
相关产品推荐
相关产品推荐

