分类: LINUX
2011-05-17 14:57:19
\kernel\arch\arm\boot\compressed\ head.S分析(2)
__armv7_mmu_cache_on:
mov r12, lr //注意,这里需要手工保存返回地址!!这样做的原因是下面的bl指令会覆盖掉原来的lr,为保证程序正确返回,需要保存原来lr的值
bl __setup_mmu
mov r0, #0
mcr p15, 0, r0, c7, c10, 4 @ drain write buffer
mcr p15, 0, r0, c8, c7, 0 @ flush I,D TLBs
mrc p15, 0, r0, c1, c0, 0 @ read control reg
orr r0, r0, #0x5000 @ I-cache enable, RR cache replacement
orr r0, r0, #0x0030
bl __common_mmu_cache_on
mov r0, #0
mcr p15, 0, r0, c8, c7, 0 @ flush I,D TLBs
mov pc, r12 //返回到cache_on
这个函数首先执行__setup_mmu,然后清空write buffer、I/Dcache、TLB.接着打开i-cache,设置为Round-robin replacement。调用__common_mmu_cache_on,打开mmu和d-cache.把页表基地址和域访问控制写入协处理器寄存器c2、c3. __common_mmu_cache_on函数数定义如下:
__common_mmu_cache_on:
#ifndef DEBUG
orr r0, r0, #0x000d @ Write buffer, mmu
#endif
mov r1, #-1 //-1的补码是ffff ffff,
mcr p15, 0, r3, c2, c0, 0 @ 把页表地址存于协处理器寄存器中
mcr p15, 0, r1, c3, c0, 0 @设置domain access control寄存 器
b
.align 5 @ cache line aligned
1: mcr p15, 0, r0, c1, c0, 0 @ load control register
mrc p15, 0, r0, c1, c0, 0 @ and read it back to
sub pc, lr, r0, lsr #32 @ properly flush pipeline
重点来看一下__setup_mmu这个函数,定义如下:
__setup_mmu: sub r3, r4, #16384 @ Page directory size
bic r3, r3, #0xff @ Align the pointer
bic r3, r3, #0x
这里r4中存放着内核执行地址,将16K的一级页表放在这个内核执行地址下面的16K空间里,上面通过 sub r3, r4, #16384 获得16K空间后,又将页表的起始地址进行16K对齐放在r3中。即ttb的低14位清零。
//初始化页表,并在RAM空间里打开cacheable 和bufferable位
mov r0, r3
mov r9, r0, lsr #18
mov r9, r9, lsl #18 @ start of RAM
add r10, r9, #0x10000000 @ a reasonable RAM size
上面这几行把一级页表的起始地址保存在r0中,并通过r0获得一个ram起始地址(每个页面大小为
mov r1, #0x12 //一级描述符的bit[1:0]为10,表示这是一个section描述符。也即分页方式为段式分页
orr r1, r1, #3 << 10 //一级描述符的access permission bits bit[11:10]为11. 即
add r2, r3, #16384 //一级描述符表的结束地址存放在r2中。
1: cmp r1, r9 @ if virt > start of RAM
orrhs r1, r1, #0x
cmp r1, r10 @ if virt > end of RAM
bichs r1, r1, #0x
str r1, [r0], #4 @ 1:1 mapping
add r1, r1, #1048576//下个
teq r0, r2
bne 1b
因为打开cache前必须打开mmu,所以这里先对页表进行初始化,然后打开mmu和cache。
上面这段就是对一级描述符表(页表)的初始化,首先比较这个描述符所描述的地址是否在那个
页表大小为16K,每个描述符4字节,刚好可以容纳4096个描述符,每个描述符映射
mov r1, #0x1e
orr r1, r1, #3 << 10 //这两行将描述的bit[11:10] bit[4:1]置位,
//具体置位的原因,在ARM11的页表项描述符里有说明,由于没找到完整的文档,这里只给出图示:
mov r2, pc, lsr #20
orr r1, r1, r2, lsl #20 //将当前地址进
add r0, r3, r2, lsl #2 //r3为刚才建立的一级描述符表的起始地址。通过将当前地
//址(pc)的高12位左移两位(形成14位索引)与r3中的地址
// (低14位为0)相加形成一个4字节对齐的地址,这个
//地址也在16K的一级描述符表内。当前地址对应的
//描述符在一级页表中的位置
str r1, [r0], #4
add r1, r1, #1048576
str r1, [r0] //这里将上面形成的描述符及其连续的下一个section描述
//写入上面4字节对齐地址处(一级页表中索引为r2左移
//2位)
mov pc, lr //返回,调用此函数时,调用指令的下一语句mov r0, #0的地 址保存在lr中
这里进行的是一致性的映射,物理地址和虚拟地址是一样。
__common_mmu_cache_on最后执行mov pc, r12返回cache_on,为何返回到的是cache_on呢?这就是上面解释保存lr的原因,因为原来的lr保存了 执行
bl cache_on语句的下条指令,因此能正确返回!
下一条指令也即是下面开始
mov r1, sp @栈空间大小是4096字节,那//么在栈空间地址上面再分配64K字节空间
add r2, sp, #0x10000 @ 分配64k字节。
栈的分配如下:
.align
.section ".stack", "w"
user_stack: .space 4096//lc0对SP进行了定义 .word user_stack+4096 @ sp
由此可见sp是往下增长的
分配了解压缩用的缓冲区,那么接下来就判断这个数据区是否和我们目前运行的代码空间重叠,如果重叠则需调整
/*
* Check to see if we will overwrite ourselves.
* r4 = final kernel address
* r5 = start of this image
* r2 = end of malloc space (and therefore this image)
* We basically want:
* r4 >= r2 -> OK
* r4 + image length <= r5 -> OK
*/
cmp r4, r2
bhs wont_overwrite
sub r3, sp, r5 @ > compressed kernel size
add r0, r4, r3, lsl #2 @ allow for 4x expansion
cmp r0, r5
bls wont_overwrite
缓冲区空间的起始地址和结束地址分别存放在r1、r2中。然后判断最终内核地址,也就是解压后内核的起始地址,是否大于malloc空间的结束地址,如果大于就跳到wont_overwrite执行,wont_overwrite函数后面会讲到。否则,检查最终内核地址加解压后内核大小,也就是解压后内核的结束地址,是否小于现在未解压内核映像的起始地址。小于也会跳到wont_owerwrite执行。如两这两个条件都不满足,则继续往下执行。
mov r5, r2 @ decompress after malloc space
mov r0, r5
mov r3, r7
bl decompress_kernel
这里将解压后内核的起始地址设为malloc空间的结束地址。然后后把处理器id(开始时保存在r7中)保存到r3中,调用decompress_kernel开始解压内核。这个函数的四个参数分别存放在r0-r3中,它在arch/arm/boot/compressed/misc.c中定义。 解压的过程为先把解压代码放到缓冲区,然后从缓冲区在拷贝到最终执行空间。
add r0, r0, #127
bic r0, r0, #127 @ align the kernel length
/*
* r0 = decompressed kernel length
* r1-r3 = unused
* r4 = kernel execution address
* r5 = decompressed kernel start
* r6 = processor ID
* r7 = architecture ID
* r8 = atags pointer
* r9-r14 = corrupted
*/
add r1, r5, r0 @ end of decompressed kernel
adr r2, reloc_start
ldr r3, LC1
add r3, r2, r3
1: ldmia r2!, {r9 - r14} @ copy relocation code
stmia r1!, {r9 - r14}
ldmia r2!, {r9 - r14}
stmia r1!, {r9 - r14}
cmp r2, r3
blo 1b
这里首先计算出重定位段,也即reloc_start段,然后对它的进行重定位
bl cache_clean_flush
add pc, r5, r0 @ call relocation code
重定位结束后跳到解压后执行 b call_kernel,不再返回。call_kernel定义如下:
call_kernel:
bl cache_clean_flush
bl cache_off
mov r0, #0 @ must be zero
mov r1, r7 @ restore architecture number
mov r2, r8 @ restore atags pointer
mov pc, r4 @ call kernel
在运行解压后内核之前,先调用了
cache_clean_flush这个函数。这个函数的定义如下:
cache_clean_flush:
mov r3, #16
b call_cache_fn
其实这里又调用了call_cache_fn这个函数,注意,这里r3的值为16,上面对cache操作已经比较详细,不再讨论。
刷新cache后,则执行mov pc, r4跳入内核,开始进行下个阶段的处理。
整个代码流程如下:
当解压缩部分的head.S执行完后,就开始执行kernel\目录下真正的linux内核代码。在内核连接文件/kernel/vmlinux/lds里定义了这部分开始所处的段空间为.text.head,也即内核代码段的头 关键代码如下: mrc p15, 0, r9, c0, c0 @ get processor id//读出CPUid bl __lookup_processor_type @ r5=procinfo r9=cpuid movs r10, r5 @ invalid processor (r5=0)? beq __error_p @ yes, error 'p' bl __lookup_machine_type @ r5=machinfo movs r8, r5 @ invalid machine (r5=0)? beq __error_a @ yes, error 'a' bl __vet_atags bl __create_page_tables 大致流程为,寻找CPU类型查找机器信息,解析内核参数列表,创建内存分页机制 __lookup_processor_type,__lookup_machine_type,__vet_atags函数都在kernel\head-comm.S内,这个文件实际上是被包含在head.S内 Linux之所以把搜索机器类型和CPU类型独立出来,就是为了让内核尽可能的和bootload独立,增强移植性,把不同CPU的差异性处理减到最小。比如不同ARM架构的CPU处理中断的,打开MMU,cach操作是不同的,因此,在内核开始执行前需要定位CPU架构,比如高通利用的ARM11,Ti用的cortex-8架构 __lookup_machine_type 寻找的机器类型结构定义在arch\arm\include\asm\mach.h中 查询方法比较简单,利用bootloa传进来的参数依次查询上述结构表项 这个表项是在编译阶段将#define MACHINE_START(_type,_name)宏定义的结构体struct machine_desc 连接到 __arch_info段,那么结构体开始和结束地址用__arch_info_begin和__arch_info_end符号引用 3: .long . .long __arch_info_begin .long __arch_info_end //r1 = 机器架构代码 number,由bootload最后阶段传进来 .type __lookup_machine_type, %function __lookup_machine_type: adr r3, 3b ldmia r3, {r4, r5, r6} sub r3, r3, r4 @ 此时没有开MMU,因此需要确定放置__arch_info_begin的实际物理地址 add r5, r5, r3 @ 调整地址,找到__arch_info的实际地址(连接地址和物理地址不一定一样,因此需要调整) add r6, r6, r3 @ 1: ldr r3, [r5, #MACHINFO_TYPE] @ MACHINFO_TYPE=机器类型域的偏移量 teq r3, r1 @ 是否和bootload传进来的参数相同? beq 2f @ 找到则跳出循环 add r5, r5, #SIZEOF_MACHINE_DESC @ 地址偏移至下个__arch_inf表项 cmp r5, r6 blo 1b mov r5, #0 @ 未知的类型 2: mov pc, lr//返回 __lookup_processor_type的查询的结构为struct proc_info_list 机器类型确定后即开始解析(__vet_atags)内核参数列表,判断第一个参数类型是不是ATAG_CORE。 内核参数列表一般放在内核前面16K地址空间处。列表的表项由struct tag构成,每个struct tag有常见的以下类型: :ATAG_CORE、ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_INITRD等。 这些类型是宏定义,比如#define ATAG_CORE 0x54410001 arch\arm\include\asm\setup.h struct tag_header { __u32 size; __u32 tag; }; struct tag { struct tag_header hdr; union { struct tag_core core;//有效的内核 struct tag_mem32 mem; struct tag_videotext videotext; struct tag_ramdisk ramdisk;//文件系统 struct tag_initrd initrd;//临时根文件系统 struct tag_serialnr serialnr; struct tag_revision revision; struct tag_videolfb videolfb; struct tag_cmdline cmdline;//命令行 } u; }; 接下来就是创建页表,因为要使能MMU进行虚拟内存管理,因此必须创建映射用的页表。页表就像一个函数发生器,保证访问虚拟地址时能从物理地址里取到正确代码 pgtbl r4 @ page table address //页表放置的位置可由下面的宏确定,即在内核所在空间的前16K处 .macro pgtbl, rd ldr \rd, =(KERNEL_RAM_PADDR - 0x4000) .endm mov r0, r4 mov r3, #0 add r6, r0, #0x4000//16K的空间,r6即是页表结束处 1: str r3, [r0], #4//清空页表项,页表项共有16K/4项 str r3, [r0], #4 str r3, [r0], #4 str r3, [r0], #4 teq r0, r6 bne 1b ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] //从从差得的proc_info_list结构PROCINFO_MM_MMUFLAGS处获取MMU的信息 /* 为内核创建1M的映射空间,这里是按照1:1一致映射,即代码的基地址(高12bit)对应相同的物理块地址。这种映射关系只是在启动阶段,在跳进start_kernel后会被paging_init().移除。这种映射可以直接利用当前地址的高12bit作为基地址,这种方式很巧妙,因为当前的PC(加颜色处的地址)依然在1M空间内,因此,高12bit(段基地址)在1M空间内都是相同的。 */ mov r6, pc, lsr #20 @ 内核映像的基地址 orr r3, r7, r6, lsl #20 @ 基地址偏移后再加上标示符,即可得一个页表项的值 str r3, [r4, r6, lsl #2] @将此表项按照页表项的索引存入对应的表项中。比如,若//基地址是0xc0001000,那么存入页表的第0xc00项中 //目前的映射依然是1:1的映射 //然后移到下个段基地址处,开始映射此KERNEL_START对应的空间 //这个空间映射的物理地址与上面的相同,也就是两个虚拟地址映射到了同一个物理地址空间 //r0+基地址组成//在第一级页表中索引到相关的项 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//下个1M物理地址空间 strls r3, [r0], #4//建立映射表项,开始创建所有的内核空间页表项 bls 1b// #ifdef CONFIG_XIP_KERNEL /* * Map some ram to cover our .data and .bss areas. */ orr r3, r7, #(KERNEL_RAM_PADDR & 0xff000000) .if (KERNEL_RAM_PADDR & 0x00f00000) orr r3, r3, #(KERNEL_RAM_PADDR & 0x00f00000) .endif add r0, r4, #(KERNEL_RAM_VADDR & 0xff000000) >> 18 str r3, [r0, #(KERNEL_RAM_VADDR & 0x00f00000) >> 18]! ldr r6, =(_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 #endif /* * Then map first 1MB of ram in case it contains our boot params. */ //虚拟ram地址的第一个1M空间包含了参数列表,也需要映射 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] mov pc, lr//页表建立完成,返回 页表创建后,具体的映射空间如下图: 执行完上述页表创建,开始执行内核跳转: ldr r13, __switch_data @ address to jump to after @ mmu has been enabled adr lr, __enable_mmu @ return (PIC) address add pc, r10, #PROCINFO_INITFUNC __switch_data 是一个数据结构,如下 .type __switch_data, %object __switch_data: .long __mmap_switched .long __data_loc @ r4 .long __data_start @ 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 语句“add pc, r10, #PROCINFO_INITFUNC”通过查表调用proc-v7.s中__v7_setup函数,该函数末尾通过将lr寄存器赋给pc,导致对__enable_mmu的调用,完成使能mmu的操作,之后将r13寄存器值赋给pc,调用__switch_data数据结构中的第一个函数__mmap_switched, .type __mmap_switched, %function __mmap_switched: adr r3, __switch_data + 4 ldmia r3!, {r4, r5, r6, r7} cmp r4, r5 @ 拷贝数据段 1: cmpne r5, r6 ldrne fp, [r4], #4 strne fp, [r5], #4 bne 1b mov fp, #0 @ 清除BSS段 1: cmp r6, r7 strcc fp, [r6],#4 bcc 1b ldmia r3, {r4, r5, r6, r7, sp}//然后调整指针到processor_id 域 str r9, [r4] @ 保存CPU ID str r1, [r5] @保存机器类型 str r2, [r6] @ 保存参数列表指针 bic r4, r0, #CR_A @ Clear 'A' bit stmia r7, {r0, r4} @ 保存控制信息 b start_kernel 最终调用init\main.c文件中的start_kernel函数。 这个start_kernel正是kernel\init\main.c的内核起始函数