Linux中的地址转换

1. 运行代码并截图

paging_lowmem.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结构来进行查找),pgd为页全局目录项*/
    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;    /*取出其高52位*/
    /*取出页偏移地址,页偏移量也就是线性地址中的低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:= paging_lowmem.o

CURRENT_PATH:=$(shell pwd)	#模块所在的当前所在路径
LINUX_KERNEL:=$(shell uname -r)	#linux内核代码的当前版本
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)	#linux内核的当前版本源码路径

all:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules	#编译模块
#				内核的路径		  当前目录编译完放哪   表明编译的是内核模块

clean:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean	#清理模块

运行结果:

在这里插入图片描述

可以看到PGDIR_SHIFT和P4D_SHIFT都是39的,这也就意味着在线性地址中,P4D这个字段是为空的,也可以看到在页目录项中,P4D的页目录项也是为1的,这也就说明了,虽然Linux采用了五级页表模型,但实际上使用的页表也只有四个

PAGE_MASK为低12位都为零,其余位都为1的一个64位的数

可以看到paddr的第一位是8,转换为二进制也就是最高位63位是为1的,这是X86平台上用来标识该物理页框是不能用来执行代码的一个保护位,其物理页框的物理地址就是后面的9位

2.利用调试工具进行调试

调试工具:

​ ①dram内核模块,其主要功能是通过mmap将物理内存中的数据映射到我们的设备文件中,我们通过对于这个设备文件进行访问,就可以达到访问物理内存的功能了

​ ②fileview,它可以按照我们想要的一种格式阅读这种二进制文件

这两个工具的源代码参考http://t.csdn.cn/2lc7y

补充:EXPORT_SYMBOL的作用:EXPORT_SYMBOL标签内定义的函数或者符号对全部内核代码公开,不用修改内核代码就可以在您的内核模块中直接调用。使用方法放下:

​ 1、在模块函数定义之后使用“EXPORT_SYMBOL(函数名)”来声明。

​ 2、在调用该函数的另外一个模块中使用extern对之声明。

​ 3、先加载定义该函数的模块,然后再加载调用该函数的模块,请注意这个先后顺序。

打开另一个终端窗口,依次执行如下指令:

​ sudo insmod dram.ko

​ sudo mknod /dev/dram c 85 0

​ ./fileview /dev/dram

运行结果:

在这里插入图片描述

寻址过程:

页目录 二进制 十进制*8B 十六进制*8B
PGD 1 0011 0111(39) 311*8B 9b8
PUD 0 1100 0101(30) 197*8B 628
PMD 0 1000 1011(21) 139*8B 458
PTE 0 0001 1000(12) 24*8B c0

页全局目录表的基地址cr3 = 0x118fa000

0x118fa000+0x9b8 = 0x118fa9b8 → 3a202067即pgd_val值(下级页表的物理地址)

0x3a202000+0x628 = 0x3a202628 → 3a203067即pud_val值(下级页表的物理地址)

0x3a203000 + 0x458 =0x3a203458 → 12148063即pmd_val值(下级页表的物理地址)

0x12148000 + 0xc0 = 0x121480c0 → 8000000011618063即pte_val值(页面物理地址)

页内偏移量page_offset为0

由此可推出,所求物理地址为11618000

在这里插入图片描述

3.结合所讲原理进行分析,有自己的见解

段机制:虚拟地址→线性地址

​ 在Intel的80x86处理器中,虚拟地址到线性地址的转换可参考《Linux操作系统原理与应用》P29:

在这里插入图片描述

​ Linux内核的设计并没有全部采用Intel所提供的段方案,仅仅有限度地用了分段机制,这不仅简化了Linux内核的设计,而且为把Linux移植到其他平台创造了条件。Linux的设计人员让段的基地址为0,而段的界限为4GB,这时任意给出一个偏移量,则等式“0+偏移量=线性地址”,也就是说“偏移地址=线性地址”。

分页机制的由来

​ 可以设想,如果不使用分页,线性地址空间直接被映射到物理空间,那么修改任何一个段的数据,都会同时修改其他段的数据,如不修改,段机制所提供的通过“基地址:界限”方式将线性地址空间分割,以让段与段之间完全隔离,这种实现段保护的方式根本就不起作用了,因为它们可能会相互覆盖。

分页机制:线性地址→物理地址

​ 两级页表的32位线性地址到物理地址的转换可参考《Linux操作系统原理与应用》P34:

在这里插入图片描述

​ 目前Linux为了兼容32位和64位的CPU,需要一个统一的页面地址模型,最常用的是4级页表模型:

在这里插入图片描述

由于是64位处理器,所以显示的地址是64位,但由于64位处理器硬件的限制,地址线只有48条,所以线性地址和物理地址实际使用的也只有48位,在64位Linux中使用了4级页表结构,它的线性地址划分如下图所示,在这种情况下页面的大小都为4kb,每一个页表项大小为8bit,整个页表可以映射的空间是256TB。而新的Intel芯片的MMU硬件规定可以进行5级的页表管理,所以在4.15的内核中,Linux在页全局目录和页上级目录之间又增加了一个新的页目录,叫做P4D页目录(在PGD和PUD之间)。CR3寄存器用来保存当前进程的页全局目录的地址,寻页的开始就是从页全局目录开始的。

在这里插入图片描述

​ 五级页表的64位线性地址到物理地址的转换可参考两级页表的32位线性地址到物理地址的转换,这里以第一问中的paging_lowmem.c代码进行说明:

​ vaddr为线性地址

	pgd = pgd_offset(current->mm, vaddr);

​ 第一个参数是当前进程的mm_struct结构,mm_struct结构是用来描述进程的虚拟地址空间的,在mm_struct中有个字段PGD就是用来保存该进程的页全局目录的物理地址的。这行代码找到了PGD表项的物理地址pgd,该物理地址下存放的是下级页表的物理地址

	p4d = p4d_offset(pgd, vaddr);

​ 这行代码找到了P4D表项的物理地址p4d,由于页四级目录没有启用,所以目录表项为1,即p4d=pgd。

​ 以此类推······,最后:

	pte = pte_offset_kernel(pmd, vaddr);

​ 到此获得PTE表项的物理地址pte,该物理地址下存放的是页面物理地址,对应于两级页表的32位线性地址到物理地址转换的第三步。

	page_addr = pte_val(*pte) & PAGE_MASK;    /*取出其高52位*/
	/*取出页偏移地址,页偏移量也就是线性地址中的低12位*/
	page_offset = vaddr & ~PAGE_MASK;
	/*将两个地址拼接起来,就得到了想要的物理地址了*/
	paddr = page_addr | page_offset;

​ 最后一步都是取其高位(64位取高52位,32位取高20位)与线性地址的低12位即偏移量拼接起来,结果就是物理地址

4.提出2个问题,并给予回答

页全局目录的地址在哪?

​ 内核在创建一个进程时就会为它分配页全局目录,在进程描述符task_struct结构中有一个指向mm_struct结构的指针mm,而mm_struct结构是用来描述进程的虚拟地址空间的,在mm_struct中有个字段PGD就是用来保存该进程的页全局目录的物理地址的。(所以在进程切换的时候,操作系统通过访问task_struct结构,再访问mm_struct结构,最终找到PGD字段,取得新进程的页全局目录的地址,填充到CR3寄存器中就完成了页表的切换)

为什么linux中逻辑地址等于线性地址?

​ 因为linux所有段(用户代码段、用户数据段、内核代码段、内核数据段)的线性地址都是从0x00000000开始,长度4G,这样,线性地址=逻辑地址+0x00000000,也就是逻辑地址等于线性地址。

猜你喜欢

转载自blog.csdn.net/qq_58538265/article/details/133920479