把虚地址转化为物理地址
-
运行代码并截图 成功运行
address.c
#include <linux/init.h> #include <linux/module.h> #include <linux/mm.h> #include <linux/mm_types.h> #include <linux/sched.h> #include <linux/export.h> #include <linux/delay.h> static unsigned long cr0,cr3; static unsigned long vaddr = 0; static void get_pgtable_macro(void) //打印页机制中的一些重要参数 { cr0 = read_cr0(); cr3 = read_cr3_pa(); printk("cr0 = 0x%lx, cr3 = 0x%lx\n",cr0,cr3); //这些宏是用来指示线性地址中相应字段所能映射的区域大小的对数的 printk("PGDIR_SHIFT = %d\n", PGDIR_SHIFT); printk("P4D_SHIFT = %d\n",P4D_SHIFT); printk("PUD_SHIFT = %d\n", PUD_SHIFT); printk("PMD_SHIFT = %d\n", PMD_SHIFT); printk("PAGE_SHIFT = %d\n", PAGE_SHIFT); //指示page offset字段,映射的是一个页面的大小,一个页面大小是4k,转换成以2为底的对数就是12,其他的宏类似 //下面的这些宏是用来指示相应的页目录表中的项的个数的,这些宏都是为了方便寻页时进行位运算的 printk("PTRS_PER_PGD = %d\n", PTRS_PER_PGD); printk("PTRS_PER_P4D = %d\n", PTRS_PER_P4D); printk("PTRS_PER_PUD = %d\n", PTRS_PER_PUD); printk("PTRS_PER_PMD = %d\n", PTRS_PER_PMD); printk("PTRS_PER_PTE = %d\n", PTRS_PER_PTE); printk("PAGE_MASK = 0x%lx\n", PAGE_MASK); //page_mask,页内偏移掩码,用来屏蔽掉page offset字段 } static unsigned long vaddr2paddr(unsigned long vaddr) //线性地址到物理地址转换 { //首先为每个目录项创建一个变量将它们保存起来 pgd_t *pgd; p4d_t *p4d; pud_t *pud; pmd_t *pmd; pte_t *pte; unsigned long paddr = 0; unsigned long page_addr = 0; unsigned long page_offset = 0; pgd = pgd_offset(current->mm,vaddr); //第一个参数是当前进程的mm_struct结构(我们申请的线性地址空间是内核,所以应该查内核页表,又因为所有的进程都共享同一个内核页表,所以可以用当前进程的mm_struct结构来进行查找) printk("pgd_val = 0x%lx, pgd_index = %lu\n", pgd_val(*pgd),pgd_index(vaddr)); if (pgd_none(*pgd)){ printk("not mapped in pgd\n"); return -1; } p4d = p4d_offset(pgd, vaddr); //查找到的页全局目录项pgd作为下级查找的参数传入到p4d_offset中 printk("p4d_val = 0x%lx, p4d_index = %lu\n", p4d_val(*p4d),p4d_index(vaddr)); if(p4d_none(*p4d)) { printk("not mapped in p4d\n"); return -1; } pud = pud_offset(p4d, vaddr); printk("pud_val = 0x%lx, pud_index = %lu\n", pud_val(*pud),pud_index(vaddr)); if (pud_none(*pud)) { printk("not mapped in pud\n"); return -1; } pmd = pmd_offset(pud, vaddr); printk("pmd_val = 0x%lx, pmd_index = %lu\n", pmd_val(*pmd),pmd_index(vaddr)); if (pmd_none(*pmd)) { printk("not mapped in pmd\n"); return -1; } pte = pte_offset_kernel(pmd, vaddr); //与上面略有不同,这里表示在内核页表中查找,在进程页表中查找是另外一个完全不同的函数 这里最后取得了页表的线性地址 printk("pte_val = 0x%lx, ptd_index = %lu\n", pte_val(*pte),pte_index(vaddr)); if (pte_none(*pte)) { printk("not mapped in pte\n"); return -1; } //从页表的线性地址中取出该页表所映射页框的物理地址 page_addr = pte_val(*pte) & PAGE_MASK; //取出其高48位 //取出页偏移地址,页偏移量也就是线性地址中的低12位 page_offset = vaddr & ~PAGE_MASK; //将两个地址拼接起来,就得到了想要的物理地址了 paddr = page_addr | page_offset; printk("page_addr = %lx, page_offset = %lx\n", page_addr, page_offset); printk("vaddr = %lx, paddr = %lx\n", vaddr, paddr); return paddr; } static int __init v2p_init(void) //内核模块的注册函数 { unsigned long vaddr = 0 ; printk("vaddr to paddr module is running..\n"); get_pgtable_macro(); printk("\n"); vaddr = __get_free_page(GFP_KERNEL); //在内核的ZONE_NORMAL中申请了一块页面,GFP_KERNEL标志指示优先从内核的ZONE_NORMAL中申请页框 if (vaddr == 0) { printk("__get_free_page failed..\n"); return 0; } sprintf((char *)vaddr, "hello world from kernel"); //在地址中写入hello printk("get_page_vaddr=0x%lx\n", vaddr); vaddr2paddr(vaddr); ssleep(600); return 0; } static void __exit v2p_exit(void) //内核模块的卸载函数 { printk("vaddr to paddr module is leaving..\n"); free_page(vaddr); //将申请的线性地址空间释放掉 } module_init(v2p_init); module_exit(v2p_exit); MODULE_LICENSE("GPL");
Makefile
obj-m += address.o #generate the path CURRENT_PATH:=$(shell pwd) #the current kernel version number LINUX_KERNEL:=$(shell uname -r) #the absolute path LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL) #complie object all: make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules #clean clean: make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
运行截图
2.利用调试工具进行调试
3.结合所讲原理进行分析,有自己的见解
进程中不直接对物理地址进行操作,CPU在运行时指定的地址要经过MMU转换后才能访问到真正的物理内存。地址转换主要分为分段和分页,本次实验主要是线性地址到物理地址的转换。CR3是用来保存当前进程的页全局目录的地址。进程切换时,操作系统通过访问task_struct结构再访问mm_struct结构(mm_struct结构是用来描述进程的虚拟地址空间的),最终找到PGD字段,取得新进程的页全局目录的地址,填充到CR3寄存器中就完成页表的切换。
本实验主要功能是:在内核中先申请一个页面,然后利用内核提供的函数按照寻页的步骤一步步查询各级页目录,最终找到所对应的物理地址。首先通过get_pgtable_macro()函数打印页机制中一些重要参数,cr3寄存器的值通过这个read_cr3_pa()函数来获得,x_SHIFT宏指示线性地址中相应字段所能映射区域大小的对数,PAGE_SHIFT是page offset字段所能映射区域大小的对数,PTRS_PER_x指示相应的页目录表中项的个数。vaddr2paddr()函数实现线性地址到物理地址的转换,pgd通过pgd_offset(current->mm,vaddr)获得,然后讲pgd作为下级查找的参数获得p4d,接着找pud、pmd,最终找到pte页表项(在主内核页表中查找),将它和PAGE_MASK进行位与操作取出其高48位,得到页框的物理地址,将PAGE_MASK按位取反和线性地址进行位与操作,得到页内偏移,将页框的物理地址和页内偏移进行位或操作,得到物理地址。
4.提出2个问题,并给予回答
(1)为什么linux中逻辑地址等于线性地址?
因为linux所有段(用户代码段、用户数据段、内核代码段、内核数据段)的线性地址都是从0x00000000开始,长度4G,这样,线性地址=逻辑地址+0x00000000,也就是逻辑地址等于线性地址。
(2)对于32位硬件,怎样让它的二级管理架构和四级转换协调工作?
从硬件角度看,32位地址被分成了三部分,不管软件怎么做,最终落实到硬件,也只认识页面目录索引、页表索引、偏移这三位。从软件角度看,由于多引入两部分,要让二层架构的硬件认识五部分,要让二层架构的硬件认识五部分很容易,在地址划分的时候,将页上级目录和页中间目录的长度设为0就可以。
怎样让它的二级管理架构和四级转换协调工作? 从硬件角度看,32位地址被分成了三部分,不管软件怎么做,最终落实到硬件,也只认识页面目录索引、页表索引、偏移这三位。从软件角度看,由于多引入两部分,要让二层架构的硬件认识五部分,要让二层架构的硬件认识五部分很容易,在地址划分的时候,将页上级目录和页中间目录的长度设为0就可以。
考虑到64位地址,使用四层转换架构的CPU,我们就不再把中间两个设为0了。