linux 内核页表替换
一.linux本身关于页表查找机制的代码实现
1. 逻辑地址到页表的查找过程
static int __follow_pte_pmd(struct mm_struct *mm, unsigned long address,
unsigned long *start, unsigned long *end,
pte_t **ptepp, pmd_t **pmdpp, spinlock_t **ptlp)
{
pgd_t *pgd;
p4d_t *p4d;
pud_t *pud;
pmd_t *pmd;
pte_t *ptep;
/* 进程的Page Global Directory基指针存放在mm->pgd */
pgd = pgd_offset(mm, address); /* 返回address指向该进程的Page Global Directory指针 */
if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
goto out;
p4d = p4d_offset(pgd, address); /* 在4级页面机制中,不做任何操作,直接返回pgd */
if (p4d_none(*p4d) || unlikely(p4d_bad(*p4d)))
goto out;
pud = pud_offset(p4d, address); /* 返回address指向该进程的Page Upper Directory指针 */
if (pud_none(*pud) || unlikely(pud_bad(*pud)))
goto out;
pmd = pmd_offset(pud, address); /* 返回address指向该进程的Page Middle Derectory指针 */
/* ... */
if (pmd_none(*pmd) || unlikely(pmd_bad(*pmd)))
goto out;
if (start && end) {
*start = address & PAGE_MASK;
*end = *start + PAGE_SIZE;
mmu_notifier_invalidate_range_start(mm, *start, *end);
}
ptep = pte_offset_map_lock(mm, pmd, address, ptlp); /* 返回address指向该进程的Page Table指针 */
/* 判断Page Table是否保留在物理内存之中 */
if (!pte_present(*ptep))
goto unlock;
*ptepp = ptep;
*ptepp = ptep;
return 0;
unlock:
pte_unmap_unlock(ptep, *ptlp);
if (start && end)
mmu_notifier_invalidate_range_end(mm, *start, *end);
out:
return -EINVAL;
}
2.Page Table转为物理地址
int follow_phys(struct vm_area_struct *vma,
unsigned long address, unsigned int flags,
unsigned long *prot, resource_size_t *phys)
{
/* ... */
if (follow_pte(vma->vm_mm, address, &ptep, &ptl))
goto out;
pte = *ptep;
/* ... */
*phys = (resource_size_t)pte_pfn(pte) << PAGE_SHIFT; /* Page Table转化为物理地址 */
ret = 0;
unlock:
pte_unmap_unlock(ptep, ptl);
out:
return ret;
}
关于分页机制的4级还是几级查找不是我关注的,我的目标是在
Page_Table到Page物理地址查找的一个替换。
phys = (resource_size_t)pte_pfn(pte) << PAGE_SHIFT; / Page Table转化为物理地址 */
这句即是Page_Table到Page的查找过程。那么其中必然就会涉及到表的查询,我们当然也可以在类似的实现中自己构造新的页表跳转。
3.页表到物理地址的转换
linux-4.14.146/arch/x86/include/asm/pgtable.h:static inline unsigned long pte_pfn(pte_t pte)
static inline unsigned long pte_pfn(pte_t pte)
{
phys_addr_t pfn = pte_val(pte); //宏用来把pgd_t类型转换成无符号长整型
pfn ^= protnone_mask(pfn);
return (pfn & PTE_PFN_MASK) >> PAGE_SHIFT;
}
/* Get a mask to xor with the page table entry to get the correct pfn. */
static inline u64 protnone_mask(u64 val)
{
return __pte_needs_invert(val) ? ~0ull : 0;
}
static inline bool __pte_needs_invert(u64 val)
{
return val && !(val & _PAGE_PRESENT);
}
#define _PAGE_PRESENT (_AT(pteval_t, 1) << _PAGE_BIT_PRESENT)
#define _PAGE_BIT_PRESENT 0 /* is present */
#ifdef __ASSEMBLY__
#define _AC(X,Y) X
#define _AT(T,X) X
#else
#define __AC(X,Y) (X##Y)
#define _AC(X,Y) __AC(X,Y)
#define _AT(T,X) ((T)(X))
#endif
#define PAGE_SHIFT 12
#define PTE_PFN_MASK ((pteval_t)PHYSICAL_PAGE_MASK)
#define PHYSICAL_PAGE_MASK (((signed long)PAGE_MASK) & __PHYSICAL_MASK)
#ifdef CONFIG_DYNAMIC_PHYSICAL_MASK
extern phys_addr_t physical_mask;
#define __PHYSICAL_MASK physical_mask
#else
#define __PHYSICAL_MASK ((phys_addr_t)((1ULL << __PHYSICAL_MASK_SHIFT) - 1))
#endif
phys_addr_t physical_mask = (1ULL << __PHYSICAL_MASK_SHIFT) - 1;
#ifdef CONFIG_DYNAMIC_PHYSICAL_MASK
extern phys_addr_t physical_mask;
#define __PHYSICAL_MASK physical_mask
#else
#define __PHYSICAL_MASK ((phys_addr_t)((1ULL << __PHYSICAL_MASK_SHIFT) - 1))
#endif
#define __PHYSICAL_MASK_SHIFT 52
typedef u64 phys_addr_t;
二.重新构建查表过程
int follow_phys(struct vm_area_struct *vma,
unsigned long address, unsigned int flags,
unsigned long *prot, resource_size_t *phys)
{
int ret = -EINVAL;
pte_t *ptep, pte;
spinlock_t *ptl;
if (!(vma->vm_flags & (VM_IO | VM_PFNMAP)))
goto out;
//if (follow_pte(vma->vm_mm, address, &ptep, &ptl))
// goto out;
if (follow_pte_pmd(vma->vm_mm,address, NULL, NULL,
&ptep, NULL, &ptl))//page find
goto out;
pte = *ptep;
if ((flags & FOLL_WRITE) && !pte_write(pte))
goto unlock;
*prot = pgprot_val(pte_pgprot(pte));
*phys = (resource_size_t)pte_pfn(pte) << PAGE_SHIFT;
ret = 0;
unlock:
pte_unmap_unlock(ptep, ptl);
out:
return ret;
}
这一段大部分是内核中的代码,但有一点follow_pte没有导出,但是导出了follow_pte_pmd。这两个函数在源码里的实现是一样的。所以完全可以替换。
另一个关键是获取的struct vm_area_struct
如下:
struct vm_area_struct* get_vma(unsigned long addr){
struct task_struct *cur_task =current;//get_current();
struct vm_area_struct *cur_vma = find_vma(cur_task->mm_strcut,addr);
if (!vma || vma->vm_start > start)
{
printk("\n");
return -EINVAL;
}
printk("\n");
return cur_vma;
}
那么理论上我们通过页表查询的方式获取到了物理地址
resource_size_t phys;
resource_size_t phys_addr;
unsigned long long* target = kmalloc(0x200, GFP_KERNEL);
unsigned long long phytarget = virt_to_phys(target);
__u64 phytargets = phytarget;
struct vm_area_struct * cur_vma = find_vma(target);
int ret = follow_phys(cur_vma,target,FOLL_WRITE, &prot, &phys);
if(ret){
printk("find physic error\n");
}
这里有个FOLL_WRITE位,内核中定义如下:
#define FOLL_WRITE 0x01 /* check pte is writable */
#define FOLL_TOUCH 0x02 /* mark page accessed */
#define FOLL_GET 0x04 /* do get_page on page */
#define FOLL_DUMP 0x08 /* give error on hole if it would be zero */
#define FOLL_FORCE 0x10 /* get_user_pages read/write w/o permission */
#define FOLL_NOWAIT 0x20 /* if a disk transfer is needed, start the IO
* and return without waiting upon it */
#define FOLL_POPULATE 0x40 /* fault in page */
#define FOLL_SPLIT 0x80 /* don't return transhuge pages, split them */
#define FOLL_HWPOISON 0x100 /* check page is hwpoisoned */
#define FOLL_NUMA 0x200 /* force NUMA hinting page fault */
#define FOLL_MIGRATION 0x400 /* wait for page to replace migration entry */
#define FOLL_TRIED 0x800 /* a retry, previous pass started an IO */
#define FOLL_MLOCK 0x1000 /* lock present pages */
可见这里应该是可以更改的。
三.内存描述属性
内核中的地址块因为不是连续的,所以存在多个vma数据块。而且这些块也是通过红黑树来组织的。所以理论上vma----mm-----pte_t的查询路径应该是可行的,但是我在4.14.146版本上进行测试的时候。kmalloc的虚拟地址无法找到对应的vma。find_vma总是NULL。
这里有点不太懂。
好在找到了另一种方式。
unsigned long init_mm_addr = kallsyms_lookup_name("init_mm");
struct mm_struct *my_mm;
my_mm = (struct mm_struct *)init_mm_addr;
printk(KERN_NOTICE "init_mm_addr %p\n",init_mm_addr);
pgd = pgd_offset(my_mm, address);
if (pgd_none(*pgd) || pgd_bad(*pgd))
goto out;
其他的页表查询方式跟之前的一样。
四.修改页表属性
经过多次尝试,终于在内核中以模块的方式实现了查询页表到物理地址的整个过程,这个过程的意义在于,对于页表属性的更改和页表替换必须依赖以上条件。
arm 64 上有两个函数set_pte_bit,clear_pte_bit。从名字看,我们对页表描述符的修改可以依赖以上两个函数。但是好像x86上没有这两个函数,不过不要紧,我们可以自己实现。
static inline pte_t set_pte_bit(pte_t pte, pgprot_t prot)
{
pte_val(pte) |= pgprot_val(prot);
return pte;
}
#define pgprot_val(x) ((x).pgprot)
typedef struct pgprot { pgprotval_t pgprot; } pgprot_t;
typedef unsigned long pgprotval_t;
#define pte_val(x) native_pte_val(x)
#define __pte(x) native_make_pte(x)
static inline pteval_t native_pte_val(pte_t pte)
{
return pte.pte;
}
typedef struct { pteval_t pte; } pte_t;
typedef unsigned long pteval_t;
#define pte_pgprot(x) __pgprot(pte_flags(x))
static inline pteval_t pte_flags(pte_t pte)
{
return native_pte_val(pte) & PTE_FLAGS_MASK;
}
#define __pgprot(x) ((pgprot_t) { (x) } )
static inline pteval_t native_pte_val(pte_t pte)
{
return pte.pte;
}
五、页表替换
这里走了点弯路,因为kmalloc分配的都是巨页,所以开始的时候老是想怎么能分配一个标准页来着。
问了下总管,说是要看下分配器的内容,开始没找到,后来发现了个直接的调用,用来分配一个物理页。
vpage = get_zeroed_page(GFP_ATOMIC);
当时还有第二种做法,就是全局变量默认都是标准页。
不过想着全局变量不太灵活,所以还是直接分配吧。
我这里以ptep为代表的说明。
这个指针代表页表描述符。主要包含了两个信息。
- 页帧号(经过位移转换可以得到对应物理页的的物理地址)
- 页属性(用来控制页的操控属性,读写执行等。)
所以我这里写了两个函数
cp_page(unsigned long * target,unsigned long address)
– lookup_address()
ch_physic(unsigned long destaddr,unsigned long srcaddr)
– lookup_address()
–lookup_address()
这里进行了三次虚拟地址查表的过程,理论上可以减少到两个,因为如果考虑到效率问题。不过替换操作发生不是很频繁,应该可以忽略,暂且这样。
#define PTE_PHY_PAGE 0x0000fffffffff000
#define PTE_PHY_PAGE_CLEAR 0xffff000000000fff
pte=*destptep;
printk("ch pte %016lx \n",pte );
pte.pte = pte.pte & PTE_PHY_PAGE_CLEAR;
printk("ch pte clear phypage %016lx \n",pte );
pten=*srcptep;
printk("ch pten %016lx \n",pten);
pten.pte = pten.pte & PTE_PHY_PAGE;
printk("ch pten phyget %016lx \n",pten);
pte.pte = pte.pte + pten.pte ;
printk("ch pte result %016lx \n",pte );
destptep->pte=pte.pte;
替换过程如上。