下面我们需要做的是创建页表了
通过前面的两步,我们已经确定了processor type 和 machine type.
此时,一些特定寄存器的值如下所示:
r8 = machine info (struct machine_desc的基地址)
r9 = cpu id (通过cp15协处理器获得的cpu id)
r10 = procinfo (struct proc_info_list的基地址)
这里,我们使用的是arm的L1主页表,L1主页表也称为段页表(section page table)L1 主页表
将4 GB 的地址空间分成若干个1 MB的段(section),因此L1页表包含4096个页表项(section entry).
每个页表项是32 bits(4 bytes)因而L1主页表占用 4096 *4 = 16k的内存空间.
L1的格式为:
31 20 19 12 11 109 8 5 4 3 2 1 0
+------------------------------+------------+-----+-+-----------+-+-+-+-+-+
| | | | | | | | | | |
| Base Address | SBZ | AP |0 | Domain |1 |C|B |1|0 |
| | | | | | | | | | |
+------------------------------+------------+-----+-+-----------+-+-+-+-+-+
B - Write Buffer Bit C - Cache Bit
+------------------------------------------------------------------+
| Data Cache |
+--------------+---------------+--------------------------------+
| Cache Bit | Buffer Bit | Page attribute |
+--------------+---------------+--------------------------------+
| 0 | 0 | not cached, not buffered |
+--------------+---------------+--------------------------------+
| 0 | 1 | not cached, buffered |
+--------------+---------------+--------------------------------+
| 1 | 0 | cached, writethrough |
+--------------+---------------+--------------------------------+
| 1 | 1 | cached, writeback |
+--------------+---------------+--------------------------------+
* r8 = machinfo
* r9 = cpuid
* r10 = procinfo
* Returns:
* r0, r3, r6, r7 corrupted
* r4 = physical page table address
__create_page_tables:
pgtbl r4 @ page table address
.macro pgtbl, rd
ldr \rd, =(KERNEL_RAM_PADDR - 0x4000)
.endm
KERNEL_RAM_PADDR在本文件的前面有定义,为(PHYS_OFFSET + TEXT_OFFSET)
PHYS_OFFSET在arch/arm/mach-s3c2410/include/mach/memory.h定义,为UL(0x30000000)
而TEXT_OFFSET在arch/arm/Makefile中定义,为内核镜像在内存中到内存开始位置的偏移为(0x00008000)。
r4 = 30004000为临时页表的起始地址首先即是初始化16K的页表,高12位虚拟地址为页表索引,
所以为4K*4 = 16K,大页表,每一个页表项,映射1MB虚拟地址。
* Clear the 16K level 1 swapper page table
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
在arch/arm/kernel/asm-offset.c中PROCINFO_MM_MMUFLAGS为
DEFINE(PROCINFO_MM_MMUFLAGS, offsetof(struct proc_info_list, __cpu_mm_mmu_flags));
R10寄存器保存的指针指向是我们前面找到的proc_info_list结构嘛。
/*
* 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.
*/
mov r6, pc
mov r6, r6, lsr #20 @ start of kernel section
orr r3, r7, r6, lsl #20 @ flags + kernel base
str r3, [r4, r6, lsl #2] @ identity mapping
上面这三行,设置了kernel的第一个section(物理地址所在的page entry)的页表项
1) 通过pc值的高12位(右移20位),得到kernel的section,并存储到r6中.因为当前是通
过运行时地址得到的kernel的section,因而是物理地址.
2) r3 = r7 | (r6 << 20); flags + kernel base,得到页表中需要设置的值.
3) 设置页表: mem[r4 + r6 * 4] = r3 这里,因为页表的每一项是32 bits(4 bytes),
所以要乘以4(<<2).因为每个section描述符占4个字节,这里把起始section数乘以4,
加上 r4中的页表起始地址,找到对应的页表入口。然后写入描述符。目的是使得MMU开
启后,pc 在未转换到虚地址0xc0008000的空间中之前,还能够继续映射原空间
/* 结果:
* 虚拟地址 物理地址
* 0x300000000 0x30000000。
* Now setup the pagetables for our kernel direct
* mapped region.为内核占用的头4M地址准备好转换表(在这采用的就不是平板地址映射模式了)
add r0, r4, #(KERNEL_START & 0xff000000) >> 18
str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
KERNEL_START在文件的前面定义,为KERNEL_RAM_VADDR,即内核的虚拟地址。
而KERNEL_RAM_VADDR在文件的前面定义为(PAGE_OFFSET + TEXT_OFFSET)映射完整
的内核代码段,初始化数据段。PAGE_OFFSET为内核镜像开始的虚拟地址,
在arch/arm/include/asm/memory.h中定义。在配置内核时选定具体值,默认为0xC0000000。
因为最高12位的值是页表中的偏移地址,而第三高的四位必然为0,每个页表项为4字节,右移20位
之后,还得再左移两位回来,所以,这里只是左移18位。R3寄存器在经过了上面的操作之后,实际
上是变成了指向内核镜像代码段的指针(物理地址),在这个地方,再一次为内核镜像的第一个MB做了映射。R6随后指向了内核镜像的尾部。R0为页表项指针。这里以1MB为单位来映射内核镜像。
TEXTADDR是内核起始虚拟地址(c0008000),(TEXTADDR & 0xff000000) >> 18和
(TEXTADDR & 0x00f00000) >> 18获得了虚拟地址的高14位,这14位中最低两位为0,4字节
对齐,和ttb中页表基址一起索引到页表中的一个位置,然后将页描述符写入页表,这个
页描述符和上一个是一样的,这样在这第一个1MB空间内不管cpu发出的是虚拟地址还是
物理地址,取的都是同一个存储单元的数据。这就解决了mmu打开是pc中存的还是没打开前的
物理地址的问题。
ldr r6, =(KERNEL_END - 1)
add r0, r0, #4
add r6, r4, r6, lsr #18 @得到页表的结束物理地址
1:
cmp r0, r6
add r3, r3, #1 << 20
strls r3, [r0], #4
bls 1b
* 结果:
* 虚拟地址 物理地址
0xc0000000 0x30000000
0xc0100000 0x30100000
0xc0200000 0x30200000
0xc0300000 0x30300000
* Then map first 1MB of ram in case it contains our boot params.
为了使用启动参数,将物理内存的第一MB映射到内核虚拟地址空间的第一个MB,r4存放的
是页表的地址。这里的PAGE_OFFSET的虚拟地址比上面的KERNEL_START要小0x8000
add r0, r4, #PAGE_OFFSET >> 18
orr r6, r7, #(PHYS_OFFSET & 0xff000000)
.if (PHYS_OFFSET & 0x00f00000)
orr r6, r6, #(PHYS_OFFSET & 0x00f00000)
.endif
str r6, [r0]
映射sdram的头1M以防boot params需要
ldr r7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflags
* Map in IO space for serial debugging.
* This allows debug messages to be output
* via a serial console before paging_init.
ldr r3, [r8, #MACHINFO_PGOFFIO]
add r0, r4, r3
rsb r3, r3, #0x4000 @ PTRS_PER_PGD*sizeof(long)
cmp r3, #0x0800 @ limit to 512MB
movhi r3, #0x0800
add r6, r0, r3
ldr r3, [r8, #MACHINFO_PHYSIO]
orr r3, r3, r7
1:
str r3, [r0], #4
add r3, r3, #1 << 20
teq r0, r6
bne 1b
mov pc, lr
ENDPROC(__create_page_tables)
总结一下,这个建立临时页表的过程:
1、为内核镜像的第一个MB建立直接映射
2、为内核镜像完整的建立从虚拟地址到物理地址的映射
3、为物理内存的第一个MB建立到内核的虚拟地址空间的第一个MB的映射。
OK,内核的临时页表建立完毕。整个初始化临时页表的过程都没有修改R8,R9和R10。
回到stext:
==> ldr r13, __switch_data @ address to jump to after
@ mmu has been enabled
这个地方实际上是在r13中保存了另一个例程的地址。后面的分析中,遇到执行到这个例程
的情况时会有详细说明。
==> adr lr, BSYM(__enable_mmu) @ return (PIC) address
BSYM()是一个宏,在文件arch/arm/include/asm/unified.h中定义,
为:
#define BSYM(sym) sym
也就是说这个语句也仅仅是把__enable_mmu例程的地址加载进lr寄存器中。
为了方便之后调用的函数返回时,直接执行__enable_mmu例程。
==> add pc, r10, #PROCINFO_INITFUNC
R10中保存的是procinfo结构的地址。PROCINFO_INITFUNC符号在
arch/arm/kernel/asm-offsets.c文件中定义,为:
DEFINE(PROCINFO_INITFUNC, offsetof(struct proc_info_list, __cpu_flush));
也就是调用结构体proc_info_list的__cpu_flush成员函数。回去查看
arch/arm/mm/proc-arm920.S文件中struct proc_info_list结构体的变量的定义,
可以看到这个成员为:
b __arm920_setup
也就是说,在设置好内核临时页表之后调用了例程__arm920_setup,
这个例程同样在arch/arm/mm/proc-arm920.S中:
__arm920_setup:
mov r0, #0
mcr p15, 0, r0, c7, c7 @ invalidate I,D caches on v4
mcr p15, 0, r0, c7, c10, 4 @ drain write buffer on v4
#ifdef CONFIG_MMU
mcr p15, 0, r0, c8, c7 @ invalidate I,D TLBs on v4
#endif
adr r5, arm920_crval
ldmia r5, {r5, r6}
mrc p15, 0, r0, c1, c0 @ get control register v4
bic r0, r0, r5
orr r0, r0, r6
mov pc, lr
这一段首先使i,d caches内容无效,然后清除write buffer,接着使TLB内容无效。接下来
加载变量arm920_crval的地址,我们看到arm920_crval变量的内容为:
rm920_crval:
crval clear=0x00003f3f, mmuset=0x00003135, ucset=0x00001130
crval为一个宏,在arch/arm/mm/proc-macros.S中定义:
.macro crval, clear, mmuset, ucset
#ifdef CONFIG_MMU
.word \clear
.word \mmuset
#else
.word \clear
.word \ucset
#endif
.endm
其实也就是定义两个变量而已。之后,在r0中,得到了我们想要往协处理器相应寄存器中
写入的内容。之后的 __arm920_setup返回,mov pc, lr,即是调用例程__enable_mmu,
这个例程在文件arch/arm/kernel/head.S中:
__enable_mmu:
#ifdef CONFIG_ALIGNMENT_TRAP
orr r0, r0, #CR_A
#else
bic r0, r0, #CR_A
#endif
#ifdef CONFIG_CPU_DCACHE_DISABLE
bic r0, r0, #CR_C
#endif
#ifdef CONFIG_CPU_BPREDICT_DISABLE
bic r0, r0, #CR_Z
#endif
#ifdef CONFIG_CPU_ICACHE_DISABLE
bic r0, r0, #CR_I
#endif
mov r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \
domain_val(DOMAIN_IO, DOMAIN_CLIENT))
mcr p15, 0, r5, c3, c0, 0 @ load domain access register
mcr p15, 0, r4, c2, c0, 0 @ load page table pointer
b __turn_mmu_on
在这儿设置了页目录地址(r4寄存器中保存),然后设置domain的保护,在前面建立
页表的例程中,注意到,页表项的控制信息,是从struct proc_info_list结构体的某字段中取的,
其页目录项的 domain都是0,domain寄存器中的domain 0对应的是0b11,表示访问模式为
manager,不受限制。在这里同时也完成r0的某些位的进一步设置。
然后,__enable_mmu例程又调用了__turn_mmu_on,在同一个文件中定义:
__turn_mmu_on:
mov r0, r0
mcr p15, 0, r0, c1, c0, 0 @ write control reg
mrc p15, 0, r3, c0, c0, 0 @ read id reg
mov r3, r3
mov r3, r13
mov pc, r3
ENDPROC(__turn_mmu_on)
接下来写控制寄存器:
mcr p15, 0, r0, c1, c0 ,0
一切设置就此生效,到此算是完成了打开d,icache和mmu的工作。
注意:
arm的d cache必须和mmu一起打开,而i cache可以单独打开。其实,cache和mmu的
关系实在是紧密,每一个页表项都有标志标示是否是cacheable的,可以说本来就是设计一起
使用的.前面有提到过,r13中存放的其实是另外一个例程的地址,其值是变量__switch_data的
第一个字段,即一个函数指针的值,__switch_data变量是在arch/arm/kernel/head-common.S
中定义的:
__switch_data:
.long __mmap_switched
.long __data_loc @ r4
.long _data @ r5
.long __bss_start @ r6
.long _end @ r7
.long processor_id @ r4
.long __machine_arch_type @ r5
.long __atags_pointer @ r6
.long cr_alignment @ r7
.long init_thread_union + THREAD_START_SP @ sp
前面的ldr r13 __switch_data,实际上也就是加载符号__mmap_switched的地址,
实际上__mmap_switched是一个arch/arm/kernel/head-common.S中定义的例程。
接着来看这个例程的定义,在arch/arm/kernel/head-common.S文件中:
__mmap_switched:
adr r3, __switch_data + 4
ldmia r3!, {r4, r5, r6, r7}
cmp r4, r5 @ Copy data segment if needed
1:
cmpne r5, r6
ldrne fp, [r4], #4
strne fp, [r5], #4
bne 1b
mov fp, #0 @ Clear BSS (and zero fp)
1:
cmp r6, r7
strcc fp, [r6],#4
bcc 1b
ldmia r3, {r4, r5, r6, r7, sp}
str r9, [r4] @ Save processor ID
str r1, [r5] @ Save machine type
str r2, [r6] @ Save atags pointer
bic r4, r0, #CR_A @ Clear 'A' bit
stmia r7, {r0, r4} @ Save control register values
b start_kernel
ENDPROC(__mmap_switched)
这个例程完成如下工作:
1、使r3指向__switch_data变量的第二个字段(从1开始计数)。
2、执行了一条加载指令,也就是在r4, r5, r6, r7寄存器中分别加载4个符号__data_loc,_data,
__bss_start ,_end的地址,这四个符号都是在链接脚本arch/arm/kernel/vmlinux.lds.S中
出现的,标识了镜像各个段的地址,我们应该不难猜出他们所代表的段。
3、如果需要的话则复制数据段(数据段和BSS段是紧邻的)。
4、初始化BSS段,全部清零,BSS是未初始化的全局变量区域。
5、又看到一条加载指令,同样在一组寄存器中加载借个符号的地址,r4中为processor_id,r5
中为__machine_arch_type, r6中为__atags_pointer, r7中为cr_alignment ,sp中为
init_thread_union + THREAD_START_SP。
6、接着我们看到下面的几条语句,则是用前面获取的信息来初始化那些全局变量r9,机器号被
保存到processor_id处;r1寄存器的值,机器号,被保存到变量__machine_arch_type中,
其他的也一样。
7、重新设置堆栈指针,指向init_task的堆栈。init_task是系统的第一个任务,init_task的堆栈
在task structure的后8K,我们后面会看到。
8、最后就要跳到C代码的 start_kernel。
b start_kernel
到此为止,汇编部分的初始化代码就结束了
现在让我们来回忆一下目前的系统状态:
临时页表已经建立,在0X30004000处,映射了映像文件大小空间,虚地址0XC000000被映
射到0X30000000。CACHE,MMU 都已经打开。堆栈用的是任务init_task的堆栈。
阅读(815) | 评论(0) | 转发(0) |