ARM Linux启动分析----head-armv.S内幕
文章出处:http://www.diybl.com/course/6_system/linux/Linuxjs/2007114/84011_6.html Linux启动后执行的第一个文件是arch/arm/kernel下的head-($PROCESSOR).S文件,processor代表的是该cpu的类型。ARM 6及其以后的处理器核心支持32位地址空间。这些处理器可以在26位和 32位PC的模式下操作。在26位PC模式下,R15寄存器的表现如同在以前的处理器上,代码只能运行在地址空间的最低的64M字节空间中。在32位PC模式下,32位的R15寄存器被用做程序计数器。使用独立的状态寄存器来存储处理器模式和状态标志。对于26位的arm处理器类型,linux用armo来表示;对于32位的arm处理器,使用armv表示。在include/linux/autoconf.h文件中通过 #define CONFIG_CPU_32 1
将处理器类型设置为支持32位PC模式。然后在arch/arm/Makefile中通过 ifeq ($(CONFIG_CPU_32),y) PROCESSOR = armv
TEXTADDR = 0xC0008000
LDSCRIPT = arch/arm/vmlinux-armv.lds.in endif
设置处理器类型为armv,这样linux运行所执行的第一个文件就是head-armv.S。接着,Makefile定义了内核中代码和数据所使用的虚拟地址TEXRADDR,最后,定义了链接器所使用的脚本文件,这个文件也是与处理器类型相关的。
在执行head-armv.S文件之前,有一点需要注意的是,bootloader已经在处理器的R1寄存器中存放了机器体系结构的类型号。由于在文件的执行过程中将要针对当前的机器体系结构设置相关的参数,如果没有这个步骤,系统将显示“ERROR:a”,同时停止执行。当然,也可以在head-armv.S文件的开头添加代码,手工对R1赋值,具体的机器类型号在arch/arm/tools/mach-types文件中。
好了,接下来我们可以开始阅读head-armv.S文件了,看看它到底作了些什么事情。由于篇幅的限制,对一些不是很关键的代码和英文注释予以省略,但是在每段代码后,我会根据自己的理解给出解释。
#if (TEXTADDR & 0xffff) != 0x8000
#error TEXTADDR must start at 0xXXXX8000 #endif
.globl SYMBOL_NAME(swapper_pg_dir)
.equ SYMBOL_NAME(swapper_pg_dir), TEXTADDR - 0x4000
.macro pgtbl, reg, rambase adr \\reg, stext
sub \\reg, \\reg, #0x4000 .endm
.macro krnladr, rd, pgtable, rambase bic \\rd, \\pgtable, #0x000ff000 .endm
首先,系统确保TEXTADDR的地址是以0x8000结尾的,前面已经提到过,TEXTADDR的地址是0xC0008000,是内核所使用的虚拟地址,而我所使用的PXA255处理器上支持的SDRAM空间是从0xA0000000开始的,这就需要通过MMU进行虚拟地址到实际物理地址的转换,也就是说将0xC0008000映射到0xA0008000。地址转换所使用的页表将存放在从0xA0008000网上的16K空间中,即从0xA0004000到0xA0008000这一段。因此,系统必须空出0xA0000000到0xA0008000这一段,存放页表和其它的一些内核将使用到的数据结构。虽然上面的代码判断的是TEXTADDR的地址是否以0x8000结尾,但从效果上说是一样的。
接着,代码定义了全局变量swapper_pg_dir,它是页表目录项的虚拟地址。前面用SYMBOL_NAME()修饰,这是因为在有的系统中,C编绎器对.C文件中的符号名有\前缀,SYMBOL_NAME()可以使汇编代码也适应这种变化。但是在当前的Linux中,SYMBOL_NAME实际上不起任何作用。大家可以参考include/linux/linkage.h中对该修饰符的定义。
然后,代码定义了pgtbl和krnladr两个宏。Pgtbl宏得到的是与位置无关的页表目录项地址,值为0xA000800往上16k的地址,即0xA0004000。stext所代表的也是内核的起始地址,通过arch/arm/vmlinux-armv.lds.in的链接脚本可以发现它在内核中的链接地址和TEXTADDR一致。那么为什么页表地址不是0xC0004000呢?因为我们在定义/reg寄存器时使用的adr指令,adr指令是在当前的PC值上+/-一个标号的偏移得到的, 所以得到的地址只跟PC和标号到PC的偏移相关, 跟编译地址无关。在MMU打开前, 代码要是地址无关的, 会经常用到adr指令。由于当前的PC运行的地址是从0xA0008000开始的地址空间,所以最后得到的页表地址为0xA0004000。krnladr宏需要配合其它代码使用,它的本意是为了使对从0xA0000000开始的内核的地址空间的寻址不会因为MMU的原因而被映射到其它的地址。因此需要将定义0xA0000000地址转换的页表项中的值的高20位定义为0xA0000,最低的12位保存的是页表的标志位。由于该页表项的索引值是由地址的最高12位所决定的,因此krnladr宏将地址的最低20位清零。在本文件中,只清空了第4--11位,是因为有其它的代码屏蔽了低12位的作用。如果将上面的代码改成bic \\rd, \\pgtable, #0x000fffff,效果是一样的。
.section \ .type stext, #function ENTRY(stext)
mov r12, r0
mov r0, #F_BIT | I_BIT | MODE_SVC @ make sure svc mode msr cpsr_c, r0 @ and all irqs disabled bl __lookup_processor_type
teq r10, #0 @ invalid processor?
moveq r0, #'p' @ yes, error 'p' beq __error
bl __lookup_architecture_type
teq r7, #0 @ invalid architecture? moveq r0, #'a' @ yes, error 'a' beq __error
bl __create_page_tables
adr lr, __ret @ return address
add pc, r10, #12 @ initialise processor @ (return control reg)
接着我们进入了head-armv.S的主程序段,参考上面的代码。首先,确保处理器进入SVC模式,屏蔽所有外部中断。接着查询处理器类型和机器的体系结构类型,其中任何一步发生错误,显示“ERROR:p”或者“ERROR:a”。然后建立页表目录项。我们来看看每个子程序段具体是如何工作的。
__lookup_processor_type: adr r5, 2f ldmia r5, {r7, r9, r10}
sub r5, r5, r10 @ convert addresses add r7, r7, r5 @ to our address space add r10, r9, r5
mrc p15, 0, r9, c0, c0 @ get processor id 1: ldmia r10, {r5, r6, r8} @ value, mask, mmuflags and r6, r6, r9 @ mask wanted bits teq r5, r6 moveq pc, lr
add r10, r10, #36 @ sizeof(proc_info_list) cmp r10, r7 blt 1b
mov r10, #0 @ unknown processor mov pc, lr
2: .long __proc_info_end .long __proc_info_begin .long 2b
.long __arch_info_begin .long __arch_info_end
代码首先在R5寄存器中存放标号2所代表的相对地址,然后通过ldmia r5, {r7, r9, r10}在R7和R9中放置__proc_info_end、__proc_info_begin的链接地址,在R10中放置标号2的链接地址。通过将R5和R10中的数值相减,得到符号的链接地址和实际地址之间的差值,进而得到__proc_info_end、__proc_info_begin的实际地址。其实这些代码的作用和adr __proc_info_end,adr __proc_info_begin的效果是一样的。在MMU还没有被打开的情况下,一般采取这种办法来进行地址之间的映射。
这里有一点要注意,在引用标号2的地址时,采取了2f和2b两种不同的表示法,这是什么原因呢?在代码中你可以使用0--99之间的数字作为标号,它们会被视为临时性的符号,可以在代码中重复使用同一个数字作为label。在一个分支指令(branch instruction)中“2f”指向下一个“2:”,而“2b”指向前一个“2:”,这样就不用费心为那些随手而写的跳转和循环起名字了,省下这些名称可以去命名那些子程序、还有那些比较关键的跳转。 接着代码通过访问P15协处理器,得到当前的CPU的处理器ID,然后与以__proc_info_begin开始的处理器信息结构中的处理器ID相比较,相等则返回,不等则跳转到下一个处理器信