主要完成的映射关系:
1.将虚拟地址空间按照“1:1”对等映射到内核映像的第一个1M处
作用:在启动MMU之前只能使用实地址模式运行
2.将整个内核地址空间直接映射区的代码那部分映射到SDRAM上
3.将内核地址空间的开始的1M映射到SDRAM开始的第一个1M空间,因为那里存放了内核启动参数
看图意会时间:
ARM920T内置的MMU地址转换方式:这个建议去看看ARM920T的使用手册,毕竟那才是原汁原味的
一级描述符的格式:这里主要涉及的是一级段式页表描述符
具体使用一级段式页表将MVA转换成PA的过程
代码时间到了
第一段注释:临时页表采用的是段式页表
分析linux-2.6.30.4/arch/arm/kernel/head.S的__create_page_tables汇编代码:
/*
* 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 = machinfo
* r9 = cpuid
* r10 = procinfo
*
* Returns:
* r0, r3, r6, r7 corrupted
* r4 = physical page table address:r4=页表的基地址,r4的值还是在内核自解压工作的时候赋给的:内核解压后映像的起始地址0x30008000
*/
__create_page_tables:
pgtbl r4 @ page table address
//pgtbl是一个汇编宏定义 .macro pgtbl, rd,通过这个宏将r4(0x30004000)设置成页表的物理基地址,往后r4值一直没有变
/*
* Clear the 16K level 1 swapper page table 清零一级交换页表16K
* 页表将4GB的地址空间分成若干个1MB的段(section),因此页表包含4096个页表项(section entry)。每个页表项是32bits(4 bytes),因而页表占用4096*4=16k的内存空间。
*/
mov r0, r4
mov r3, #0
add r6, r0, #0x4000
1: str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r6
bne 1b
ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags 获得proc_info_list的__cpu_mm_mmu_flags的值,并存储到r7中,看第二段注释
//r7 = __cpu_mm_mmu_flags = 0x00000C1E
/*
* Create identity mapping for first MB of kernel to
* cater for the MMU enable. This identity mapping
* will be removed by paging_init(). We use our current program
* counter to determine corresponding section base address.
* 接下来是为内核空间的直接映射区第一个1MB建立页表,这都是为之后MMU的启动,之后会被paging_init()函数移除销毁
* 我们通过当前的程序计数寄存器PC值来获得段基址
*/
mov r6, pc, lsr #20 @ start of kernel section r6 = 0x300
orr r3, r7, r6, lsl #20 @ flags + kernel base(PA) r3 = (r7) | ((r6)<<20) r3里边放的内容就是一级描述符
str r3, [r4, r6, lsl #2] @ identity mapping [0x30004000+0x300<<2] = r3 将一级描述符放到页目录中
@不过这里为什么要左移两位(乘于4)???为什么又是存放到这个地址处
@想到了:r4里边是ttb基址(启动MMU时会被写到cp15的c2寄存器),r6里边的是ttb表中对应1M的索引值,每个描述符是4字节
@实际上只是将pc的值右移了18位
@上面这一段代码实现了VA=PA(1:1)的映射:根据ARM920T的MMU一级地址转换,4G的虚拟地址空间被分成4096个条目(描述符,每个4字节),因此每一个条目对应1M=4G/4096的地址空间映射。
@到这里第一个映射关系建立完成
/*
* Now setup the pagetables for our kernel direct
* mapped region.
* 现在为了内核直接映射区来设置页表
* 即为kernel镜像所占有空间,即KERNL_START到KERNEL_END建立内存映射
* 由于这块内核虚拟空间要映射到SDRAM中内核映像0x30008000开始处,所以第一个1M的描述符(保存到r3寄存器中)和上面的是样的
* 这里是将整个内核空间的直接映射区全部映射完毕--以段的方式(1M)
*/
add r0, r4, #(KERNEL_START & 0xff000000) >> 18 @取内核空间地址(VA)高8位,arm的立即数只能是8位
str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]! @再取内核空间地址(VA)高8位往后4位,和上一句加起来共12位,“!”将地址写到r0
ldr r6, =(KERNEL_END - 1) @r4=ttb基址 r0=ttb中kernel第1M描述符检索值
add r0, r0, #4
add r6, r4, r6, lsr #18 @r6= 用来标识ttb中内核地址空间结束的检索地址
1: cmp r0, r6
add r3, r3, #1 << 20 ;将描述符的段偏移地址加1,即物理段基地址加1
strls r3, [r0], #4 ;表示小于等于,即只要r0<=r6,strls就会执行
bls 1b
#ifdef CONFIG_XIP_KERNEL ;没有定义CONFIG_XIP_KERNEL,注释掉这个条件分支,XIP技术就是内核代码可以不用拷贝到SDRAM而立地执行
.......
#endif
/*
* Then map first 1MB of ram in case it contains our boot params.
* 接下来建立内核临时页表的最后一个页表描述符,即SDRAM开始1M的地址空间,那里保存了uboot传递给内核的启动参数
*/
add r0, r4, #PAGE_OFFSET >> 18 ;PAGE_OFFSET被定义为SDRAM起始地址0x30000000
orr r6, r7, #(PHYS_OFFSET & 0xff000000) ;r6 = PA section base addr + flags
.if (PHYS_OFFSET & 0x00f00000)
orr r6, r6, #(PHYS_OFFSET & 0x00f00000)
.endif
str r6, [r0] ;将描述符写入到对应的页表位置,第三个页表描述符建立完成
;........中间省略一段代码,因为条件编译不成立,相当于注释掉...............
mov pc, lr ; 从__create_page_tables返回
ENDPROC(__create_page_tables)
#include "head-common.S"
第二段注释:
在arch/arm/include/asm/procinfo.h头文件定义处理器相关信息数据结构
struct proc_info_list {
unsigned int cpu_val;
unsigned int cpu_mask;
unsigned long __cpu_mm_mmu_flags; /* used by head.S */这个值在哪里被赋值?往下看
unsigned long __cpu_io_mmu_flags; /* used by head.S */
unsigned long __cpu_flush; /* used by head.S */
const char *arch_name;
const char *elf_name;
unsigned int elf_hwcap;
const char *cpu_name;
struct processor *proc;
struct cpu_tlb_fns *tlb;
struct cpu_user_fns *user;
struct cpu_cache_fns *cache;
};
在文件arch/arm/kernel/vmlinux.lds.S中定义了一个“.proc.info.init”段属性
__proc_info_begin = .;
*(.proc.info.init)
__proc_info_end = .;
因此,以关键字“.proc.info.init”在工程里面搜索,找到arch/arm/mm/proc-arm920.S文件,第三行的定义就是对__cpu_mm_mmu_flags“映射属性标识”的赋值
__arm920_proc_info:
.long 0x41009200
.long 0xff00fff0
.long PMD_TYPE_SECT | \
PMD_SECT_BUFFERABLE | \
PMD_SECT_CACHEABLE | \
PMD_BIT4 | \
PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ
.long PMD_TYPE_SECT | \
PMD_BIT4 | \
PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ
b __arm920_setup
.long cpu_arch_name
.long cpu_elf_name
.long HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB
.long cpu_arm920_name
.long arm920_processor_functions
.long v4wbi_tlb_fns
.long v4wb_user_fns
#ifndef CONFIG_CPU_DCACHE_WRITETHROUGH
.long arm920_cache_fns
#else
.long v4wt_cache_fns
#endif
.size __arm920_proc_info, . - __arm920_proc_info
在arch/arm/include/asm/pgtable-hwdef.h找到上面用到的宏的定义:
#define PMD_TYPE_SECT (2 << 0) 采用段式地址映射
#define PMD_SECT_BUFFERABLE (1 << 2)
#define PMD_SECT_CACHEABLE (1 << 3)
#define PMD_BIT4 (1 << 4)
#define PMD_SECT_AP_WRITE (1 << 10)
#define PMD_SECT_AP_READ (1 << 11)
.long是分配长整型长度的空间,在arm9处理器占4个字节(32位)
按位或之后:__cpu_mm_mmu_flags = 0x00000C1E
从临时页表创建原理到具体的代码实现过程可以说到这里我已经消化了。突然又想到一个问题:三张页表描述符创建好了,MMU也启动了,cpu执行过程中究竟是怎么知道什么时候使用对等映射描述表符,什么时候使用内核空间与内核映像的那个描述符,又是什么时候要知道切换使用启动参数那块的映射描述符???
仔细想想就可以知道了,我们知道ttb的“选择子”就是MVA(这里就相当于VA)的高12位,这12位的变化就是cpu自动选择ttb中哪一个描述符的根本原因。arm9从cpu上电开始工作到在SDRAM取指执行时(MMU未开启)PC指针都是0x3打头的,在开始真正运行内核那段head.S代码并且C语言运行环境未设置好时,必须使用位置无关码来实现汇编函数跳转,也就是说这就是为了保持PC是0x3打头的,这样一来,即使之后启动了MMU,我的PC保持原有作风就可以让我的“选择子”等于“0x300”选中1:1的对等映射描述符表项。同理只要往后pc的值不再是0x3打头就会选中0xC打头的描述符表项,具体选中哪一个就看0xCxx了。
内核临时页表的创建过程分析完毕,linux之路又前进了一小步,加油!