mmu以及页表 linuxkernel(2)

来源:互联网 发布:欧洲人 知乎 编辑:程序博客网 时间:2024/04/29 14:49

对于mmu的作用,参看第一篇的介绍

这里讲linux kenel的mmu和页表

部分内容图片参考 http://blog.csdn.net/luckyapple1028/article/details/45287617  非常不错的blog



对于1M段大小的虚拟地址和物理地址转换,arm1176计算方式如下

  1. 虚拟地址的[31:20]位存放一级页表的入口index[19:0]位存放段偏移;

  2. TTBRtranslation table base register,协处理器CP15中的一个寄存器,用于存放一级页表的基址)寄存器中获取一级页表的基址;

  3. 一级页表基址+ VA[31:20] = 该虚拟地址对应的页表描述符的入口地址;

  4. 页表描述符的[31:20]位为该虚拟地址对应的物理段基址;

  5. 物理段基址+ VA[19:0]段偏移物理地址



linux有两次页表处理


第一次是在arch/arm/kernel/head.s里面

第二次是是在start_kernel以后

还有其他的一些io地址映射

内核解压到了0x8000处,并且从0x8000开始执行


第一次在arch/arm/kernel/head.s

/* * Setup the initial page tables.  We only setup the barest * amount which are required to get the kernel running, which * generally means mapping in the kernel code. * * r8 = phys_offset, r9 = cpuid, r10 = procinfo * * Returns: *  r0, r3, r5-r7 corrupted *  r4 = page table (see ARCH_PGD_SHIFT in asm/memory.h) */__create_page_tables:pgtblr4, r8@ page table address/* * Clear the swapper page table */movr0, r4movr3, #0addr6, r0, #PG_DIR_SIZE1:strr3, [r0], #4strr3, [r0], #4strr3, [r0], #4strr3, [r0], #4teqr0, r6bne1b

其中pgtbl

.macropgtbl, rd, physadd\rd, \phys, #TEXT_OFFSETsub\rd, \rd, #PG_DIR_SIZE.endm

这里明确说明建立的页表只是为了kernel能够运行,因此只映射kernel code.

输入的时候 r8是物理地址偏移(0) r9是cpuid r10是procinfo

返回值r4是页表基地址(0x4000)


下面来看这个操作过程


pgtbl r4,r8被展开为

add r4,r8,#TEXT_OFFSET 表示内核起始地址相对于RAM地址的偏移值 (定义在arch/arm/Makefile中 TEXT_OFFSET := $(textofs-y)  textofs-y:= 0x00008000 )

sub r4,r4,#PG_DIR_SIZE 表示页目录的大小。

我这里变成

ADD     R4, R8, #0x8000
SUB     R4, R4, #0x4000

0x4000是页目录的大小为16KB

0x8000表示内核起始地址相对于RAM地址的偏移值 

r4=r8+0x8000-0x4000

得到r4等于物理页表的起始地址

后面就是循环清空这一块地址 从r4->r4+0x4000。全部清0


ldrr7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags/* * Create identity mapping to cater for __enable_mmu. * This identity mapping will be removed by paging_init(). */adrr0, __turn_mmu_on_locldmiar0, {r3, r5, r6}subr0, r0, r3@ virt->phys offsetaddr5, r5, r0@ phys __turn_mmu_onaddr6, r6, r0@ phys __turn_mmu_on_endmovr5, r5, lsr #SECTION_SHIFTmovr6, r6, lsr #SECTION_SHIFT

从proc结构中取出mm_mmuflags标记放到r7中(0xc0e)

__turn_mmu_on_loc:.long..long__turn_mmu_on.long__turn_mmu_on_end

将__trun_mmu_on_loc地址存放到r0中 也就是r0指向这个3个long字节的数据第一个 r0=0x8168

从r0中取出数据来放到r3 r5 r6中 adr是取得运行时的相对地址。此时r0是相对地址


第一个.表示当前位置 也就是__trun_mmu_on_loc的值=r3(0xc0008168),第二个是__turn_mmu_on函数的起始地址=r5(0xc0008200),第三个是__turn_mmu_on函数的结束地址=r6(0xc0008220)这些地址都是虚拟地址,是0XC0000000起头的地址


sub r0,r0,r3  r0=r0-r3  相对的运行地址r0-绝对的0XC000XXX地址r3得到两者之间的偏移

