arm-linux内核start_kernel之前启动分析(1)-接过bootloader的衣钵
转载地址:http://blog.csdn.net/skyflying2012/article/details/41344377
前段时间移植uboot仔细研究过uboot启动过程,最近耐不住寂寞,想对kernel下手。
Uboot启动过程分析博文连接如下:
移植内核时kernel启动过程需要我们修改的地方比较少,研究这个对于编写driver也没有多大帮助,但对了解整个Linux架构,各种机制还是非常有用。
只有知道kernel如何启动,我们才能真正的去理解kernel
作为一个嵌入式工作者,我想不能仅仅局限于某个module driver,而应深入到kernel的汪洋大海中去傲游!
学习启动过程,我本着打破沙锅问到底的原则,希望能研究的明明白白,但也鉴于水平有限,还是有很多纰漏之处
共享博文,希望大家多多交流指正,辛苦整理,如需转载,还请注明出处。
对于arm linux,start_kernel之前都是汇编代码,区区上百行汇编,但是却蕴含着很多精髓。
这部分代码分3篇来分析,另外两篇链接地址如下:
http://blog.csdn.net/skyflying2012/article/details/41447843
http://blog.csdn.net/skyflying2012/article/details/48054417
今天先来学习前几十行!
Kernel版本号:3.4.55
在arch/arm/kernel/head.S中,如下:
- .arm
- __HEAD
- ENTRY(stext)
- THUMB( adr r9, BSYM(1f) ) @ Kernel is always entered in ARM.
- THUMB( bx r9 ) @ If this is a Thumb-2 kernel,
- THUMB( .thumb ) @ switch to Thumb now.
- THUMB(1: )
- //处理器进入svc模式,关闭中断
- setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @ ensure svc mode
- @ and irqs disabled
- //获取处理器ID
- mrc p15, 0, r9, c0, c0 @ get processor id
- bl __lookup_processor_type @ r5=procinfo r9=cpuid
- //将proc_type_list pointer存在r10中,如果为NULL,则error_p
- movs r10, r5 @ invalid processor (r5=0)?
- THUMB( it eq ) @ force fixup-able long branch encoding
- beq __error_p @ yes, error 'p'
- //CONFIG_ARM_LPAE不太明白含义,我使用处理器配置文件没有选择该项,感兴趣朋友可以研究下
- #ifdef CONFIG_ARM_LPAE
- mrc p15, 0, r3, c0, c1, 4 @ read ID_MMFR0
- and r3, r3, #0xf @ extract VMSA support
- cmp r3, #5 @ long-descriptor translation table format?
- THUMB( it lo ) @ force fixup-able long branch encoding
- blo __error_p @ only classic page table format
- #endif
- #ifndef CONFIG_XIP_KERNEL
- //获取物理地址与虚拟地址的offset,存在r8中
- adr r3, 2f
- ldmia r3, {r4, r8}
- sub r4, r3, r4 @ (PHYS_OFFSET - PAGE_OFFSET)
- add r8, r8, r4 @ PHYS_OFFSET
- #else
- //定义CONFIG_XIP_KERNEL,offset为PHYS_OFFSET
- ldr r8, =PHYS_OFFSET @ always constant in this case
- #endif
- /*
- * r1 = machine no, r2 = atags or dtb,
- * r8 = phys_offset, r9 = cpuid, r10 = procinfo
- */
- //对bootloader传来的tags参数进行检查
- bl __vet_atags
在arch/arm/kernel/vmlinux.lds.S,如下:
- OUTPUT_ARCH(arm)
- ENTRY(stext)
- #ifndef __ARMEB__
- jiffies = jiffies_64;
- #else
- jiffies = jiffies_64 + 4;
- #endif
- SECTIONS
- {
- ........
- #ifdef CONFIG_XIP_KERNEL
- . = XIP_VIRT_ADDR(CONFIG_XIP_PHYS_ADDR);
- #else
- . = PAGE_OFFSET + TEXT_OFFSET;
- #endif
- }
./arch/arm/include/asm/memory.h中:
- #define PAGE_OFFSET UL(CONFIG_PAGE_OFFSET)
- Menuconfig中CONFIG_PAGE_OFFSET = 0xc0000000
- ./arch/arm/Makefile中:
- textofs-y := 0x00008000
- textofs-$(CONFIG_ARCH_CLPS711X) := 0x00028000
- # We don't want the htc bootloader to corrupt kernel during resume
- textofs-$(CONFIG_PM_H1940) := 0x00108000
- # SA1111 DMA bug: we don't want the kernel to live in precious DMA-able memory
- ifeq ($(CONFIG_ARCH_SA1100),y)
- textofs-$(CONFIG_SA1111) := 0x00208000
- endif
- textofs-$(CONFIG_ARCH_MSM7X30) := 0x00208000
- textofs-$(CONFIG_ARCH_MSM8X60) := 0x00208000
- textofs-$(CONFIG_ARCH_MSM8960) := 0x00208000
- ......
- # The byte offset of the kernel image in RAM from the start of RAM.
- TEXT_OFFSET := $(textofs-y)
但是实际操作中,kernel是加载到0x80008000地址运行的。
(我使用处理器sdram物理起始地址是0x80000000)
为什么链接地址和运行地址不一致?
学习完start_kernel之前的汇编,就会明白原因了。在stext中,首先调用到__lookup_processor_type,Kernel代码将所有CPU信息的定义都放到.proc.info.init段中,因此可以认为.proc.info.init段就是一个数组,每个元素都定义了一个或一种CPU的信息。
目前__lookup_processor_type使用该元素的前两个字段cpuid和mask来匹配当前CPUID,如果满足CPUID & mask == cpuid,则找到当前cpu的定义并返回。
代码如下:
- __CPUINIT
- __lookup_processor_type:
- //3行汇编,计算出物理地址与虚拟地址之间的offset,存在r3中
- adr r3, __lookup_processor_type_data
- ldmia r3, {r4 - r6}
- sub r3, r3, r4 @ get offset between virt&phys
- //获取__proc_info_begin的物理地址
- add r5, r5, r3 @ convert virt addresses to
- //获取__proc_info_end的物理地址
- add r6, r6, r3 @ physical address space
- //mask cp15读出的cpuid,与proc_type_list中value对比
- 1: ldmia r5, {r3, r4} @ value, mask
- and r4, r4, r9 @ mask wanted bits
- teq r3, r4
- //一致则返回,不一致则跳到下一个proc_type_list,继续对比
- beq 2f
- add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list)
- cmp r5, r6
- blo 1b
- //匹配成功,r5存该proc_type_list指针,匹配失败,r5置0
- mov r5, #0 @ unknown processor
- 2: mov pc, lr
- ENDPROC(__lookup_processor_type)
- /*
- * Look in <asm/procinfo.h> for information about the __proc_info structure.
- */
- .align 2
- .type __lookup_processor_type_data, %object
- __lookup_processor_type_data:
- .long .
- .long __proc_info_begin
- .long __proc_info_end
- .size __lookup_processor_type_data, . - __lookup_processor_type_data</span>
因为kernel要开启MMU,所以kernel编译链接地址是虚拟地址(物理地址经过MMU转换后CPU看到的地址),并不是物理地址,
链接确定了变量的绝对地址(虚拟地址),但在现阶段,没开启MMU,CPU看到的sdram地址就是其物理地址(0x80000000起始)。如果直接运行,对于变量的寻址则会出现问题(函数寻址没问题,因为arm函数寻址使用相对跳转指令b bl)
比如,kernel image中全局变量i链接地址在0xc0009000,但现阶段i物理地址是在0x80009000,对于CPU来说,只能在0x80009000上才能找到i。
去0xc0009000寻址,程序运行就出错了。
这就是为什么我们所理解的,链接地址 加载地址 运行地址必须一致的原因。
kernel现阶段给出的解决方法,就是lookup_processor_type前3行汇编:
adr r3, __lookup_processor_type_data 加载__lookup_processor_type_data地址(实际运行地址,这里就是物理地址)到r3
ldmia r3, {r4 - r6} 获取以r3 r3+4 r3+8为地址的变量到r4,r5,r6.
地址变量值是在链接时确定的,所以r4中存的是__lookup_processor_type_data的链接地址(虚拟地址)。
sub r3 ,r3 ,r4 r3中存储的是物理地址与虚拟地址的偏移。
这是多么genius的操作啊!
_proc_info_begin _proc_info_end在链接脚本中定义,是.proc.info.init段的首尾。
该段中是proc_info_list struct,表示处理器相关信息,定义如下:
- 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/mm/proc-xxx.S中填充,定义了对应arm指令集的处理器特性和初始化函数,在第三篇文章中我们还会详细来理解proc info的作用,这里先按下不表。
lookup_processor_type_data返回stext中。接下来同样用上面的方法获取phy&virt offset,存在r8.
根据我之前分析uboot传参kernel的博文(链接如下:http://blog.csdn.NET/skyflying2012/article/details/35787971)
r1存储machine id,r2存储atags。stext中__vet_atags会对atags做一个基本的检查,代码如下:
- __vet_atags:
- tst r2, #0x3 @ aligned?
- bne 1f
- ldr r5, [r2, #0]
- //判断是否是dtb类型
- #ifdef CONFIG_OF_FLATTREE
- ldr r6, =OF_DT_MAGIC @ is it a DTB?
- cmp r5, r6
- beq 2f
- #endif
- cmp r5, #ATAG_CORE_SIZE @ is first tag ATAG_CORE?
- cmpne r5, #ATAG_CORE_SIZE_EMPTY
- bne 1f
- ldr r5, [r2, #4]
- ldr r6, =ATAG_CORE
- cmp r5, r6
- bne 1f
- //正确tags,返回
- 2: mov pc, lr @ atag/dtb pointer is ok
- //错误tags,清空r2,返回
- 1: mov r2, #0
- mov pc, lr
- ENDPROC(__vet_atags)
检查tag头4 byte(tag_core的size)和第二个4 byte(tag_core的type)是否正确。
对于stext中前几十行汇编,已经分析完成,总结下做了哪些工作:
(1)设置CPU模式
(2)检查CPUID是否匹配
(3)获取phy&virt offset
(4)检查atags参数
这段代码就分析到这,不过引起了我对于链接地址 运行地址的思考。
教科书上是这样教的,我也一直这么认为,链接地址 运行地址(加载地址)必须是一致,但是却没有真正去思考过为什么。
程序的链接地址与运行地址为什么要一致?
我的理解,链接确定程序运行绝对地址,也确定了其中变量及函数的绝对地址,加载运行地址不是其链接地址,变量实际存储的地址就变了。
这时如果对变量进行寻址,就会有不可知的结果,这是我能想到的原因。
平时我们编译链接都是一些C语言编写程序,难免会定义一些全局变量,如果链接和运行地址不一致,就不能正常寻址。
如果想运行和链接地址不一致,我能想到的办法,只能是汇编中尽量不去涉及一些绝对地址,使用PIC位置无关代码。
联想之前分析的uboot relocation原理(博文链接:http://blog.csdn.net/skyflying2012/article/details/37660265),
uboot在relocation之后,kernel在开启MMU之前,都实现了链接地址和运行地址不一致,看看它们用的什么方法?
(1)uboot在relocation时修改rel.dyn段(存储所有变量地址),实现将所有变量地址重定位到新运行地址
所以说,链接地址一定要等于运行地址吗?不一定,嵌入式最著名的uboot kernel就是例子!
arm-linux内核start_kernel之前启动分析(2)- 页表的准备
arm-Linux内核start_kernel之前启动分析另外2篇博文链接地址如下:
http://blog.csdn.net/skyflying2012/article/details/41344377
http://blog.csdn.net/skyflying2012/article/details/48054417
bl __create_page_tables
kernel版本:3.4.55
看看kernel启动初期,开启MMU之前如何初始化页表。此处分析过程我都写在对应的代码处,方便查看。
- #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
- .....
- .macro pgtbl, rd, phys
- add \rd, \phys, #TEXT_OFFSET - PG_DIR_SIZE
- .endm
- .....
- __create_page_tables:
- //据上篇博文分析,r8存储着sdram的物理起始地址(我的板子0x80000000)
- //pgtbl宏获取0x80008000之下16K的地址空间作为页表空间
- //arm页表一页是4 bytes,完成虚拟地址空间4GB中1MB的映射,
- //一共需要4 x 4096 bytes的页表空间
- //可以看出,单页完成的是虚拟地址和物理地址高12位的转换。
- //低20位的地址(1M内的地址)偏移是一致的。
- pgtbl r4, r8 @ page table address
- /*
- * Clear the swapper page table
- */
- //按照16bytes一页将16K页表空间清空
- 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
- //如果定义CONFIG_ARM_LPAE,在PGD与PMD之前还要再加一级页表,这里不详解这种情景
- #ifdef CONFIG_ARM_LPAE
- /*
- * Build the PGD table (first level) to point to the PMD table. A PGD
- * entry is 64-bit wide.
- */
- mov r0, r4
- add r3, r4, #0x1000 @ first PMD table address
- orr r3, r3, #3 @ PGD block type
- mov r6, #4 @ PTRS_PER_PGD
- mov r7, #1 << (55 - 32) @ L_PGD_SWAPPER
- 1: str r3, [r0], #4 @ set bottom PGD entry bits
- str r7, [r0], #4 @ set top PGD entry bits
- add r3, r3, #0x1000 @ next PMD table
- subs r6, r6, #1
- bne 1b
- add r4, r4, #0x1000 @ point to the PMD tables
- #endif
- //据上篇博文分析,r10中存储该CPU的processor_type_list(处理器信息结构体),获取该CPU的mmuflags
- ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
- /*
- * Create identity mapping to cater for __enable_mmu.
- * This identity mapping will be removed by paging_init().
- */
- //首先建立包含turn_mmu_on函数1M空间的平映射(virt addr = phy addr)
- //turn_mmu_on距stext不远,所以实际完成0x8000000-0x81000000空间的平映射
- //老方法,上篇博文分析过,获取phy到virt的offset
- adr r0, __turn_mmu_on_loc
- ldmia r0, {r3, r5, r6}
- sub r0, r0, r3 @ virt->phys offset
- //获取turn_mmu_on的首尾物理地址
- add r5, r5, r0 @ phys __turn_mmu_on
- add r6, r6, r0 @ phys __turn_mmu_on_end
- //因1页映射1M空间,所以SECTION_SHIFT为20
- //右移20位后,r5,r6代表该段地址空间的物理地址页号,因为是平映射,也代表了页表中的须知下标,即虚拟地址页号!
- mov r5, r5, lsr #SECTION_SHIFT
- mov r6, r6, lsr #SECTION_SHIFT
- //r5左移20位,获取该页基地址,或上CPU的mmuflags,存在r3中
- 1: orr r3, r7, r5, lsl #SECTION_SHIFT @ flags + kernel base
- //将r3值存储在页表空间(r4起始)的(r5<<4)的页表中
- //因一页用4bytes表示,所以PMD_ORDER=2
- str r3, [r4, r5, lsl #PMD_ORDER] @ identity mapping
- //r5与r6之前相距多个1M,则需要填写多个页表。
- //因turn_mmu_on函数很短,所以肯定在1M内,该处r5=r6
- cmp r5, r6
- addlo r5, r5, #1 @ next section
- blo 1b
- //从上面这次填页表的过程可以看出,16KB的页表以虚拟地址页号为寻址下标,覆盖整个虚拟的4G地址空间
- /*
- * Now setup the pagetables for our kernel direct
- * mapped region.
- */
- //接下来以多个1M的线性映射页表,建立kernel整个镜像的线性映射,((0x80000000-0x80000000+kernel_end)-(0xc0000000-0xc0000000+kernel_end))
- //开启MMU之后就实现了链接地址(0xc0008000)与运行地址(0xc0008000)的统一
- //这里有一个小技巧,利用当前PC值作为内核物理地址起始,create_page_tables距离内核起始地址不超过1MB,因此移位之后就是内核起始的物理页号。
- //arm的create_page_tables中,不管是turn_mmu_on还是这里,都是使用的当前pc值计算物理页号,
- //这样的好处是,不管内核加载到什么物理地址,都可以迅速的建立正确的页表映射。并且不需要内核开发人员对这部分代码进行修改
- mov r3, pc
- mov r3, r3, lsr #SECTION_SHIFT
- orr r3, r7, r3, lsl #SECTION_SHIFT
- //将该1M空间的物理起始地址存储到页表中相应虚拟地址页中
- add r0, r4, #(KERNEL_START & 0xff000000) >> (SECTION_SHIFT - PMD_ORDER)
- str r3, [r0, #((KERNEL_START & 0x00f00000) >> SECTION_SHIFT) << PMD_ORDER]!
- ldr r6, =(KERNEL_END - 1)
- add r0, r0, #1 << PMD_ORDER
- add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)
- 1: cmp r0, r6
- add r3, r3, #1 << SECTION_SHIFT
- strls r3, [r0], #1 << PMD_ORDER
- bls 1b
- #ifdef CONFIG_XIP_KERNEL
- /*
- * Map some ram to cover our .data and .bss areas.
- */
- add r3, r8, #TEXT_OFFSET
- orr r3, r3, r7
- add r0, r4, #(KERNEL_RAM_VADDR & 0xff000000) >> (SECTION_SHIFT - PMD_ORDER)
- str r3, [r0, #(KERNEL_RAM_VADDR & 0x00f00000) >> (SECTION_SHIFT - PMD_ORDER)]!
- ldr r6, =(_end - 1)
- add r0, r0, #4
- add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)
- 1: cmp r0, r6
- add r3, r3, #1 << 20
- strls r3, [r0], #4
- bls 1b
- #endif
- /*
- * Then map boot params address in r2 or the first 1MB (2MB with LPAE)
- * of ram if boot params address is not specified.
- */
- //将atags的1M地址空间做线性映射,方便start_kernel中对args进行分析
- //据上篇博文分析,r2中存储着bootloader传来的atag基地址(我的板子在0x80000100)
- //所以该1M空间是0x80000000-0x81000000,映射到0xc0000000-0xc1000000
- mov r0, r2, lsr #SECTION_SHIFT
- movs r0, r0, lsl #SECTION_SHIFT
- moveq r0, r8
- sub r3, r0, r8
- add r3, r3, #PAGE_OFFSET
- add r3, r4, r3, lsr #(SECTION_SHIFT - PMD_ORDER)
- orr r6, r7, r0
- str r6, [r3]
- //如果需要早期串口输出进行调试,在这里进行I/O空间的映射,从而实现可以对串口控制器的操作,这里不详解了。
- #ifdef CONFIG_DEBUG_LL
- #if !defined(CONFIG_DEBUG_ICEDCC) && !defined(CONFIG_DEBUG_SEMIHOSTING)
- /*
- * Map in IO space for serial debugging.
- * This allows debug messages to be output
- * via a serial console before paging_init.
- */
- addruart r7, r3, r0
- mov r3, r3, lsr #SECTION_SHIFT
- mov r3, r3, lsl #PMD_ORDER
- 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
- mov r3, r7, lsr #SECTION_SHIFT
- ldr r7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflags
- orr r3, r7, r3, lsl #SECTION_SHIFT
- #ifdef CONFIG_ARM_LPAE
- mov r7, #1 << (54 - 32) @ XN
- #else
- orr r3, r3, #PMD_SECT_XN
- #endif
- 1: str r3, [r0], #4
- #ifdef CONFIG_ARM_LPAE
- str r7, [r0], #4
- #endif
- add r3, r3, #1 << SECTION_SHIFT
- cmp r0, r6
- blo 1b
- #else /* CONFIG_DEBUG_ICEDCC || CONFIG_DEBUG_SEMIHOSTING */
- /* we don't need any serial debugging mappings */
- ldr r7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflags
- #endif
- #if defined(CONFIG_ARCH_NETWINDER) || defined(CONFIG_ARCH_CATS)
- /*
- * If we're using the NetWinder or CATS, we also need to map
- * in the 16550-type serial port for the debug messages
- */
- add r0, r4, #0xff000000 >> (SECTION_SHIFT - PMD_ORDER)
- orr r3, r7, #0x7c000000
- str r3, [r0]
- #endif
- #ifdef CONFIG_ARCH_RPC
- /*
- * Map in screen at 0x02000000 & SCREEN2_BASE
- * Similar reasons here - for debug. This is
- * only for Acorn RiscPC architectures.
- */
- add r0, r4, #0x02000000 >> (SECTION_SHIFT - PMD_ORDER)
- orr r3, r7, #0x02000000
- str r3, [r0]
- add r0, r4, #0xd8000000 >> (SECTION_SHIFT - PMD_ORDER)
- str r3, [r0]
- #endif
- #endif
- #ifdef CONFIG_ARM_LPAE
- sub r4, r4, #0x1000 @ point to the PGD table
- #endif
- mov pc, lr
- ENDPROC(__create_page_tables)
- .ltorg
- .align
- __turn_mmu_on_loc:
- .long .
- .long __turn_mmu_on
- .long __turn_mmu_on_end
create_page_table完成了3种地址映射的页表空间填写:
(1)turn_mmu_on所在1M空间的平映射(2)kernel image的线性映射
(2)atags所在1M空间的线性映射
物理地址空间和虚拟地址空间映射关系图如下:
对于这3种地址空间映射,我觉得分别有3个值得思考的地方:
(1)为什么turn_mmu_on要做平映射?
turn_mmu_on我会在下一篇博文中分析,主要是完成开启MMU的操作。
那为什么将turn_mmu_on处做一个平映射?
可以想象,执行开启MMU指令之前,CPU取指是在0x80008000附近turn_mmu_on中。
如果只是做kernel image的线性映射,执行开启MMU指令后,CPU所看到的地址就全变啦。
turn_mmu_on对于CPU来说在0xc0008000附近,0x80008000附近对于CPU来说已经不可预知了。
但是CPU不知道这些,它只管按照地址一条条取指令,执行指令。
所以不做turn_mmu_on的平映射(virt addr = phy addr),turn_mmu_on在开启MMU后的运行是完全不可知。
完成turn_mmu_on的平映射,我们可以在turn_mmu_on末尾MMU已经开启稳定后,修改PC到0xc0008000附近,就可以解决从0x8xxxxxxx到0xcxxxxxxx的跳转。
(2)kernel image加载地址为什么会在0x****8000?
分析了kernel image线性映射部分,这个就好理解了,
kernel编译链接时的入口地址在0xc0008000(PAGE_OFFSET + TEXT_OFFSET),但其物理地址不等于其链接的虚拟地址,image的线性映射实现其运行地址等于链接地址。
kernel的每一页表映射1M,所以入口处在(0x80000000-->0xc0000000)映射页表中完成映射。物理地址和虚拟地址的1M内偏移必须一致呀。
kernel定义的TEXT_OFFSET = 0x8000.所以加载的物理地址必须为0x****8000.
这样,开启MMU后,访问0xc0008000附近指令,MMU根据TLB才能正确映射找到0x****8000附近的指令。
(3)atags跟kernel入口是在同一1M空间内,bootparams的线性映射操作是否多余?
根据第二个问题的分析,kernel image可以加载到任何sdram地址空间的0x****8000即可。
atags地址是有bootloader中指定,然后告诉kernel。
那就有这样一种情况,加入sdram起始地址为0x80000000,atags起始地址为0x80000100。
但kernel image我加载到0x81008000,可以看出,这时atags跟kernel image就在不同一1M空间啦
atags单独的线性映射操作还是很有必要的。
这是我想到的关于create_page_table的3个疑问,大家如果有别的疑问,欢迎留言讨论,共同学习。
今天就分析到这,页表准备就绪,只待开启MMU!
arm-linux内核start_kernel之前启动分析(3)-开启MMU,走进新时代
最近在忙一款PPC处理器的芯片验证和内核移植工作,导致arm-Linux启动分析最后一部一直没有写,今天将arm-linux start_kernel之前的最后一部分分析记录下。之前2篇文章链接如下:
http://blog.csdn.net/skyflying2012/article/details/41344377
http://blog.csdn.net/skyflying2012/article/details/41447843
kernel版本号:3.4.55
之前分析到__create_page_tables在内核代码区TEXT_OFF下部的16KB区域内进行页表的配置,完成turn_mmu_on的平映射以及kernel image的线性映射。接下来就需要开启MMU,让整个CPU进入虚拟地址运行的新阶段。head.S中stext最后一段代码如下:
/*
* The following calls CPU specific code in a position independent
* manner. See arch/arm/mm/proc-*.S for details. r10 = base of
* xxx_proc_info structure selected by __lookup_processor_type
* above. On return, the CPU will be ready for the MMU to be
* turned on, and r0 will hold the CPU control register value.
*/
ldr r13, =__mmap_switched @ address to jump to after
@ mmu has been enabled
adr lr, BSYM(1f) @ return (PIC) address
mov r8, r4 @ set TTBR1 to swapper_pg_dir
ARM( add pc, r10, #PROCINFO_INITFUNC )
THUMB( add r12, r10, #PROCINFO_INITFUNC )
THUMB( mov pc, r12 )
1: b __enable_mmu
ENDPROC(stext)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
看注释也可以明白接下来要完成的2件工作:执行CPU特定处理代码,开启MMU。
在第一篇分析中我们知道r10中存储着本CPU的proc_info_list首地址。到这里需要再详细解释下内核的proc
info机制。在kernel image中定义有一个.proc.info.init的段。在arch/arm/kernel/vmlinux.lds.S中,如下:
#define PROC_INFO \
. = ALIGN(4); \
VMLINUX_SYMBOL(__proc_info_begin) = .; \
*(.proc.info.init) \
VMLINUX_SYMBOL(__proc_info_end) = .;
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
__proc_info_begin和__pro_info_end分别代表该段的头尾。.proc.info.init段中存储的数据是在arch/arm/mm/proc-xxx.S中定义的。到底使用哪个proc-xxx.S则由处理器的指令集版本号决定。
以我的cortex-A8处理器为例,是armv7指令集,根据arch/arm/mm/Makefile。
obj-$(CONFIG_CPU_V6) += proc-v6.o
obj-$(CONFIG_CPU_V6K) += proc-v6.o
obj-$(CONFIG_CPU_V7) += proc-v7.o
- 1
- 2
- 3
- 1
- 2
- 3
编译的是proc-v7.S,在该文件中有如下一段汇编:
.section ".rodata"
string cpu_arch_name, "armv7"
string cpu_elf_name, "v7"
.align
//接下来定义的数据都在.proc.info.init段中
.section ".proc.info.init", #alloc, #execinstr
/*
* Standard v7 proc info content
*/
//定义了宏定义__v7_proc,这个宏定义非常重要!
.macro __v7_proc initfunc, mm_mmuflags = 0, io_mmuflags = 0, hwcaps = 0
ALT_SMP(.long PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
PMD_SECT_AF | PMD_FLAGS_SMP | \mm_mmuflags)
ALT_UP(.long PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
PMD_SECT_AF | PMD_FLAGS_UP | \mm_mmuflags)
.long PMD_TYPE_SECT | PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ | PMD_SECT_AF | \io_mmuflags
W(b) \initfunc
.long cpu_arch_name
.long cpu_elf_name
.long HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB | HWCAP_FAST_MULT | \
HWCAP_EDSP | HWCAP_TLS | \hwcaps
.long cpu_v7_name
.long v7_processor_functions
.long v7wbi_tlb_fns
.long v6_user_fns
.long v7_cache_fns
.endm
//没有选择LPAE
#ifndef CONFIG_ARM_LPAE
/*
* ARM Ltd. Cortex A5 processor.
*/
.type __v7_ca5mp_proc_info, #object
__v7_ca5mp_proc_info:
.long 0x410fc050
.long 0xff0ffff0
__v7_proc __v7_ca5mp_setup
.size __v7_ca5mp_proc_info, . - __v7_ca5mp_proc_info
/*
* ARM Ltd. Cortex A9 processor.
*/
.type __v7_ca9mp_proc_info, #object
__v7_ca9mp_proc_info:
.long 0x410fc090
.long 0xff0ffff0
__v7_proc __v7_ca9mp_setup
.size __v7_ca9mp_proc_info, . - __v7_ca9mp_proc_info
#endif /* CONFIG_ARM_LPAE */
/*
* ARM Ltd. Cortex A7 processor.
*/
.type __v7_ca7mp_proc_info, #object
__v7_ca7mp_proc_info:
.long 0x410fc070
.long 0xff0ffff0
__v7_proc __v7_ca7mp_setup, hwcaps = HWCAP_IDIV
.size __v7_ca7mp_proc_info, . - __v7_ca7mp_proc_info
/*
* ARM Ltd. Cortex A15 processor.
*/
.type __v7_ca15mp_proc_info, #object
__v7_ca15mp_proc_info:
.long 0x410fc0f0
.long 0xff0ffff0
__v7_proc __v7_ca15mp_setup, hwcaps = HWCAP_IDIV
.size __v7_ca15mp_proc_info, . - __v7_ca15mp_proc_info
/*
* Match any ARMv7 processor core.
*/
.type __v7_proc_info, #object
__v7_proc_info:
.long 0x000f0000 @ Required ID value
.long 0x000f0000 @ Mask for ID
__v7_proc __v7_setup
.size __v7_proc_info, . - __v7_proc_info
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
这段汇编看出,指定.proc.info.init段中存储的是一些结构体,定义了V7指令集特定处理器的属性和处理函数。在C文件中我们找到了这些结构体的定义,在arch/arm/include/asm/proc-info.h中:
/*
* Note! struct processor is always defined if we're
* using MULTI_CPU, otherwise this entry is unused,
* but still exists.
*
* NOTE! The following structure is defined by assembly
* language, NOT C code. For more information, check:
* arch/arm/mm/proc-*.S and arch/arm/kernel/head.S
*/
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;
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
proc-v7.S中定义的__v7_ca5mp_proc_info __v7_ca9mp_proc_info __v7_ca7mp_proc_info __v7_ca15mp_proc_info __v7_proc_info都是proc_info_list结构体,分别对应A5 A9 A7
A15以及其他v7指令集处理器,这些结构体定义时都使用了__v7_proc宏定义来完成成员变量赋值。
为了验证这些proc_info_list结构体都在.proc.info.init段中,我对编译生成的vmlinux的.proc.info.init段进行反汇编。如下:
#arm-linux-objdump -dS --section=.proc.info.init vmlinux
vmlinux: file format elf32-littlearm
Disassembly of section .init.proc.info:
c03d98cc <__proc_info_begin>:
c03d98cc: 410fc050 .word 0x410fc050
c03d98d0: ff0ffff0 .word 0xff0ffff0
c03d98d4: 00000c0e .word 0x00000c0e
c03d98d8: 00000c02 .word 0x00000c02
*/
.type __v7_ca5mp_proc_info, #object
__v7_ca5mp_proc_info:
.long 0x410fc050
.long 0xff0ffff0
__v7_proc __v7_ca5mp_setup
c03d98dc: eafff872 b c03d7aac <__v7_ca5mp_setup>
c03d98e0: c02f07a4 .word 0xc02f07a4
c03d98e4: c02f07aa .word 0xc02f07aa
c03d98e8: 00008097 .word 0x00008097
c03d98ec: c0017b98 .word 0xc0017b98
c03d98f0: c03db2c0 .word 0xc03db2c0
c03d98f4: c03c52c8 .word 0xc03c52c8
c03d98f8: c03db2b8 .word 0xc03db2b8
c03d98fc: c03db290 .word 0xc03db290
c03d9900 <__v7_ca9mp_proc_info>:
c03d9900: 410fc090 ff0ffff0 00000c0e 00000c02 ...A............
*/
.type __v7_ca9mp_proc_info, #object
__v7_ca9mp_proc_info:
.long 0x410fc090
.long 0xff0ffff0
__v7_proc __v7_ca9mp_setup
c03d9910: eafff865 c02f07a4 c02f07aa 00008097 e...../.../.....
c03d9920: c0017b98 c03db2c0 c03c52c8 c03db2b8 .{....=..R<...=.
c03d9930: c03db290 ..=.
c03d9934 <__v7_ca7mp_proc_info>:
c03d9934: 410fc070 ff0ffff0 00000c0e 00000c02 p..A............
*/
.type __v7_ca7mp_proc_info, #object
__v7_ca7mp_proc_info:
.long 0x410fc070
.long 0xff0ffff0
__v7_proc __v7_ca7mp_setup, hwcaps = HWCAP_IDIV
c03d9944: eafff85a c02f07a4 c02f07aa 00068097 Z...../.../.....
c03d9954: c0017b98 c03db2c0 c03c52c8 c03db2b8 .{....=..R<...=.
c03d9964: c03db290 ..=.
c03d9968 <__v7_ca15mp_proc_info>:
c03d9968: 410fc0f0 ff0ffff0 00000c0e 00000c02 ...A............
*/
.type __v7_ca15mp_proc_info, #object
__v7_ca15mp_proc_info:
.long 0x410fc0f0
.long 0xff0ffff0
__v7_proc __v7_ca15mp_setup, hwcaps = HWCAP_IDIV
c03d9978: eafff84d c02f07a4 c02f07aa 00068097 M...../.../.....
c03d9988: c0017b98 c03db2c0 c03c52c8 c03db2b8 .{....=..R<...=.
c03d9998: c03db290 ..=.
c03d999c <__v7_proc_info>:
c03d999c: 000f0000 000f0000 00000c0e 00000c02 ................
*/
.type __v7_proc_info, #object
__v7_proc_info:
.long 0x000f0000 @ Required ID value
.long 0x000f0000 @ Mask for ID
__v7_proc __v7_setup
c03d99ac: eafff841 c02f07a4 c02f07aa 00008097 A...../.../.....
c03d99bc: c0017b98 c03db2c0 c03c52c8 c03db2b8 .{....=..R<...=.
c03d99cc: c03db290 ..=.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
很显然,跟我们分析的一样,.proc.info.init段中存储的就是proc-v7.S中定义的proc_info_list结构体。
第一篇分析中指出__lookup_processor_type函数即遍历.proc.info.init段,根据CP15
c0寄存器读到的处理器版本号进行匹配。将匹配到的proc_info_list结构体存在r10中。
根据实际的运行情况,我的A8处理器遍历后匹配到的是__v7_proc_info,因此r10寄存器存储的是__v7_proc_info结构体首地址。
.proc.info.init段先解释到这里,回到stext中,r13中存下__mmap_switched地址,需要注意的是该地址是链接地址,第二篇说过现在整个kernel image还是运行在物理地址上,因此该地址跳转必须等到开启MMU之后了。接下来对于ARM指令(不是THUMB指令),跳转到__v7_proc_info->__cpu_flush执行。
proc-v7.S中分析过,__v7_proc_info中使用__v7_proc宏定义定义了其中的一些成员变量,其中__cpu_flush=__v7_setup.__v7_setup在proc-v7.S中定义如下:
__v7_setup:
adr r12, __v7_setup_stack @ the local stack
stmia r12, {r0-r5, r7, r9, r11, lr}
bl v7_flush_dcache_all
ldmia r12, {r0-r5, r7, r9, r11, lr}
mrc p15, 0, r0, c0, c0, 0 @ read main ID register
and r10, r0, #0xff000000 @ ARM?
teq r10, #0x41000000
bne 3f
and r5, r0, #0x00f00000 @ variant
and r6, r0, #0x0000000f @ revision
orr r6, r6, r5, lsr #20-4 @ combine variant and revision
ubfx r0, r0, #4, #12 @ primary part number
/* Cortex-A8 Errata */
ldr r10, =0x00000c08 @ Cortex-A8 primary part number
teq r0, r10
bne 2f
#ifdef CONFIG_ARM_ERRATA_430973
teq r5, #0x00100000 @ only present in r1p*
mrceq p15, 0, r10, c1, c0, 1 @ read aux control register
orreq r10, r10, #(1 << 6) @ set IBE to 1
mcreq p15, 0, r10, c1, c0, 1 @ write aux control register
#endif
#ifdef CONFIG_ARM_ERRATA_458693
teq r6, #0x20 @ only present in r2p0
mrceq p15, 0, r10, c1, c0, 1 @ read aux control register
orreq r10, r10, #(1 << 5) @ set L1NEON to 1
orreq r10, r10, #(1 << 9) @ set PLDNOP to 1
mcreq p15, 0, r10, c1, c0, 1 @ write aux control register
#endif
#ifdef CONFIG_ARM_ERRATA_460075
teq r6, #0x20 @ only present in r2p0
mrceq p15, 1, r10, c9, c0, 2 @ read L2 cache aux ctrl register
tsteq r10, #1 << 22
orreq r10, r10, #(1 << 22) @ set the Write Allocate disable bit
mcreq p15, 1, r10, c9, c0, 2 @ write the L2 cache aux ctrl register
#endif
b 3f
/* Cortex-A9 Errata */
2: ldr r10, =0x00000c09 @ Cortex-A9 primary part number
teq r0, r10
bne 3f
#ifdef CONFIG_ARM_ERRATA_742230
cmp r6, #0x22 @ only present up to r2p2
mrcle p15, 0, r10, c15, c0, 1 @ read diagnostic register
orrle r10, r10, #1 << 4 @ set bit #4
mcrle p15, 0, r10, c15, c0, 1 @ write diagnostic register
#endif
#ifdef CONFIG_ARM_ERRATA_742231
teq r6, #0x20 @ present in r2p0
teqne r6, #0x21 @ present in r2p1
teqne r6, #0x22 @ present in r2p2
mrceq p15, 0, r10, c15, c0, 1 @ read diagnostic register
orreq r10, r10, #1 << 12 @ set bit #12
orreq r10, r10, #1 << 22 @ set bit #22
mcreq p15, 0, r10, c15, c0, 1 @ write diagnostic register
#endif
#ifdef CONFIG_ARM_ERRATA_743622
teq r5, #0x00200000 @ only present in r2p*
mrceq p15, 0, r10, c15, c0, 1 @ read diagnostic register
orreq r10, r10, #1 << 6 @ set bit #6
mcreq p15, 0, r10, c15, c0, 1 @ write diagnostic register
#endif
#if defined(CONFIG_ARM_ERRATA_751472) && defined(CONFIG_SMP)
ALT_SMP(cmp r6, #0x30) @ present prior to r3p0
ALT_UP_B(1f)
mrclt p15, 0, r10, c15, c0, 1 @ read diagnostic register
orrlt r10, r10, #1 << 11 @ set bit #11
mcrlt p15, 0, r10, c15, c0, 1 @ write diagnostic register
1:
#endif
3: mov r10, #0
mcr p15, 0, r10, c7, c5, 0 @ I+BTB cache invalidate
dsb
#ifdef CONFIG_MMU
mcr p15, 0, r10, c8, c7, 0 @ invalidate I + D TLBs
v7_ttb_setup r10, r4, r8, r5 @ TTBCR, TTBRx setup
ldr r5, =PRRR @ PRRR
ldr r6, =NMRR @ NMRR
mcr p15, 0, r5, c10, c2, 0 @ write PRRR
mcr p15, 0, r6, c10, c2, 1 @ write NMRR
#endif
#ifndef CONFIG_ARM_THUMBEE
mrc p15, 0, r0, c0, c1, 0 @ read ID_PFR0 for ThumbEE
and r0, r0, #(0xf << 12) @ ThumbEE enabled field
teq r0, #(1 << 12) @ check if ThumbEE is present
bne 1f
mov r5, #0
mcr p14, 6, r5, c1, c0, 0 @ Initialize TEEHBR to 0
mrc p14, 6, r0, c0, c0, 0 @ load TEECR
orr r0, r0, #1 @ set the 1st bit in order to
mcr p14, 6, r0, c0, c0, 0 @ stop userspace TEEHBR access
1:
#endif
adr r5, v7_crval
ldmia r5, {r5, r6}
#ifdef CONFIG_CPU_ENDIAN_BE8
orr r6, r6, #1 << 25 @ big-endian page tables
#endif
#ifdef CONFIG_SWP_EMULATE
orr r5, r5, #(1 << 10) @ set SW bit in "clear"
bic r6, r6, #(1 << 10) @ clear it in "mmuset"
#endif
mrc p15, 0, r0, c1, c0, 0 @ read control register
bic r0, r0, r5 @ clear bits them
orr r0, r0, r6 @ set them
THUMB( orr r0, r0, #1 << 30 ) @ Thumb exceptions
mov pc, lr @ return to head.S:__ret
ENDPROC(__v7_setup)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
初看到这段汇编肯定头大,但是仔细过一遍就会好些,大部分都是一些特定处理器的条件编译并且还有一些跳转label,很多代码可以忽略,对于A8处理器有用的部分我加粗标注出来了。完成的工作如下:
(1)刷新cache,v7_flush_dcache_all
(2)无效掉cache
(3)无效掉指令和数据TLB
(4)从CP15 c0寄存器读出到r0,进行处理,做为下一步写入CP15 c0的值
说实话这部分汇编内核开发者不需要修改,除非是你所用的处理器做了处理器核级的修改,我们只需要了解即可。这里最关心的是r0的数据,因为接下来写如CP15的c0寄存器启动MMU,值就是来自r0.
r0从CP15 c0获取后,首先清掉r5的置位,然后置位r6的置位,r5 r6来自于v7_crval前8个字节,v7_crval在proc-v7-2level.S和proc-v7-3level.S中分别由定义,到底使用哪个是在proc-info.S中条件编译指定的。
#ifdef CONFIG_ARM_LPAE
#include "proc-v7-3level.S"
#else
#include "proc-v7-2level.S"
#endif
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
我的A8处理器选择使用proc-v7-2level.S,v7_crval定义如下:
v7_crval:
crval clear=0x0120c302, mmuset=0x10c03c7d, ucset=0x00c01c7c
- 1
- 2
- 1
- 2
要明白这里定义的v7_crval值含义就需要看CP15 c0寄存器的定义,限于篇幅这里不再详细解释了,感兴趣的朋友可以看我的另一篇说明ARM CP15协处理器的博文:
http://blog.csdn.net/skyflying2012/article/details/25823967
__v7_setup执行完毕,回到stext,接下来跳转到__enable_mmu,也在head.S中,如下:
/*
* Setup common bits before finally enabling the MMU. Essentially
* this is just loading the page table pointer and domain access
* registers.
*
* r0 = cp#15 control register
* r1 = machine ID
* r2 = atags or dtb pointer
* r4 = page table pointer
* r9 = processor ID
* r13 = *virtual* address to jump to upon completion
*/
__enable_mmu:
/*
* turn on L2 cache
*/
mov r5, #0x82
mcr p15, 1, r5, c9, c0, 2
mrc p15, 0, r5, c1, c0, 1
orr r5, r5, #CR_L2
mcr p15, 0, r5, c1, c0, 1
#if defined(CONFIG_ALIGNMENT_TRAP) && __LINUX_ARM_ARCH__ < 6
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
#ifdef CONFIG_ARM_LPAE
mov r5, #0
mcrr p15, 0, r4, r5, c2 @ load TTBR0
#else
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
#endif
b __turn_mmu_on
ENDPROC(__enable_mmu)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
该函数根据内核配置对CP15和r0寄存器进行配置,工作如下:
(1)操作CP15的c1寄存器打开L2 cache
(2)根据内核配置,关闭指令或者数据cache
(3)配置CP15 c3寄存器,配置ARM域的访问权限
(4)配置CP15 c2寄存器,指定内存页表地址
需要注意的是前面__v7_setup中只是无效掉cache,无效是cache操作一种,表示cache中数据无效了,下次由cache读取数据,则需要cache从内存中重新获取数据。这是保证cache数据一致性的手段。
接下来再次跳转到__turn_mmu_on执行,在head.S中如下:
/*
* Enable the MMU. This completely changes the structure of the visible
* memory space. You will not be able to trace execution through this.
* If you have an enquiry about this, *please* check the linux-arm-kernel
* mailing list archives BEFORE sending another post to the list.
*
* r0 = cp#15 control register
* r1 = machine ID
* r2 = atags or dtb pointer
* r9 = processor ID
* r13 = *virtual* address to jump to upon completion
*
* other registers depend on the function called upon completion
*/
.align 5
.pushsection .idmap.text, "ax"
ENTRY(__turn_mmu_on)
mov r0, r0
instr_sync
mcr p15, 0, r0, c1, c0, 0 @ write control reg
mrc p15, 0, r3, c0, c0, 0 @ read id reg
instr_sync
mov r3, r3
mov r3, r13
mov pc, r3
__turn_mmu_on_end:
ENDPROC(__turn_mmu_on)
.popsection
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
该函数将r0值写入CP15的c0寄存器,查看CP15说明,c0寄存器的第0位表征MMU的开启,刚才__v7_setup中v7_crval给出的mmuset值已经将第0位置1了,因此写入c0后MMU就开启了!
在第二篇启动分析文章中讲到create pgtable时,对__turn_mmu_on所在的1M地址空间做了平映射,到现在这个阶段就看出其作用了。
CPU执行完成“mcr p15, 0, r0, c1, c0,0”指令后开启MMU,接下来CPU取指地址是当前pc+4,由于做了平映射,虽然接下来是虚拟地址,但是该虚拟地址跟之前的物理地址是完全一致的,pc+4可以取到mrc p15, 0, r3, c0, c0, 0指令,并不会导致CPU取指上的问题,
可以想象如果做的不是平映射,CPU取pc+4指令,该虚拟地址不是映射到mrc p15, 0, r3,c0, c0, 0指令所在物理地址,CPU接下来的执行就不可预测了。
开启MMU后,修改PC值为r13执行,也就是__mmap_switched,该函数已经是运行在MMU之上的虚拟地址了,因此不需要位置无关代码,如下:
/*
* The following fragment of code is executed with the MMU on in MMU mode,
* and uses absolute addresses; this is not position independent.
*
* r0 = cp#15 control register
* r1 = machine ID
* r2 = atags/dtb pointer
* r9 = processor ID
*/
__INIT
__mmap_switched:
adr r3, __mmap_switched_data
//拷贝data段
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
//清空bss
mov fp, #0 @ Clear BSS (and zero fp)
1: cmp r6, r7
strcc fp, [r6],#4
bcc 1b
//加载r3之后的5个int到r4-sp中
ARM( ldmia r3, {r4, r5, r6, r7, sp})
THUMB( ldmia r3, {r4, r5, r6, r7} )
THUMB( ldr sp, [r3, #16] )
//保存数据到全局变量中
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)
.align 2
.type __mmap_switched_data, %object
__mmap_switched_data:
.long __data_loc @ r4
.long _sdata @ 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
.size __mmap_switched_data, . - __mmap_switched_data
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
__mmap_switched完成的工作如下:
(1)如果有必要则进行data段的拷贝搬移
(2)清空bss段
(3)保存处理器ID atag地址等到指定全局变量中,设置SP,跳转到start_kernel。
看到start_kernel说明我们的分析马上就要结束了,不过这里还是有2个地方需要思考下:
(1)全局变量赋值后,processor_id = r9,__machine_arch_type = r1, __atags_pointer = r2.
start_kernel中对atag等进行分析处理时直接对这些变量进行操作就可以。
(2)sp = init_thread_union + THREAD_START_SP,设置栈指针,为接下来的C函数运行做准备。对于arm-linux来说,start_kernel之前全部都是汇编代码。
init_thread_union代表的是内核第一个进程,pid = 0,该进程是内核人为造出来的,而不是fork出来的,在arch/arm/kernel/init_task.c中定义了该进程,
/*
* Initial thread structure.
*
* We need to make sure that this is 8192-byte aligned due to the
* way process stacks are handled. This is done by making sure
* the linker maps this in the .text segment right after head.S,
* and making head.S ensure the proper alignment.
*
* The things we do for performance..
*/
union thread_union init_thread_union __init_task_data =
{ INIT_THREAD_INFO(init_task) };
thread_union定义如下:
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
其中定义了THREAD_SIZE(8KB)的静态栈空间,__mmap_switched将sp设置在了内核第一进程的栈顶部,栈向下生长,因此接下来start_kernel就运行在该内核栈。
直到rest_init中调用kernel_thread创建进程kernel_init。kernel_init的pid = 1.
__mmap_switched最后跳转到start_kernel开始进入C函数运行环境,这时整个kernel image已经运行在虚拟地址之上,运行地址 链接地址保持了一致,内核运行进入了新时代!
arm-linux启动到start_kernel的汇编运行过程就分析到这。
这段时间做PPC处理器的内核移植,对PPC的启动过程也学习了一番,有时间也将PPC-linux启动到start_kernel的过程加以对比分析!