实验环境

  • CPU: i5-6500
  • OS: ubuntu20.04
  • kernel: Linux-6.1.0

模块说明

​ 我们需要编写一个模块mtest_proc_write ,我们只需要编写.write即可。

static struct proc_ops mtest_proc_fops = {
    .proc_write = mtest_proc_write
}

​ 接着我们需要编写mtest_proc_write函数,函数需要根据输入,判断执行哪个功能。具体操作是使用copy from user,将数组拷贝到内核,然后解析输入的参数,调用相应的函数。

static ssize_t mtest_proc_write(struct file *file, const chat __user *buffer, size_t count, loff_t *data){
	unsigned long int addr;
    unsigned long int value;
    
    int copy_return_msg = copy_from_user(proc_buf, buffer, count);
	if(copy_return_msg != 0){
        printk(KERN_ERR "Error occurred when copying from user...\n");
        return -EFAULT;
    }
    //...
    
    if(strncmp(proc_buf), "findpage", 8) == 0){
        sscanf(proc_buf + 9, "%lx", &addr);
        //..
        mtest_find_page(addr);
    }
	//...
} 

任务1:进程虚拟地址输出

​ 在Linux中,每个进程都有自己的进程描述符task_struct,当前进程的进程描述符可以通过current中获取。而task_struct中的mm_struct mm 指向了进程的内存描述符,我们可以在源码中的include/linux/mm_types.h中找到,在该版本中,定义位于512行,描述了进程的虚拟地址空间信息。

struct mm_struct {
    struct {
		struct vm_area_struct *mmap;        /* list of VMAs */
        struct rb_root mm_rb;
        u64 vmacache_seqnum;                   /* per-thread vmacache */
		//...
        unsigned long mmap_base;    /* base of mmap area */
        unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */
		//...
        unsigned long task_size;    /* size of task vm space */
        unsigned long highest_vm_end;   /* highest vma end address */
        pgd_t * pgd;
    }
    //..
}

​ 而vm_area_struct是一个双向循环链表的结构,它的成员如下:

struct vm_area_struct {
	unsigned long vm_start;		/* Our start address within vm_mm. */
	unsigned long vm_end;		/* The first byte after our end address within vm_mm. */
	/* linked list of VM areas per task, sorted by address */
	struct vm_area_struct *vm_next, *vm_prev;
	struct rb_node vm_rb;
	// ...
	struct mm_struct *vm_mm;	/* The address space we belong to. */
	unsigned long vm_flags;		/* Flags, see mm.h. */
};

​ 这个结构体的作用是将进程的虚拟内存地址组织成一个双向循环链表,双向循环链表中每个节点都包含了这段虚拟地址的起始地址vm_start和结束地址vm_end,拥有这个结构我们就可以遍历一个进程的虚拟地址空间。

static void mtest_list_vma(void){

    struct vm_area_struct *cur = current->mm->mmap;
    printk(KERN_INFO "Current process ID: %d", current->pid);

    while(cur) {
        // Permission
        char permission[5] = "----";

        // Read
        if(cur->vm_flags & VM_READ) {
            permission[0] = 'r';
        }
        // Write
        if(cur->vm_flags & VM_WRITE) {
            permission[1] = 'w';
        }
        // Execute
        if(cur->vm_flags & VM_EXEC) {
            permission[2] = 'x';
        }
        // Shared or not
        if(cur->vm_flags & VM_SHARED) {
            permission[3] = 's';
        } else {
            permission[3] = 'p';
        }

        printk(KERN_INFO "0x%lx 0x%lx %s\n", cur->vm_start, cur->vm_end, permission);
        cur = cur->vm_next;
    }
}

​ 通过一个while循环结构,我们可以从头开始x遍历,并输出每个虚拟地址区间的读写执行权限,最后一项用于表达是否是共享的虚拟地址区间。

任务二:虚拟地址转化为物理地址

static struct page* find_page_based_on_vma(struct vm_area_struct *vma, unsigned long addr){
    struct mm_struct *mm = vma->vm_mm;
    pgd_t *pgd = pgd_offset(mm, addr);
    p4d_t *p4d = NULL;
    pud_t *pud = NULL;
    pmd_t *pmd = NULL;
    pte_t *pte = NULL;

    struct page *page = NULL;

    if(pgd_none(*pgd) || pgd_bad(*pgd)) { return NULL; }

    p4d = p4d_offset(pgd, addr);
    if(p4d_none(*p4d) || p4d_bad(*p4d)) { return NULL; }

    pud = pud_offset(p4d, addr);
    if(pud_none(*pud) || pud_bad(*pud)) { return NULL; }

    pmd = pmd_offset(pud, addr);
    if(pmd_none(*pmd) || pmd_bad(*pmd)) { return NULL; }

    pte = pte_offset_kernel(pmd, addr);
    if(pte_none(*pte)) { return NULL; }

    page = pte_page(*pte);
    if(!page) { return NULL; }
    return page;
}

​ 这里从页目录开时,将虚拟地址逐级翻译,并通过pgd_none()pgd_bad()等进行检查,最后返回页表描述符。

static void mtest_find_page(unsigned long addr) {
    struct page *page = NULL;
    unsigned long physical_addr;

    struct vm_area_struct *vma = find_vma(current->mm, addr);
    if(!vma){
        printk(KERN_ERR "Error occurred when finding vma...\n");
        return ;
    }

    page = find_page_based_on_vma(vma, addr);
    if(!page) {
        printk(KERN_ERR "Error occurred when finding page based on vma...\n");
        return ;
    }

    physical_addr = page_to_phys(page) | (addr & ~PAGE_MASK);
    printk(KERN_INFO "Virtual Addres: 0x%lx -> Physic Address: 0x%lx\n", addr, physical_addr);

}

​ 这里使用find_vma函数,这个函数将通过mm_struct结构体和虚拟地址,找到虚拟地址所在的或者下一个vm_area_struct。找到敌营的vma结构体后,就可以通过之前的函数进行虚实地址转化。这里参考的代码,mm->vma->mm->pgd,实现有点冗余。最后通过页表的基地址加上页偏移获取实际的物理地址。

任务三:写入一个虚拟地址

static void mtest_write_val(unsigned long addr, unsigned long val) {
    struct page *page = NULL;
    unsigned long *kernel_addr;
    struct vm_area_struct *vma = find_vma(current->mm, addr);
    if(!vma) {
        printk(KERN_ERR "Error occurred when finding vma...\n");
    }

    //If vm cannot be written
    if(!(vma->vm_flags & VM_WRITE)) {
        printk(KERN_ERR "Error occurred when writing to an unwritable page...");
        return ;
    }

    page = find_page_based_on_vma(vma, addr);
    if(!page) {
        printk(KERN_ERR "Error occurred when finding page base on vma...\n");
        return ;
    } else {
        kernel_addr = (unsigned long *)page_to_phys(page) | (addr & ~PAGE_MASK);
        *kernel_addr = val;
        printk(KERN_INFO "Successfully write %ld into virtual address: 0x%lx\n", val, addr);
    }

}

​ 与前面不同的是这里进行了一次写检查。