然后 r5和r6这两个绝对地址0XC0000XXX 都加上这个偏移。就得到了当前物理内存中的__turn_mmu_on(0x8200)和__turn_mmu_on_end(0x8220)的地址


然后将r5 r6的物理地址都右移20位,这样r5和r6里面保存的就是__trun_mmu_on和__trun_mmu_on_end的物理基地址的索引。(r5=r6=0)


比如R5=0X123ABCDE。按照1M分段。那么0x123就是[31:20]的基地址。0xABCDE就是段内偏移。

R5右移20位后变成了0X123


1:orrr3, r7, r5, lsl #SECTION_SHIFT@ flags + kernel basestrr3, [r4, r5, lsl #PMD_ORDER]@ identity mappingcmpr5, r6addlor5, r5, #1@ next sectionblo1b

接下来先将 R5左移20位  然后或上r7,也就是0X12300000 or r7,此时r5还是没有变,依然是0X123。也就是描述符标记,形成了一个4字节的页表描述符(0xc0e),放到R3当中。

str r3 ,[r4,r5, lsl #2]等同于r4[r5*4]=r3。也就是将这个描述符放到相应的位置上去。循环直到R6停止。

到这里为止

turn_mmu_on到turn_mmu_end的代码所属的物理位置的页表描述符已经设置好了

这样就做到了虚拟地址和物理地址一一映射。因为要保证执行完turn_mmu_on以后,这部分代码依然是一一映射的,开启完毕以后即使在虚拟地址上也可以执行后续的代码

实际上我的树莓派中R5=0因此就是 r4[0]=0xc0e如下图




接下来是映射内核从开始映射到末尾.bss段

/* * Map our RAM from the start to the end of the kernel .bss section. */addr0, r4, #PAGE_OFFSET >> (SECTION_SHIFT - PMD_ORDER)ldrr6, =(_end - 1)orrr3, r8, r7addr6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)1:strr3, [r0], #1 << PMD_ORDERaddr3, r3, #1 << SECTION_SHIFTcmpr0, r6bls1b


在我的反汇编代码中变成了

.head.text:C000811C                 ADD     R0, R4, #0x3000.head.text:C0008120                 LDR     R6, =0xC08F269F.head.text:C0008124                 ORR     R3, R8, R7.head.text:C0008128                 ADD     R6, R4, R6,LSR#18.head.text:C000812C.head.text:C000812C loc_C000812C                            ; CODE XREF: __create_page_tables+7Cj.head.text:C000812C                 STR     R3, [R0],#4.head.text:C0008130                 ADD     R3, R3, #0x100000.head.text:C0008134                 CMP     R0, R6.head.text:C0008138                 BLS     loc_C000812C

PAGE_OFFSET=0XC0000000 

SECTION_SHIFT=20 

PMD_ORDER=2

首先将PAGE_OFFSET(0xc0008000)右移20-2 ,再加上r4(页表物理基地址)得到内核起始链接地址对应页表项的物理地址,保存到r0中

r0等于0x7000  

r4=0x4000

将.end结束地址放到R6当中

将R7|R8的值保存到 R3中。

然后计算内核结束虚拟地址对应应页表项的物理地址保存到r6中

每一次循环都会将r3增加一个1<<SECTION_SHIFT 的大小,也就是增加1MB将描述符填充到页表对应的位置里面

r4=pagetable=0x4000

为什么要用  addr>>(20-2)+pagetable 得到内核起始地址和结束地址在pagetable对应的页表项地址呢?

这个-2是因为一个页表项占用4字节,也就是左移两位,相当于乘以4

比如要计算0XC0008000 先取得高12bit 0xC00,这个就是索引。访问的时候就是访问pagetable[0XC00]项,每一项四个字节,

pagetable[0xc00]=pagetable+(0xc0008000>>20)*4=pagetable+0xc0008000>>20<<2=pagetable+0xc0008000>>(20-2)=0x4000+0xc0008000>>18

以R3为基准,每次增加1MB大小。

比如 R3=0X00000C0E    pagetable[0xc00]=0X00000C0E

R3=R3+0X100000

R3=0x00100C0E             pagetable[0xc00]=0X00100C0E

R3=R3+0X100000

R3=0x00200C0E             pagetable[0xc00]=0X00200C0E

pagetable[0xc00] 开始处的内存如下图





到这里为止。我们映射了内核代码部分,也映射了turn_mmu_on部分

注意到页表地址0x7000里面是0x00000c0e 前面的0x4000里面也是0x00000c0e。那么就表示,这两个虚拟地址都映射到了同一个物理地址。

因此我们访问0xc0008000 和访问0x8000 访问的是同一块物理地址。


接下来就是映射参数地址了。r2 atag或者DTB

/* * Then map boot params address in r2 if specified. * We map 2 sections in case the ATAGs/DTB crosses a section boundary. */movr0, r2, lsr #SECTION_SHIFTmovsr0, r0, lsl #SECTION_SHIFTsubner3, r0, r8addner3, r3, #PAGE_OFFSETaddner3, r4, r3, lsr #(SECTION_SHIFT - PMD_ORDER)orrner6, r7, r0strner6, [r3], #1 << PMD_ORDERaddner6, r6, #1 << SECTION_SHIFTstrner6, [r3]

首先得到物理地址r2的高12bit。其实就是r0=r2&0xfff00000

如果r0是0,后面就不再映射了,这里我的r0是0,所以后续的映射都没有执行。

因为我的r2是0x100,恰好和内核起始地址0x8000在同一个段内,所以在映射内核的时候就顺带映射了r2。

(疑问?如我我r2=0x100 内核的起始地址在0x100800,不在同一个段内那不是就没有映射了吗?)。

这个疑问暂留。



接下来一般就是return 了。


但是为了我们能够在start_kernel之前实现串口打印。通常可以通过配置CONFIG_DEBUG_LL实现

make menuconfig ---> Kernel hacking ---> 选中:Kernel debugging。

当选中Kernel debugging后,才能看见Kernel low-level debugging functions. 选中即可


所以在return之前可以映射一下串口地址。

/* * Map in IO space for serial debugging. * This allows debug messages to be output * via a serial console before paging_init. */addruart r7, r3, r0movr3, r3, lsr #SECTION_SHIFTmovr3, r3, lsl #PMD_ORDERaddr0, r4, r3movr3, r7, lsr #SECTION_SHIFTldrr7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflagsorrr3, r7, r3, lsl #SECTION_SHIFTorrr3, r3, #PMD_SECT_XNstrr3, [r0], #4

对于我的树莓派addruart的定义是

.macroaddruart, rp, rv, tmpldr\rp, =UART0_BASEldr\rv, =IO_ADDRESS(UART0_BASE).endm
其中addruart实现的宏就是 r7存放UART需要映射的物理地址,R3存放映射后的虚拟地址,r0是一个临时变量,可供自由使用

并且在arm\mach-bcm2708\include\mach\platform.h里面找到了相关的定义

/* macros to get at IO space when running virtually */#define IO_ADDRESS(x)(((x) & 0x0fffffff) + (((x) >> 4) & 0x0f000000) + 0xf0000000)#define BCM2708_PERI_BASE        0x20000000#define UART0_BASE               (BCM2708_PERI_BASE + 0x201000)/* Uart 0 */

BCM2708外设的基地址是 0X20000000, UART0口的基地址是 0X201000

所以只要映射 物理地址r7=0X20201000 到虚拟地址r3=IO_ADDRESS(0x20201000)=(0xF2201000)

IO的映射公式 是先取物理地址paddr的低28bit得到v1 ,取得最高位加到v1的[27:24] bit上,最后加上0xf0000000(映射到其他合适的虚拟地址应该也可以???)

首先r3=r3>>(20-2) 得到描述页表项相对于页表基地址的偏移。

r4是页表基地址 r0=r4+r3, 得到r0就是对应的描述页表项的物理地址。

接下来构造页表描述项

r3=r7>>20

再把物理地址r7右移20位放到R3中;

r7=io_mmu_flags

取出io_mmu_flags到r7中

r3=r3<<20 | r7

再或上另外一个标记PMD_SECT_XN

得到最终的页表项描述符 r3

然后把r3存储到r0地址中.(再把r0自增4 r0=r0+4,这个r0没有必要再+4了。没什么作用)

这样访问F2XXXXX的时候就会访问IO地址0X2XXXXXX了。





0 0
原创粉丝点击