mmu以及页表 linuxkernel(2)

对于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:
	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)


下面来看这个操作过程


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


	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了。





  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值