对于mmu的作用,参看第一篇的介绍
这里讲linux kenel的mmu和页表
部分内容图片参考 http://blog.csdn.net/luckyapple1028/article/details/45287617 非常不错的blog
对于1M段大小的虚拟地址和物理地址转换,arm1176计算方式如下
-
虚拟地址的[31:20]位存放一级页表的入口index,[19:0]位存放段偏移;
-
从TTBR(translation table base register,协处理器CP15中的一个寄存器,用于存放一级页表的基址)寄存器中获取一级页表的基址;
-
一级页表基址+ VA[31:20] = 该虚拟地址对应的页表描述符的入口地址;
-
页表描述符的[31:20]位为该虚拟地址对应的物理段基址;
-
物理段基址+ 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:
pgtbl r4, r8 @ page table address
/*
* Clear the swapper page table
*/
mov r0, r4
mov r3, #0
add r6, r0, #PG_DIR_SIZE
1: str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r6
bne 1b
其中pgtbl
.macro pgtbl, rd, phys
add \rd, \phys, #TEXT_OFFSET
sub \rd, \rd, #PG_DIR_SIZE
.endm
这里明确说明建立的页表只是为了kernel能够运行,因此只映射kernel code.
输入的时候 r8是物理地址偏移(0) r9是cpuid r10是procinfo
返回值r4是页表基地址(0x4000)
下面来看这个操作过程
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
ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
/*
* Create identity mapping to cater for __enable_mmu.
* This identity mapping will be removed by paging_init().
*/
adr r0, __turn_mmu_on_loc
ldmia r0, {r3, r5, r6}
sub r0, r0, r3 @ virt->phys offset
add r5, r5, r0 @ phys __turn_mmu_on
add r6, r6, r0 @ phys __turn_mmu_on_end
mov r5, r5, lsr #SECTION_SHIFT
mov r6, 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: orr r3, r7, r5, lsl #SECTION_SHIFT @ flags + kernel base
str r3, [r4, r5, lsl #PMD_ORDER] @ identity mapping
cmp r5, r6
addlo r5, r5, #1 @ next section
blo 1b
接下来先将 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.
*/
add r0, r4, #PAGE_OFFSET >> (SECTION_SHIFT - PMD_ORDER)
ldr r6, =(_end - 1)
orr r3, r8, r7
add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)
1: str r3, [r0], #1 << PMD_ORDER
add r3, r3, #1 << SECTION_SHIFT
cmp r0, r6
bls 1b
在我的反汇编代码中变成了
.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.
*/
mov r0, r2, lsr #SECTION_SHIFT
movs r0, r0, lsl #SECTION_SHIFT
subne r3, r0, r8
addne r3, r3, #PAGE_OFFSET
addne r3, r4, r3, lsr #(SECTION_SHIFT - PMD_ORDER)
orrne r6, r7, r0
strne r6, [r3], #1 << PMD_ORDER
addne r6, r6, #1 << SECTION_SHIFT
strne r6, [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, r0
mov r3, r3, lsr #SECTION_SHIFT
mov r3, r3, lsl #PMD_ORDER
add r0, r4, r3
mov r3, r7, lsr #SECTION_SHIFT
ldr r7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflags
orr r3, r7, r3, lsl #SECTION_SHIFT
orr r3, r3, #PMD_SECT_XN
str r3, [r0], #4
对于我的树莓派addruart的定义是
.macro addruart, rp, rv, tmp
ldr \rp, =UART0_BASE
ldr \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了。