我们使用的cpu是采用xscale技术的pxa250,具体的开发板是cerfboard。这里的代码解释都是根据pxa250进行的。不过,正如我们下面会看到,大部分的代码都是和具体的pxa型号无关的。另外,我们列出代码的行号仅仅为了解释方便,并不是代码在源文件中真正的行号。
1. bootloader及内核解压
bootloader将内核加载到内存中,设定一些寄存器,然后就将控制权交由内核了,这时并不需要打开MMU功能。
一般,我们所说的内核都是指被压缩了的内核piggy.gz,其与head.o、misc.o和head-xscale.o一起构成最终的内核代码。
启动内核的过程首先包括解压,最先执行的代码是:
arch/arm/boot/compressed/head.S
arch/arm/boot/compressed/head-xscale.S
arch/arm/boot/compressed/misc.c
上面代码主要任务是解压真正的内核代码,真正的内核代码是从head-armv.S开始,以后提到的内核都是指真正的内核:
arch/arm/kernel/head-armv.S
2. 内核启动第一阶段
内核启动第一阶段是从ENTRY(stext)到c语言函数start_kernel,这一部分涉及的代码有
arch/arm/kernel/head-armv.S
arch/arm/mm/proc-xscale.S
在bootloader调用内核运行之前,要在指定的寄存器中存放参数,如下:
r0 = 0,
r1 = machine type number,对于MACH_TYPE_PXA_CERF是139
r2 = physical address of tagged list in system RAM.
2.1 stext
ENTRY(stext)是整个内核的入口,其代码位于编译后生成的目标文件kernel.o中的.text.init区(section),最后被链接到内核的.init区。
1.section ".text.init",#alloc,#execinstr@定义所属区
2.typestext, #function@定义符号stext为函数符号
3ENTRY(stext)
4movr12, r0
5movr0, #F_BIT | I_BIT | MODE_SVC@ make sure svc mode
6msrcpsr_c, r0@ and all irqs disabled
7bl__lookup_processor_type
8teqr10, #0@ invalid processor?
9moveqr0, #'p'@ yes, error 'p'
10beq__error
11bl__lookup_architecture_type
12teqr7, #0@ invalid architecture?
13moveqr0, #'a'@ yes, error 'a'
14beq__error
15bl__create_page_tables
16adrlr, __ret@ return address
17addpc, r10, #12@ initialise processor
18@ (return control reg)
第5行,准备进入SVC工作模式,同时关闭中断(I_BIT)和快速中断(F_BIT)
第7行,查看处理器类型,主要是为了得到处理器的ID以及页表的flags,详细说明见下。
第11行,查看一些体系结构的信息,详细说明见下。
第15行,建立页表。
第17行,跳转到处理器的初始化函数,其函数地址是从__lookup_processor_type中得到的,需要注意的是第16行,当处理器初始化完成后,会直接跳转到__ret去执行,这是由于初始化函数最后的语句是mov pc, lr。
2.2 __lookup_processor_type
我们先来看看__lookup_processor_type函数,我们知道arm系列的处理器有arm7、arm9、arm10以及xscale等,可是如何区分呢?在内核中有个结构struct proc_info_list,用来记录处理器相关的信息:
19struct proc_info_list {
20unsigned intcpu_val;
21unsigned intcpu_mask;
22unsigned long__cpu_mmu_flags;/* used by head-armv.S */
23unsigned long__cpu_flush;/* used by head-armv.S */
24const char*arch_name;
25const char*elf_name;
26unsigned intelf_hwcap;
27struct proc_info_item*info;
28struct processor*proc;
29};
对于采用xscale技术pxa250,具体的处理器相关信息存放在arch/arm/mm/proc-xscale.S中,这里抽出了与pxa250有关的信息,和struct proc_info_list对照就明白了:
30.section ".proc.info", #alloc, #execinstr
31__pxa250_proc_info:
32.long0x69052100
33.long0xfffff7f0
34#if CACHE_WRITE_THROUGH
35.long0x00000c0a
36#else
37.long0x00000c0e
38#endif
39b__xscale_setup
40.longcpu_arch_name
41.longcpu_elf_name
42.longHWCAP_SWP|HWCAP_HALF|HWCAP_THUMB|HWCAP_FAST_MULT|HWCAP_EDSP
43.longcpu_pxa250_info
44.longxscale_processor_functions
45.size__pxa250_proc_info, . - __pxa250_proc_info
第39行是处理器初始化函数__xscale_setup的地址
这里的一些结构随着我们对于代码的深入就会渐渐清晰了,现在我们不用过分纠缠与此。
由于第30行.section指示符,上面定义的__pxa250_proc_info信息在编译的时候,被链接到.proc.info区中,而arch/arm/vmlinux-armv.lds中指定一个符号__proc_info_begin指向此.proc.info区的起始地址,而__proc_info_end指向此.proc.info区结尾的下一地址。这是GNU的ld的一些功能,各位若不清楚,可以查看ld的手册。
__proc_info_begin = .;
*(.proc.info)
__proc_info_end = .;
这样,我们通过循环就可以访问所有的struct proc_info_list类型的对象了。
好了,做了这么多铺垫,我们来看看__lookup_processor_type函数吧:
46__lookup_processor_type:
47adrr5, 2f@ get runtime address
48ldmiar5, {r7, r9, r10}
49subr5, r5, r10@ convert addresses, offset between
50 @ runtime address and link address
51addr7, r7, r5@ to our address space
52addr10, r9, r5
53mrcp15, 0, r9, c0, c0@ get processor id
541:ldmiar10, {r5, r6, r8}@ value, mask, mmuflags
55andr6, r6, r9@ mask wanted bits
56teqr5, r6
57moveqpc, lr
58addr10, r10, #36@ sizeof(proc_info_list)
59cmpr10, r7
60blt1b
61movr10, #0@ unknown processor
62movpc, lr
63
64/*
65 * Look in include/asm-arm/procinfo.h and arch/arm/kernel/arch.[ch] for
66 * more information about the __proc_info and __arch_info structures.
67 */
682:.long__proc_info_end
69.long__proc_info_begin
70.long2b
71.long__arch_info_begin
72.long__arch_info_end
第68-69行就是我们刚才说的指向.proc.info区起始和结尾地址的符号,在第47行,我们将第68行代码的地址放到r5中,那么作为指针,r5指向的(非是r5的内容)是.proc.info区结尾的下一地址__proc_info_end。
第48行结果是r7=__proc_info_end,r9= __proc_info_begin,r10=第70行的地址
这个函数执行完毕后,寄存器情况如下:
r8 = page table flags
r9 = processor ID
r10 = pointer to processor structure(成功时),r10 = 0 (失败时)
2.3 __lookup_architecture_type
类似于struct proc_info_list,每个机器(某种开发板)都有自己特定的体系结构,如物理内存地址,物理I/O地址,显存起始地址等等,这个结构为struct machine_desc:
struct machine_desc {
/*
* Note! The first four elements are used
* by assembler code in head-armv.S
*/
unsigned intnr;/* architecture number*/
unsigned intphys_ram;/* start of physical ram */
unsigned intphys_io;/* start of physical io*/
unsigned intio_pg_offst;/* byte offset for io page tabe entry*/
const char*name;/* architecture name*/
unsigned intparam_offset;/* parameter page*/
unsigned intvideo_start;/* start of video RAM*/
unsigned intvideo_end;/* end of video RAM*/
unsigned intreserve_lp0 :1;/* never has lp0*/
unsigned intreserve_lp1 :1;/* never has lp1*/
unsigned intreserve_lp2 :1;/* never has lp2*/
unsigned intsoft_reboot :1;/* soft reboot*/
void(*fixup)(struct machine_desc *,
struct param_struct *, char **,
struct meminfo *);
void(*map_io)(void);/* IO mapping function*/
void(*init_irq)(void);
};
我所使用的开发板其特定的体系配置文件在arch/arm/mach-pxa/cerf.c中,是通过一些特定的宏来实现的,如下:
1MACHINE_START(PXA_CERF, "CerfBoard PXA Reference Board")
2MAINTAINER("Intrinsyc Software Inc.")
3BOOT_MEM(0xa0000000, 0x40000000, 0xf8000000)
4BOOT_PARAMS(0xa0000100)
5FIXUP(fixup_cerf)
6MAPIO(cerf_map_io)
7INITIRQ(cerf_init_irq)
8MACHINE_END
第1行的宏设定nr为MACHINE_START_PXA_CERF,name为"CerfBoard PXA Reference Board"。
第3行的宏设定物理内存起始地址为0xa0000000,物理I/O起始地址为0x40000000
第4行的宏设定内核参数的物理地址为0xa0000100
2.4 __create_page_tables
我们的内核起始物理地址是0xA000,8000,虚拟地址是0xC000,8000。下面的代码,是建立内核起始处4MB空间的映射,采用了一级映射方式,即段式(section)映射(非x386的段)方式,每段映射范围为1MB空间。于是我们需要建立4个表项,实现:
虚拟地址0xC000,0000~0xC030,0000映射到物理地址0xA000,0000~0xA030,0000
另外在调用__create_page_tables之前,一些寄存器已存储一些必要的值,如下所示:
r5 = physical address of start of RAM
r6 = physical IO address
r7 = byte offset into page tables for IO
r8 = page table flags
函数调用完成后,r4存储的是页表的地址
1__create_page_tables:
2pgtbl r4, r5@ page table address r4= 0xa0004000
3
4/*
5 * Clear the 16K level 1 swapper page table,
6 * addr 0xa0004000: 0xa0008000 is page table, total 16K
7 */
8movr0, r4@ r0=0xa0004000
9movr3, #0
10addr2, r0, #0x4000@ r2=0xa0008000
111:strr3, [r0], #4@ r3->[r0], r0=r0+#4
12strr3, [r0], #4
13strr3, [r0], #4
14strr3, [r0], #4
15teqr0, r2
16bne1b
17
18/*
19 * Create identity mapping for first MB of kernel to
20 * cater for the MMU enable. This identity mapping
21 * will be removed by paging_init()
22 *
23 *.macrokrnladr, rd, pgtable, rambase
24 *bic\rd, \pgtable, #0x000ff000
25 *.endm
26 */
27krnladrr2, r4, r5@ start of kernel, r2=0xa0000000
28addr3, r8, r2@ flags + kernel base, r3=0xa0000c0e
29strr3, [r4, r2, lsr #18]@ identity mapping
30@ value r3=0xa0000c0e store to addr 0xa0006800
31
32/*
33 * TEXTADDR= 0xC0008000
34 */
35addr0, r4, #(TEXTADDR & 0xff000000) >> 18 @ start of kernel, r0=0xa0007000
36bicr2, r3, #0x00f00000@ r2=0xa0000c0e
37strr2, [r0]@ PAGE_OFFSET + 0MB, 0xa0000c0e->0xa00070000
38addr0, r0, #(TEXTADDR & 0x00f00000) >> 18 @ r0=0xa0007000, no change
39strr3, [r0], #4@ KERNEL + 0MB
40addr3, r3, #1 << 20@ r3=0xa0100c0e ->0xa00070004
41strr3, [r0], #4@ KERNEL + 1MB
42addr3, r3, #1 << 20@ r3=0xa0200c0e ->0xa00070008
43strr3, [r0], #4@ KERNEL + 2MB
44addr3, r3, #1 << 20@ r3=0xa0300c0e ->0xa0007000c
45strr3, [r0], #4@ KERNEL + 3MB
46
47/* r0= 0xa00070010*/
48/*
49 * Ensure that the first section of RAM is present.
50 * we assume that:
51 * 1. the RAM is aligned to a 32MB boundary
52 * 2. the kernel is executing in the same 32MB chunk
53 * as the start of RAM.
54 */
55bicr0, r0, #0x01f00000 >> 18@ round down, r0=0xa0007000
56andr2, r5, #0xfe000000@ round down
57addr3, r8, r2@ flags + rambase
58strr3, [r0]
59
60bicr8, r8, #0x0c@ turn off cacheable and bufferable bits
61 movpc, lr
我已经把每一步涉及的地址详细列出了,读者可以自行对照阅读。
第11~16行,清空页表项从0xA000,4000到0xA000,8000,共16KB
第28行,取得__cpu_mmu_flags。
第35~45行,填写页表项,共4项。
读者可以对照XScale的地址映射手册,因为采用的是段式映射方式,所以每1MB虚拟空间映射到相同的页表表项,根据手册说明,段式映射只有一级表索引,是虚拟地址的前12位;而页式映射的页目录表是前12位,页表是接着的8位,最后12位才是页内偏移,读者一定不要和386的10位页目录表,10位页表的机制相混淆。
我们举个例子说明,对于虚拟地址0xC00x,xxxxx,其前12位为C00,页表基址为0xA000,4000,所以表项地址为0xA000,4000+0xC00<<2=0xA000,7000,而这个地址内容为0xA0000C0E,其前12位0xA00为段基地址,后20位为一些flags,这是从刚才__pxa250_proc _info中取得的。
2.5 处理器初始化
我们来看看xscale系列处理器的初始化函数__xscale_setup
1__xscale_setup:
2movr0, #F_BIT|I_BIT|SVC_MODE
3msrcpsr_c, r0
4mcrp15, 0, ip, c7, c7, 0@ invalidate I, D caches & BTB
5mcrp15, 0, ip, c7, c10, 4@ Drain Write (& Fill) Buffer
6mcrp15, 0, ip, c8, c7, 0@ invalidate I, D TLBs
7mcrp15, 0, r4, c2, c0, 0@ 装载位于r4的页表基址指针
8movr0, #0x1f@ Domains 0, 1 = client
9mcrp15, 0, r0, c3, c0, 0@ load domain access register
10movr0, #1@ Allow user space to access
11mcrp15, 0, r0, c15, c1, 0@ can access CP0 and CP15 only.
12#if CACHE_WRITE_THROUGH
13movr0, #0x20
14#else
15movr0, #0x00
16#endif
17mcrp15, 0, r0, c1, c1, 0@ set auxiliary control reg
18mrcp15, 0, r0, c1, c0, 0@ get control register
19bicr0, r0, #0x0200@ ......R.........
20bicr0, r0, #0x0082@ ........B.....A.
21orrr0, r0, #0x0005@ .............C.M 打开MMU
22orrr0, r0, #0x3900@ ..VIZ..S........
23#ifdef CONFIG_XSCALE_CACHE_ERRATA
24bicr0, r0, #0x0004@ see cpu_xscale_proc_init
25#endif
26movpc, lr
2.6 __ret函数
下面看看__ret函数
1.type__switch_data, %object
2__switch_data:.long__mmap_switched
3.longSYMBOL_NAME(__bss_start)
4.longSYMBOL_NAME(_end)
5.longSYMBOL_NAME(processor_id)
6.longSYMBOL_NAME(__machine_arch_type)
7.longSYMBOL_NAME(cr_alignment)
8.longSYMBOL_NAME(init_task_union)+8192
9
10.type__ret, %function
11__ret:ldrlr, __switch_data
12mcrp15, 0, r0, c1, c0@ turn on mmu
13mrcp15, 0, r0, c1, c0, 0@ read it back.
14movr0, r0
15movr0, r0
16#ifdef CONFIG_PXA
17mrcp15, 0, r0, c2, c0, 0@ arbitrary read of cp15
18movr0, r0@ wait for completion
19sub pc, pc, #4@ flush instruction pipeline
20#endif
21movpc, lr
第11行,将__switch_data符号地址内的内容,即__mmap_switched函数的地址存放到lr,这样__ret结束时,由第21行跳转到__mmap_switched去执行。
init_task_union是在arch/arm/kernel/init_task.c中定义的,放在.init.task区,init_task_union总共有8192字节,从最低地址开始是一个task_struct结构,最高地址是堆栈基址,堆栈向下生长。
2.7 __mmap_switched函数
最后我们看看__mmap_switched函数
1函数执行之前的寄存器情况:
2r0 = processor control register
3r1 = machine ID
4r9 = processor ID
5
6.align5
7__mmap_switched:
8adrr3, __switch_data + 4@ r3=&__bss_start
9ldmiar3, {r4, r5, r6, r7, r8, sp}@ r2 = compat
10@ sp = stack pointer
11
12movfp, #0@ Clear BSS (and zero fp)
131:cmpr4, r5
14strccfp, [r4],#4
15bcc1b
16
17strr9, [r6]@ Save processor ID
18strr1, [r7]@ Save machine type
19#ifdef CONFIG_ALIGNMENT_TRAP
20orrr0, r0, #2@ ...........A.
21#endif
22bicr2, r0, #2@ Clear 'A' bit
23stmiar8, {r0, r2}@ Save control register values
24bSYMBOL_NAME(start_kernel)
第9行执行结果是r4=__bss_start, r5=_end, r6=processor_id, r7= __machine_arch_type, r8= cr_alignment, sp为init_task_union+8192。
第12~15行将__bss_start到_end之间清零。
第17、18行分别将处理器类型和机器类型存储到变量processor_id和__machine_arch_type中,这些变量以后会在start_kernel->setup_arch中使用,来得到当前处理器的struct proc_info_list结构和当前系统的machine_desc结构的数据。
最后,跳转到init/main.c中的start_kernel进入内核启动的第二阶段,从此以后,内核代码就以C语言为主了。
3. 内核启动第二阶段
内核启动第二阶段从init/main.c的start_kernel()函数开始到函数结束,这一阶段对整个系统内存、cache、信号、设备等进行初始化,最后产生新的内核线程init后,调用cpu_idle()完成内核第二阶段。有很多书籍介绍这一部分的内容,我们这里仅仅讲述与xscale结构相关的部分。
3.1 setup_arch
在start_kernel()中我们碰到的第一个重要的函数就是setup_arch()了,这个函数主要任务是初始化bootmem的内存分配管理,初始化页表等:
1void __init setup_arch(char **cmdline_p)
2{
3struct tag *tags = (struct tag *)&init_tags;
4struct machine_desc *mdesc;
5char *from = default_command_line;
6
7ROOT_DEV = MKDEV(0, 255);
8
9setup_processor();
10mdesc = setup_machine(machine_arch_type);
11machine_name = mdesc->name;
12
13if (mdesc->soft_reboot)
14reboot_setup("s");
15
16if (mdesc->param_offset)
17tags = phys_to_virt(mdesc->param_offset);
18
19if (mdesc->fixup)
20mdesc->fixup(mdesc, (struct param_struct *)tags,
21 &from, &meminfo);
第9行,设置当前的processor,主要根据__proc_info_begin和__proc_info_end这2个指针来寻找当前processor的struct proc_info_list结构数据,这个我们前面已经提到过。
第10行,则是根据__arch_info_begin和__arch_info_end这2个指针寻找当前系统的struct machine_desc结构数据。我们假定的系统是采用PXA250的CerfBoard,所以是在arch/arm/mach-pxa/cerf.c中通过宏MACHINE_START来定义的。
第20行,调用特定机器的fixup()函数,此函数的主要作用是设定内存的BANK,保存在全局变量meminfo中,例如,我们的系统的fixup()函数如下:
static void __init fixup_cerf(struct machine_desc *desc, struct param_struct *params,
char **cmdline, struct meminfo *mi)
{
#ifdef CONFIG_PXA_CERF_RAM_128MB
SET_BANK (0, CERF_RAM_BASE, 64*1024*1024);
SET_BANK (1, CERF_RAM_BASE+64*1024*1024, 64*1024*1024);
mi->nr_banks = 2;
#else
SET_BANK (0, CERF_RAM_BASE, CERF_RAM_SIZE);
mi->nr_banks = 1;
#endif
}
我们接着往下看:
22if (tags->hdr.tag != ATAG_CORE)
23convert_to_tag_list(tags);
24
25if (tags->hdr.tag == ATAG_CORE) {
26if (meminfo.nr_banks != 0)
27squash_mem_tags(tags);
28parse_tags(tags);
29}
30
31if (meminfo.nr_banks == 0) {
32meminfo.nr_banks = 1;
33meminfo.bank[0].start = PHYS_OFFSET;
34meminfo.bank[0].size = MEM_SIZE;
35}
36
37init_mm.start_code = (unsigned long) &_text;
38init_mm.end_code = (unsigned long) &_etext;
39init_mm.end_data = (unsigned long) &_edata;
40init_mm.brk = (unsigned long) &_end;
第22~29行,设定内核的tag表,这是bootloader传给内核的
第37~40行,我们知道在arch/arm/kernel/init_task.c中有init_task_union作为系统初始化时的PCB,这个PCB有一个指针active_mm指向init_mm,而init_mm则是系统初始化时的内存管理变量。_text、_etext、_edata和_end都是在内核链接脚本arch/arm/vmliux.lds中定义的虚拟地址。
41memcpy(saved_command_line, from, COMMAND_LINE_SIZE);
42saved_command_line[COMMAND_LINE_SIZE-1] = '\0';
43parse_cmdline(&meminfo, cmdline_p, from);
44bootmem_init(&meminfo);
45paging_init(&meminfo, mdesc);
46request_standard_resources(&meminfo, mdesc);
47
48/*
49 * Set up various architecture-specific pointers
50 */
51init_arch_irq = mdesc->init_irq;
52
53#ifdef CONFIG_VT
54#if defined(CONFIG_VGA_CONSOLE)
55conswitchp = &vga_con;
56#elif defined(CONFIG_DUMMY_CONSOLE)
57conswitchp = &dummy_con;
58#endif
59#endif
60}
第44、45行,分别初始化bootmem的内存分配管理,初始化页表,这两个函数,我们下面会详细介绍。
3.2 bootmem_init