Linux内核修改内存属性

在 Linux 内核中修改内存属性通常涉及对页表(Page Tables)的操作,主要应用场景包括设置内存区域的读写/执行权限、缓存策略等。以下是常见的实现方式和注意事项:


一、常见内存属性修改场景

  1. 设置内存只读/可写(RO/RW)
  2. 禁用/启用执行权限(NX/Execute)
  3. 修改缓存属性(如 Uncached、Write-Combined)
  4. 内存保护(如防止代码注入)

二、内核提供的 API

Linux 内核提供了一组函数用于修改内存属性,主要位于 asm/pgtable.hasm/set_memory.h

1. 修改读写/执行权限

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

// 设置为只读(Read-Only)
int set_memory_ro(unsigned long addr, int num_pages);

// 设置为可写(Read-Write)
int set_memory_rw(unsigned long addr, int num_pages);

// 禁用执行权限(NX)
int set_memory_nx(unsigned long addr, int num_pages);

// 启用执行权限(X)
int set_memory_x(unsigned long addr, int num_pages);

2. 修改缓存属性

1
2
3
4
5
6
7
8
// 设置内存为不可缓存(Uncached)
int set_memory_uc(unsigned long addr, int num_pages);

// 设置内存为写合并(Write-Combined)
int set_memory_wc(unsigned long addr, int num_pages);

// 恢复默认缓存属性(Write-Back)
int set_memory_wb(unsigned long addr, int num_pages);

3. 直接操作页表项(PTE)

对于高级需求,可直接操作页表项,但需谨慎:

1
2
3
4
5
6
7
8
9
10
#include <asm/pgtable.h>

// 获取 PTE
pte_t *pte = lookup_address(addr, &level);
if (pte) {
// 修改权限(例如清除可写位)
pte_val(*pte) &= ~_PAGE_RW;
// 刷新 TLB
__flush_tlb_one_kernel(addr);
}

三、使用示例

示例 1:将内核模块代码设为只读

1
2
3
4
5
6
// 假设代码区域地址为 addr,大小为 size
unsigned long addr = (unsigned long)your_function;
int num_pages = PFN_UP(size);

// 设置为只读
set_memory_ro(addr, num_pages);

示例 2:禁用某内存区域的执行权限

1
2
// 禁用执行权限(防漏洞利用)
set_memory_nx(addr, num_pages);

四、注意事项

  1. 地址对齐
    地址需按页对齐(PAGE_SIZE),可通过 virt_to_page()PFN_ALIGN 处理。

  2. TLB 刷新
    修改页表后需刷新 TLB,内核 API 通常自动处理,直接操作 PTE 时需手动调用 __flush_tlb_one_kernel()

  3. 安全风险

    • 修改内核内存属性可能导致系统不稳定。
    • 禁用执行权限(NX)需硬件支持(如 x86 的 NX 位、ARM 的 XN 位)。
  4. 权限限制
    部分操作需要内核配置(如 CONFIG_STRICT_KERNEL_RWX 是否启用)。

  5. 模块签名
    修改内核代码段可能导致模块签名验证失败(Secure Boot 环境)。


五、调试与验证

  • 检查页表属性
    通过 /proc/kpageflagscrash 工具分析内存属性。
  • 内核日志
    使用 dmesg 查看可能的警告或错误。

六、高级场景

  • 动态修改用户空间内存
    需结合 get_user_pages()set_page_attributes()
  • KVM 虚拟化
    修改客户机内存属性需操作 EPT/NPT。

七、完整代码示例(内核模块)

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
#include <linux/module.h>
#include <linux/init.h>
#include <asm/set_memory.h>

static void __exit my_exit(void) {
printk("Module unloaded\n");
}

static int __init my_init(void) {
unsigned long addr = (unsigned long)my_init; // 测试地址
int num_pages = 1;

// 将当前模块的 init 函数设为只读
if (set_memory_ro(addr, num_pages)) {
printk("Failed to set memory RO\n");
return -EINVAL;
}

printk("Memory set to read-only\n");
return 0;
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");

八、参考资料

  • Linux 内核源码:arch/x86/mm/pat/set_memory.c
  • 内核文档:Documentation/x86/pat.txt
  • 《Understanding the Linux Kernel》(内存管理章节)

通过上述方法,您可以安全地修改内存属性,但请务必在开发环境中充分测试!