MACHINE_START(MINI2440, "MINI2440")
/* Maintainer: Michel Pollet */
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,
.map_io = mini2440_map_io,
.init_machine = mini2440_init,
.init_irq = s3c24xx_init_irq,
.timer = &s3c24xx_timer,
MACHINE_END
OK, __lookup_machine_type这个例程的我们也搞明白了。回忆一下,启动代码现在已经完成的工作,R10寄存器中为指向proc_info_list结构体的指针(物理地址空间),这个结构体包含有关于我们的处理器的一些重要信息。R8寄存器中为指向一个与我们的平台相匹配的machine_desc结构体的指针,这个结构体中保存有一些关于我们的平台的重要信息。
回来接着看arch/arm/kernel/head.S文件中的stext:
bl __vet_atags
这个例程同样同样也是在arch/arm/kernel/head-common.S文件中定义:
__vet_atags:
tst r2, #0x3 @ aligned?
bne 1f
ldr r5, [r2, #0] @ is first tag ATAG_CORE?
cmp r5, #ATAG_CORE_SIZE
cmpne r5, #ATAG_CORE_SIZE_EMPTY
bne 1f
ldr r5, [r2, #4]
ldr r6, =ATAG_CORE
cmp r5, r6
bne 1f
mov pc, lr @ atag pointer is ok
1: mov r2, #0
mov pc, lr
ENDPROC(__vet_atags)
这个例程接收机器信息(R8寄存器)为参数,并检测r2中传入的ATAGS 指针的合法性。内核使用tag来作为bootloader传递内核参数的方式。系统要求r2中传进来的ATAGS指针式4字节对齐的,同时要求ATAGS列表的第一个tag是一个ATAG_CORE类型的。
此时R10寄存器中保存有指向CPU信息结构体的指针,R8寄存器中保存有指向机器结构体的指针,R2寄存器中保存有指向tag表的指针,R9中还保存有CPU ID信息。
回到arch/arm/kernel/head.S文件中的stext,之后就要进入初始化过程中比较关键的一步了,开始设置mmu,但首先要填充一个临时的内核页表,映射4m的内存,这在初始化过程中是足够了:
bl __create_page_tables
这个例程设置初始页表,这里只设置最起码的数量,只要能使内核运行即可,r8 = machinfo,r9 = cpuid,r10 = procinfo,在r4寄存器中返回物理页表地址。
__create_page_tables例程在文件arch/arm/kernel/head.S中定义:
__create_page_tables:
pgtbl r4 @ page table address
// pgtbl是一个宏,本文件的前面部分有定义:
// .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中定义,为内核镜像在内存中到内存
// 开始位置的偏移(字节),为$(textofs-y)
// textofs-y也在文件arch/arm/Makefile中定义,
// 为textofs-y := 0x00008000
// r4 = 30004000为临时页表的起始地址
// 首先即是初始化16K的页表,高12位虚拟地址为页表索引,所以为
// 4K*4 = 16K,大页表,每一个页表项,映射1MB虚拟地址。
// 这个地方还来了个循环展开,以优化性能。
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
// PROCINFO_MM_MMUFLAGS在arch/arm/kernel/asm-offsets.c文件中定义,
// 为DEFINE(PROCINFO_MM_MMUFLAGS,
// offsetof(struct proc_info_list, __cpu_mm_mmu_flags));
// R10寄存器保存的指针指向是我们前面找到的proc_info_list结构嘛。
// 为内核的第一个MB创建一致的映射,以为打开MMU做准备,这个映射将会被
// paging_init()移除,这里使用程序计数器来获得相应的段的基地址。
// 这个地方是直接映射。
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_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为单位来映射内核镜像。
add r0, r4, #(KERNEL_START & 0xff000000) >> 18
str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
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
// 为了使用启动参数,将物理内存的第一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]
// 上面的这个步骤显得似乎有些多余。
// 总结一下,这个建立临时页表的过程:
// 1、为内核镜像的第一个MB建立直接映射
// 2、为内核镜像完整的建立从虚拟地址到物理地址的映射
// 3、为物理内存的第一个MB建立到内核的虚拟地址空间的第一个MB的映射。
// OK,内核的临时页表建立完毕。整个初始化临时页表的过程都没有修改R8,
// R9和R10。
mov pc, lr
ENDPROC(__create_page_tables)
回到stext:
ldr r13, __switch_data @ address to jump to after
@ mmu has been enabled
这个地方实际上是在r13中保存了另一个例程的地址。后面的分析中,遇到执行到这个例程的情况时会有详细说明。
接着看stext:
adr lr, BSYM(__enable_mmu) @ return (PIC) address
BSYM()是一个宏,在文件arch/arm/include/asm/unified.h中定义,为:
#define BSYM(sym) sym
也就是说这个语句也仅仅是把__enable_mmu例程的地址加载进lr寄存器中。为了方便之后调用的函数返回时,直接执行__enable_mmu例程。
接着看stext下一句:
ARM( add pc, r10, #PROCINFO_INITFUNC )
ARM()也是一个宏,同样在文件arch/arm/include/asm/unified.h中定义,当配置内核为生成ARM镜像,则为:#define ARM(x...) x
所以这一条语句也就是在调用一个例程。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。
[1] [2] [3] [4]