简介
linux内核是通过MMU来管理内存的,MMU其中的一个功能就是把虚拟地址转换为物理地址,但是linux在启动过程中,MMU未打开之前,代码的执行都是在物理地址空间的,那么怎么才能实现物理地址到虚拟地址空间的切换是本文的重点。
swapper_pg_dir定义
#define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET)
#if (KERNEL_RAM_VADDR & 0xffff) != 0x8000
#error KERNEL_RAM_VADDR must start at 0xXXXX8000
#endif
#ifdef CONFIG_ARM_LPAE
/* LPAE requires an additional page for the PGD */
#define PG_DIR_SIZE 0x5000
#define PMD_ORDER 3
#else
#define PG_DIR_SIZE 0x4000
#define PMD_ORDER 2
#endif
.globl swapper_pg_dir
.equ swapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZE
PAGE_OFFSET定义
arch/arm/include/asm/memory.h
/* PAGE_OFFSET - the virtual address of the start of the kernel image */
#define PAGE_OFFSET UL(CONFIG_PAGE_OFFSET)
TEXT_OFFSET定义
arch/arm/Makefile
# The byte offset of the kernel image in RAM from the start of RAM.
TEXT_OFFSET := $(textofs-y)
textofs-y := 0x00008000
因此swapper_pg_dir的地址大小是:0xc0000000 + 0x00008000-0x4000=0xc0004000,从内核编译完成的System.map文件也可以看到swapper_pg_dir的虚拟地址大小:
c0004000 A swapper_pg_dir
c0008000 T _text
c0008000 T stext
head.S汇编分析
- 分配物理地址给r8寄存器
#ifndef CONFIG_XIP_KERNEL
(1) adr r3, 2f
(2) ldmia r3, {r4, r8}
(3) sub r4, r3, r4 @ (PHYS_OFFSET - PAGE_OFFSET)
(4) add r8, r8, r4 @ PHYS_OFFSET
#else
ldr r8, =PLAT_PHYS_OFFSET @ always constant in this case
#endif
#ifndef CONFIG_XIP_KERNEL
2: .long .
.long PAGE_OFFSET
#endif
物理地址PHYS_OFFSET的定义如下:
arch/arm/include/asm/memory.h
#define PLAT_PHYS_OFFSET UL(CONFIG_PHYS_OFFSET)
对于没有定义CONFIG_XIP_KERNEL宏的平台来说,找到物理地址还是有点困难的。
adr汇编指令是相对寻址,与当前位置无关
(1)表示将标号2的物理地址赋值给r3,当前运行的是物理地址,adr汇编指令会被解析为pc+标号2的偏移地址
(2)将当前虚拟地址也就是绝对地址赋值给r4,将PAGE_OFFSET赋值给r8
(3)表示链接地址和物理地址的偏差
(4)得到实际的物理起始地址
- 调用 bl __create_page_tables
bl __create_page_tables
__create_page_tables:
pgtbl r4, r8 @ page table address
/*参见代码详解1*/
/*
* 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
/*参见代码详解2*/
ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
/*参见代码详解3*/
/*
* 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
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
/*参见代码详解4*/
/*
* 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
/*参见代码详解5*/
(1)代码详解1
pgtbl r4, r8 @ page table address
.macro pgtbl, rd, phys
add \rd, \phys, #TEXT_OFFSET
sub \rd, \rd, #PG_DIR_SIZE
.endm
pgtbl 宏用于通过DRAM物理地址来获取页表的物理地址。 前面我们已经知道r8用于存放DRAM的起始物理地址,r4则是要存放计算得到的页表物理地址。
kernel起始地址=DRAM起始物理地址+TEXT_OFFSET
内核页表地址=kernel起始地址-PG_DIR_SIZE
(2)代码详解2
为临时内核页表分配空间之后,接下来的任务就是清空临时内核页表分配空间。
/*
* Clear the swapper page table
*/
mov r0, r4
@ 将页表物理地址放到r0上
mov r3, #0
@ 把0赋值给r3寄存器
add r6, r0, #PG_DIR_SIZE
@ 把r0+PG_DIR_SIZE的值赋给r6,也就是说临时内核页表的末尾物理地址放到r6上
1: str r3, [r0], #4
@ 把0值写入以r0为地址的存储器中,并将新地址r0+4写入到r0中,接下来的语句表示:从r0(临时内核页表物理地址)指向的寄存器上开始写入0值,每16个字节一个循环
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r6
@ 比较是否已经写到了r6(临时内核页表的末尾物理地址)上
bne 1b
@ 如果还没有写完,跳转到标号1,进入下一个循环
(3)代码详解3
设置MMU的标识并存放到r7寄存器中,后续需要写入到临时内核页表的页表项中。
(4)代码详解4
开始进行映射表的创建,首先是创建恒等映射,就是将物理地址相应到相同的虚拟地址上。其实这个恒等映射仅映射__enable_mmu功能函数区的页表 ,以保证在启用mmu时代码的正确执行–1:1映射(物理地址=虚拟地址)。
首先看一下__turn_mmu_on_loc的定义
__turn_mmu_on_loc:
.long .
.long __turn_mmu_on
.long __turn_mmu_on_end
__turn_mmu_on和__turn_mmu_on_end标识了打开MMU的起始代码地址和结束代码地址,kernel将这些链接地址存放到了__turn_mmu_on_loc中。
adr r0, __turn_mmu_on_loc
@当前代码是运行在物理地址空间上,将pc指针+__turn_mmu_on_loc的偏移量赋值给r0,也就是说r0保存的是__turn_mmu_on_loc物理地址
ldmia r0, {r3, r5, r6}
@将r0地址的内容依次赋值给r3,r5,r6,也就是说r3指向的是__turn_mmu_on_loc虚拟地址,r5指向__turn_mmu_on的虚拟地址,r6指向__turn_mmu_on_end的虚拟地址
sub r0, r0, r3 @ virt->phys offset
@得到虚拟地址到物理地址的偏移
add r5, r5, r0 @ phys __turn_mmu_on
@r5保存__turn_mmu_on物理地址
add r6, r6, r0 @ phys __turn_mmu_on_end
@r6保存__turn_mmu_on_end物理地址
mov r5, r5, lsr #SECTION_SHIFT
mov r6, r6, lsr #SECTION_SHIFT
@以上两条语句是分别把r5和r6右移20位,也就是1M,分别得到r5和r6的段序号;
@arm打开MMU初期,使用的是临时内核页表,其类型就是段式页表。段式页表将4GB的地址空间(32bit系统)划分成4096个1MB的段,因此段式页表有4096个页表项,每个页表项有32bit(4 byte),故段式页表需要16KB的空间。
1: orr r3, r7, r5, lsl #SECTION_SHIFT @ flags + kernel base
@页表项内容为段序号(r5)左移SECTION_SHIFT后或上MMU标识(r7),存放在r3上,这样就填写对应段的段页表项的内容了
str r3, [r4, r5, lsl #PMD_ORDER] @ identity mapping
@将段页表项值(r3)写入到对应的段页表项中
@ 段页表项的地址=段页表起始地址(r4)+段序号(r5)*段页表项的大小,PMD_ORDER大小是4
cmp r5, r6
addlo r5, r5, #1 @ next section
blo 1b
@判断是否已经写到__turn_mmu_on结束地址的对应的段页表项中,如果没有的话,继续写入下一个段,跳转到1标识符。
(5)映射kernel的内核空间
通过System.map中可以看出kernel的连接区域如下:
c0008000 T _text
c0a58438 B _end
代码分析:
/*
* Map our RAM from the start to the end of the kernel .bss section.
*/
add r0, r4, #PAGE_OFFSET >> (SECTION_SHIFT - PMD_ORDER)
@ PAGE_OFFSET表示内核空间的起始地址,如果用户空间和内核空间按照3:1划分,那么该参数是0xc0000000
@ 将PAGE_OFFSET右移(SECTION_SHIFT - PMD_ORDER)后得到0xc0000000所在段的段页表项的地址偏移,其实也就是先右移SECTION_SHIFT得到段序号,然后再左移PMD_ORDER得到段页表项偏移
@ 将段页表项的地址偏移+临时内核页表地址(r4)得到0xc0000000所在段的段页表项的物理地址,然后将这个物理地址赋值给r0
ldr r6, =(_end - 1)
@ 将内核映射区的结束地址存入r6寄存器中。
orr r3, r8, r7
@将r8寄存器或上r7,赋值给r3,其中r8寄存器是存放DRAM的起始物理地址,r7是mmu标识
add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)
@将内核映射区的结束地址左移(SECTION_SHIFT - PMD_ORDER)得到段页表项的地址偏移,然后再加上临时内核页表项地址r4,最后赋值给r6;
1: str r3, [r0], #1 << PMD_ORDER
@将r3的内容存放到r0地址存储器中,也就是内核起始地址0xc0000000所在段的页表项物理地址,然后更新r0的地址:r0=r0+4
add r3, r3, #1 << SECTION_SHIFT
@将r3的值加上1左移SECTION_SHIFT,得到最新的r3值,也就是下一个段页表项值
cmp r0, r6
@判断r0和r6是否相等,也就是说判断是否已经是内核映射区的结束段了。
bls 1b
@如果不是,跳转到1标号,继续进行内核映射
(6)创建DTB映射区
uboot引导内核的时候会给传递参数,其中r2寄存器存放的就是dtb的地址,所以创建DTB映射区,主要是从r2中提取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
@将r2右移SECTION_SHIFT,把低位清0,赋值给r0
movs r0, r0, lsl #SECTION_SHIFT
@将r0值再左移SECTION_SHIFT,也就是说得到这个物理内存段的起始地址
subne r3, r0, r8
@得到r0相对于r8(物理起始地址的偏移,然后存放在r3中
addne r3, r3, #PAGE_OFFSET
@将r3的值加上PAGE_OFFSET,物理地址转虚拟地址,也就是说得到了DTB所在物理段对应的虚拟地址
addne r3, r4, r3, lsr #(SECTION_SHIFT - PMD_ORDER)
@得到映射的虚拟地址的段的页表项的地址,存放在r3中,不懂的话可以参考前文描述
orrne r6, r7, r0
@将物理内存段地址(r0)或上mmu标识(r7),得到对应页表项值,存放到r6中
strne r6, [r3], #1 << PMD_ORDER
@将页表项值(r6)写入到页表项中[r3]存储器中,然后更新r3值:r3+4,获取到下一个页表项的地址
addne r6, r6, #1 << SECTION_SHIFT
@页表项值+0x100000,得到下一个应该写入的页表项值
strne r6, [r3]
@将页表项值(r6)写入到页表项中([r3])
综上,临时内核页表就创建完成了。