两个方案
- 方案1 将原来只读的内存地址, 重新映射到一个可写的页面
- 方案2 获取内存地址所在的pte, 修改pte属性为可写
今天我们来讨论方案1, 方案2在这篇文章中讨论
步骤如下
- 使用
phys_to_page()
获取指定地址所在的page
- 使用
vmap()
将page
重新映射为可写
使用phys_to_page()
获取指定地址所在的page
由于要写入的数据可能跨越2个PAGE
, 我们需要先计算需要映射的页面数
//
// 计算所需page个数
// offset_in_page()获取addr在页内的偏移量, 即占一个页内的多少字节, 加上len可能会超过1个page的大小
// DIV_ROUND_UP()向上取整, 计算总共需要几个page
//
int page_num = DIV_ROUND_UP(offset_in_page(addr) + len, PAGE_SIZE);
然后我们获取需要的page
, 为方便阅读我删除了错误处理的代码
// 获取指定地址所在页面的起始地址
#define base_of_page(x) ((void*)((unsigned long)(x) & PAGE_MASK))
// 获取给定地址所在的页面
static int __get_addr_pages(void* addr, struct page *pages[], int page_num)
{
int i;
void* page_base_addr = base_of_page(addr);
for (i = 0; i < page_num; i++) {
pages[i] = phys_to_page(__pa(page_base_addr));
if (!pages[i]){
return -EFAULT;
}
// 下一个page的起始地址
page_base_addr += PAGE_SIZE;
}
return 0;
}
使用vmap()
将page重新映射为可写
代码如下, 为方便阅读我删除了错误处理的代码
// 重新映射可写
static void* remap_with_write_permissions(void* addr, int len)
{
void* writeable_addr;
int page_num = DIV_ROUND_UP(offset_in_page(addr) + len, PAGE_SIZE);
struct page **pages = kmalloc(page_num * sizeof(*pages), GFP_KERNEL);
if (!pages){
return NULL;
}
// 获取addr对应的page
if (__get_addr_`pages(addr, pages, page_num)){
goto err;
}
// 重新映射page到可写的地址
writeable_addr = vmap(pages, page_num, VM_MAP, PAGE_KERNEL);
kfree(pages);
return writeable_addr + offset_in_page(addr);
err:
kfree(pages);
return NULL;
}
映射成功之后会返回一个可以写入的地址, 使用此地址就可以写入数据了, 示例如下
int __my_memcpy(void *dst, void *src, int len)
{
void *new_dst = remap_with_write_permissions(dst, len);
if(new_dst){
memcpy(new_dst, src, len);
vunmap(base_of_page(new_dst));
}
return 0;
}
参考资料
https://gist.github.com/ohnx/02c181c2674ca6db88ddd12b36e6c1bf#file-ttgl-c