两个方案
方案1 将原来只读的内存地址, 重新映射到一个可写的页面
方案2 获取内存地址所在的pte, 修改pte属性为可写
今天我们来讨论方案2
事实上, Linux内核已经提供了相关的函数set_memory_rw
, 但是此函数有个限制, 那就是只能修改通过vmalloc
或者vmap
申请的内存地址, 我们可以参考它实现一个无此限制的函数
实际上直接把vm_area的检测删除即可, 代码如下
static int change_memory_common(unsigned long addr, int numpages,
pgprot_t set_mask, pgprot_t clear_mask)
{
unsigned long start = addr;
unsigned long size = PAGE_SIZE*numpages;
unsigned long end = start + size;
struct vm_struct *area;
if (!PAGE_ALIGNED(addr)) {
start &= PAGE_MASK;
end = start + size;
WARN_ON_ONCE(1);
}
if (!numpages)
return 0;
return __change_memory_common(start, size, set_mask, clear_mask);
}
其中最重要的是__change_memory_common
函数, 我们看下它的实现
/*
* This function assumes that the range is mapped with PAGE_SIZE pages.
*/
static int __change_memory_common(unsigned long start, unsigned long size,
pgprot_t set_mask, pgprot_t clear_mask)
{
struct page_change_data data;
int ret;
data.set_mask = set_mask;
data.clear_mask = clear_mask;
ret = apply_to_page_range(&init_mm, start, size, change_page_range,
&data);
flush_tlb_kernel_range(start, start + size);
return ret;
}
最重要的就是这个函数apply_to_page_range
, 此函数会遍历要修改的内存范围所在的pte, 针对范围内的每一个pte调用我们提供的回调函数change_page_range
, 回调函数实现如下
static int change_page_range(pte_t *ptep, pgtable_t token, unsigned long addr,
void *data)
{
struct page_change_data *cdata = data;
pte_t pte = READ_ONCE(*ptep);
pte = clear_pte_bit(pte, cdata->clear_mask);
pte = set_pte_bit(pte, cdata->set_mask);
set_pte(ptep, pte);
return 0;
}
我们直接在回调函数中修改pte的属性即可, 最后__change_memory_common
会调用flush_tlb_kernel_range
刷新tlb完成修改
使用方法如下, 设置指定的地址可以读写:
int set_memory_rw(unsigned long addr, int numpages)
{
return change_memory_common(addr, numpages,
__pgprot(PTE_WRITE),
__pgprot(PTE_RDONLY));
}
参考资料
https://elixir.bootlin.com/linux/v4.19.273/source/arch/arm64/mm/pageattr.c
https://elixir.bootlin.com/linux/v4.19.273/source/mm/memory.c