作者:谷丰,和他联系
转载请包含以上内容
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 ".text.init",#alloc,#execinstr
.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相比较,相等则返回,不等则跳转到下一个处理器信息结构继续比较。从__proc_info_begin开始的保存处理器信息的结构的类型为struct proc_info_list,在include/asm-arm/procinfo.h中有具体的定义。实际的各处理器信息结构的赋值在arch/arm/mm/proc-xscale.S文件的.section ".proc.info", #alloc, #execinstr语句下面。为什么是在.proc.info段的下面呢?这是由vmlinux-armv.lds.in文件中的代码
__proc_info_begin = .;
*(.proc.info)
__proc_info_end = .;
所决定的。
该段子程序完成后,各寄存器情况如下:
R8 = 页表目录项的标志位
R9 = 处理器ID
R10 = 指向当前处理器信息结构的指针
__lookup_architecture_type:
adr r4, 2b
ldmia r4, {r2, r3, r5, r6, r7} @ throw away r2, r3
sub r5, r4, r5 @ convert addresses
add r4, r6, r5 @ to our address space
add r7, r7, r5
1: ldr r5, [r4] @ get machine type
teq r5, r1
beq 2f
add r4, r4, #SIZEOF_MACHINE_DESC
cmp r4, r7
blt 1b
mov r7, #0 @ unknown architecture
mov pc, lr
2: ldmib r4, {r5, r6, r7} @ found, get results
mov pc, lr
这里开始查找机器的体系结构信息。前面已经提到过,在开始执行head-armv.S文件之前,R1中已经包含了当前的体系结构的类型号。现在所要做的,就是在__arch_info_begin开始的地址中,查找与R1中的值相匹配的机器类型信息。从__arch_info_begin开始的保存机器体系结构信息的类型为struct machine_desc,该结构在include/asm-arm/mach/arch.h中有具体的定义。对该结构的赋值使用MACHINE_START宏,该宏的定义同样在arch.h文件中。具体的机器体系结构的信息在arch/arm/kernel/arch.c中,当然也可以在arch/arm/mach-($machine-type)目录下的文件中添加与你自己的机器体系相对应的代码。这一点在进行linux移植的工作中很重要。
该段子程序完成后,各寄存器情况如下:
R5 = 内存(SDRAM)的起始物理地址
R6 = IO的起始物理地址
R7 = IO虚拟地址在页表中的索引项
?这里有一个疑问。将R7赋值是为了调试的需要,使得串口可以打印调试信息,因此需要操作IO。现在,我的IO起始地址为0x40000000,在这里被映射到了0xFC000000。获得UART的虚拟地址的宏addruart在/arch/arm/kernel/debug-armv.S中被定义,这个宏通过io_p2v宏得到IO的虚拟地址,该宏在include/asm-arm/arch-pxa/hardware.h中被定义,但是它将0x40000000的物理地址映射到了0xF8000000,而不是上面提到的0xFC000000,这是为什么呢?希望能有人解释一下这个问题。
__create_page_tables:
pgtbl r4, r5 @ page table address
/* Clear the 16K level 1 swapper page table */
mov r0, r4
mov r3, #0
add r2, r0, #0x4000
1: str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r2
bne 1b
/*
* Create identity mapping for first MB of kernel to
* cater for the MMU enable. This identity mapping
* will be removed by paging_init()
*/
krnladr r2, r4, r5 @ start of kernel
add r3, r8, r2 @ flags + kernel base
str r3, [r4, r2, lsr #18] @ identity mapping
/*
* Now setup the pagetables for our kernel direct
* mapped region. We round TEXTADDR down to the
* nearest megabyte boundary.
*/
add r0, r4, #(TEXTADDR & 0xff000000) >> 18 @ start of kernel
bic r2, r3, #0x00f00000
str r2, [r0] @ PAGE_OFFSET + 0MB
add r0, r0, #(TEXTADDR & 0x00f00000) >> 18
str r3, [r0], #4 @ KERNEL + 0MB
add r3, r3, #1 << 20
str r3, [r0], #4 @ KERNEL + 1MB
add r3, r3, #1 << 20
str r3, [r0], #4 @ KERNEL + 2MB
add r3, r3, #1 << 20
str r3, [r0], #4 @ KERNEL + 3MB
/*
* Ensure that the first section of RAM is present.
* we assume that:
* 1. the RAM is aligned to a 32MB boundary
* 2. the kernel is executing in the same 32MB chunk
* as the start of RAM.
*/
bic r0, r0, #0x01f00000 >> 18 @ round down
and r2, r5, #0xfe000000 @ round down
add r3, r8, r2 @ flags + rambase
str r3, [r0]
bic r8, r8, #0x0c @ turn off cacheable
@ and bufferable bits
代码创建页表目录。首先清空从0xA0004000开始的16K页表项。然后,为了可以访问从0xA0000000开始的内核的1M空间,将该地址对应的页表项赋值。接着映射从TEXTADDR开始的4M的虚拟地址空间,这需要4个页表项。最后,由于SDRAM开始的第一MB的空间存放有启动时的一些参数,所以也需要映射。在这里,该映射和前面的虚拟地址的映射在地址上是相等的。
在创建页表目录完成后,代码通过前面主程序的最后一句add pc, r10, #12跳转到实际的CPU的设置子程序__xscale_setup。
__xscale_setup:
mov r0, #F_BIT|I_BIT|SVC_MODE
msr cpsr_c, r0
mcr p15, 0, ip, c7, c7, 0 @ invalidate I, D caches & BTB
mcr p15, 0, ip, c7, c10, 4 @ Drain Write (& Fill) Buffer
mcr p15, 0, ip, c8, c7, 0 @ invalidate I, D TLBs
mcr p15, 0, r4, c2, c0, 0 @ load page table pointer
mov r0, #0x1f @ Domains 0, 1 = client
mcr p15, 0, r0, c3, c0, 0 @ load domain access register
mov r0, #1 @ Allow user space to access
mcr p15, 0, r0, c15, c1, 0 @ ... CP 0 only.
#if CACHE_WRITE_THROUGH
mov r0, #0x20
#else
Mov r0, #0x00
#endif
mcr p15, 0, r0, c1, c1, 0 @ set auxiliary control reg
mrc p15, 0, r0, c1, c0, 0 @ get control register
bic r0, r0, #0x0200 @ ......R.........
bic r0, r0, #0x0082 @ ........B.....A.
orr r0, r0, #0x0005 @ .............C.M
orr r0, r0, #0x3900 @ ..VIZ..S........
#ifdef CONFIG_XSCALE_CACHE_ERRATA
bic r0, r0, #0x0004 @ see cpu_xscale_proc_init
#endif
mov pc, lr
主要是操作协处理器,设置页表目录项基地址,对CACHE和BUFFER的控制位进行一些操作。具体大家可以看看介绍ARM编程的书。
.type __switch_data, %object
__switch_data: .long __mmap_switched
.long SYMBOL_NAME(__bss_start)
.long SYMBOL_NAME(_end)
.long SYMBOL_NAME(processor_id)
.long SYMBOL_NAME(__machine_arch_type)
.long SYMBOL_NAME(cr_alignment)
.long SYMBOL_NAME(init_task_union)+8192
.type __ret, %function
__ret: ldr lr, __switch_data
mcr p15, 0, r0, c1, c0
mov r0, r0
mov r0, r0
mov r0, r0
mov pc, lr
.align 5
__mmap_switched:
adr r3, __switch_data + 4
ldmia r3, {r4, r5, r6, r7, r8, sp} @ r2 = compat
@ sp = stack pointer
mov fp, #0 @ Clear BSS (and zero fp)
1: cmp r4, r5
strcc fp, [r4],#4
bcc 1b
str r9, [r6] @ Save processor ID
str r1, [r7] @ Save machine type
#ifdef CONFIG_ALIGNMENT_TRAP
orr r0, r0, #2 @ ...........A.
#endif
bic r2, r0, #2 @ Clear 'A' bit
stmia r8, {r0, r2} @ Save control register values
b SYMBOL_NAME(start_kernel)
最后这段代码的作用主要是在进入C函数前先做一些变量的初始化和保存工作。首先清空BSS区域,然后保存处理器ID和机器类型到各自变量地址,接着保存cr_alignment,最后跳转到init/main.c中的start_kernel函数运行。
以上介绍的是head-armv.S文件的主要内容和功能,它是linux运行的第一个文件,具有非常重要的意义。很好的阅读该文件,对于我们理解ARM处理器的工作方式有很大的帮助。同时,在许多linux系统的移植工作中,往往需要对该文件透彻的理解。