ARM linux的启动部分源代码简略分析
以友善之臂的mini2440开发板为平台,以较新的内核linux-2.6.32.7版本为例,仅作说明之⽤。
当内核映像被加载到RAM之后,Bootloader的控制权被释放。内核映像并不是可直接运⾏的⽬标代码,⽽是⼀个压缩过的zImage(⼩内核)。但是,也并⾮是zImage映像中的⼀切均被压缩了,映像中包含未被压缩的部分,这部分中包含解压缩程序,解压缩程序会解压缩映像中被压缩的部分。zImage使⽤gzip压缩的,它不仅仅是⼀个压缩⽂件,⽽且在这个⽂件的开头部分内嵌有gzip解压缩代码。当zImage被调⽤时它从arch/arm/boot/compressed/head.S的start汇编例程开始执⾏。这个例程进⾏⼀些基本的硬件设置,并调⽤arch/arm/boot/compressed/misc.c中的decompress_kernel()解压缩内核。
arch/arm/kernel/head.S⽂件是内核真正的启动⼊⼝点,⼀般是由解压缩内核的程序来调⽤的。⾸先先看下对于运⾏这个⽂件的要求:MMU = off; D-cache = off; I-cache = ⽆所谓,开也可以,关也可以; r0 = 0;r1 = 机器号;r2 = atags 指针。
这段代码是位置⽆关的,所以,如果以地址0xC0008000来链接内核,那么就可以直接⽤__pa(0xc0008000)地址来调⽤这⾥的代码。
其实,在这个(Linux内核中总共有多达⼏⼗个的以head.S命名的⽂件)head.S⽂件中的⼀项重要⼯作就是设置内核的临时页表,不然mmu开起来也玩不转,但是内核怎么知道如何映射内存呢?linux的内核将映射到虚地址0xCxxx xxxx处,但他怎么知道在4GB的地址空间中有哪⼀⽚ram是可⽤的,从⽽可以映射过去呢?
因为不同的系统有不通的内存映像,所以,LINUX约定,要调⽤内核代码,⼀定要满⾜上⾯的调⽤要求,以为最初的内核代码提供⼀些最重要的关于机器的信息。内核代码开始的时候,R1存放的是系统⽬标平台的代号,对于⼀些常见的,标准的平台,内核已经提供了⽀持,只要在编译的时候选中就⾏了,例如对X86平台,内核是从物理地址1M开始映射的。好了好了,看下⾯的代码。
ENTRY(stext)是这个⽂件的⼊⼝点。最初的⼏⾏是这样的: setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9@ ensure svc mode@ and irqs disabled
// 设置为SVC模式,关闭中断和快速中断
// 此处设定系统的⼯作状态为SVC,arm有7种状态每种状态
// 都有⾃⼰的堆栈,SVC为管理模式,具有完全的权限,可以执⾏任意指令// 访问任意地址的内存
// setmode是⼀个宏,其定义为:// .macro setmode, mode, reg// msr cpsr_c, #\\mode// .endm
mrc p15, 0, r9, c0, c0 @ get processor id
bl __lookup_processor_type @ r5=procinfo r9=cpuid movs r10, r5 @ invalid processor (r5=0)? beq __error_p @ yes, error 'p'
这⼏⾏是查询处理器的类型的,我们知道arm系列有很多型号,arm7、arm9、arm11、Cortex核等等类型,这么多型号要如何区分呢?其
实,在arm的15号协处理器(其实ARM暂时也就这么⼀个协处理器)中有⼀个只读寄存器,存放与处理器相关信息。
__lookup_processor_type是arch/arm/kernel/head-common.S⽂件中定义的⼀个例程,这个head-common.S⽤include命令被包含在head.S⽂件中。其定义为:__lookup_processor_type: adr r3, 3f ldmia r3, {r5 - r7} add r3, r3, #8
sub r3, r3, r7 @ get offset between virt&phys add r5, r5, r3 @ convert virt addresses to add r6, r6, r3 @ physical address space1: ldmia r5, {r3, r4} @ value, mask and r4, r4, r9 @ mask wanted bits teq r3, r4 beq 2f
add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list) cmp r5, r6 blo 1b
mov r5, #0 @ unknown processor2: mov pc, lr
ENDPROC(__lookup_processor_type)
这个例程接受处理器ID(保存在寄存器r9中)为参数,查找链接器建⽴的⽀持的处理器表。此时此刻还不能使⽤__proc_info表的绝对地址,因为这时候MMU还没有开启,所以此时运⾏的程序没有在正确的地址空间中。所以不得不计算偏移量。若没有找到processor ID对应的处理器,则在r5寄存器中返回返回0,否则返回⼀个proc_info_list结构体的指针(在物理地址空间)。proc_info_list结构体在 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; }; 第⼀项是CPU id,将与协处理器中读出的id作⽐较,其余的字段也都是与处理器相关的信息,到下⾯初始化的过程中⾃然会⽤到。 另外,这个例程加载符地址的代码也是挺值得我辈学习的: adr r3, 3f 加载⼀个符号的地址,这个符号在加载语句前⾯(下⾯)定义,forward嘛,这个符号为3,离这条语句最近的那个。在那个符号为3的位置我们看到这样的代码: .align 2 3: .long __proc_info_begin .long __proc_info_end4: .long . .long __arch_info_begin .long __arch_info_end 搜索这两个符号的值,在⽂件arch/arm/kernel/vmlinux.lds.S中: __proc_info_begin = .; *(.proc.info.init) __proc_info_end = .; 这两个符号分别是⼀种初始化的段的结束开始地址和结束地址。为了了解由struct proc_info_list结构体组成的段的实际构成,我们还是得要了解⼀下在系统中到底都有哪些变量是声明了要被放到这个段的。⽤关键字.proc.info.init来搜,全部都是arch/arm/mm/proc-*.S⽂件,这些都是特定于处理器的汇编语⾔⽂件,对于我们的mini2440, ⾃然是要看proc-arm920.S⽂件的,在其中可以看到这些内容: .section \".proc.info.init\ .type __arm920_proc_info,#object__arm920_proc_info: .long 0x41009200 .long 0xff00fff0 .long PMD_TYPE_SECT | \\ PMD_SECT_BUFFERABLE | \\ PMD_SECT_CACHEABLE | \\ PMD_BIT4 | \\ PMD_SECT_AP_WRITE | \\ PMD_SECT_AP_READ .long PMD_TYPE_SECT | \\ PMD_BIT4 | \\ PMD_SECT_AP_WRITE | \\ PMD_SECT_AP_READ b __arm920_setup .long cpu_arch_name .long cpu_elf_name .long HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB .long cpu_arm920_name .long arm920_processor_functions .long v4wbi_tlb_fns .long v4wb_user_fns #ifndef CONFIG_CPU_DCACHE_WRITETHROUGH .long arm920_cache_fns#else .long v4wt_cache_fns#endif .size __arm920_proc_info, . - __arm920_proc_info 看到这⼉我们再回国头去看__lookup_processor_type的代码: ldmia r3, {r5 - r7} add r3, r3, #8 sub r3, r3, r7 尽管符号3处只有两个有效值,但它加载了三个数,⽽第三个数,我们看到是这样定义的:.long . __lookup_processor_type中,给r3加上8,也就是让r3指向“.”的地址,然后⽤r3减r7来获取虚拟地址与物理地址的差,这样看来,“.”就应该是虚拟空间(编译地址)⾥那个数据的地址。 之后的代码获得__proc_info_begin和__arch_info_end这两个符号在物理空间中的地址: add r5, r5, r3 @ convert virt addresses to add r6, r6, r3 然后便是在那个段中逐个的检查struct proc_info_list结构体,以找到与我们的CPU相匹配的:1: ldmia r5, {r3, r4} @ value, mask and r4, r4, r9 @ mask wanted bits teq r3, r4 beq 2f add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list) cmp r5, r6 blo 1b mov r5, #0 @ unknown processor2: mov pc, lr __lookup_processor_type例程会返回在⽂件arch/arm/mm/proc-arm920.S中定义的⼀个保存有与我们的处理器相关的信息的structproc_info_list结构体的地址。 接下来我们继续看stext的代码: bl __lookup_machine_type @ r5=machinfo movs r8, r5 @ invalid machine (r5=0)? beq __error_a @ yes, error 'a' 在获得了处理器信息之后,则调⽤__lookup_machine_type来查找机器信息。这个例程同样也在arch/arm/kernel/head-common.S⽂件中定义。这个例程的定义如下:__lookup_machine_type: adr r3, 4b ldmia r3, {r4, r5, r6} sub r3, r3, r4 @ get offset between virt&phys add r5, r5, r3 @ convert virt addresses to add r6, r6, r3 @ physical address space1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type teq r3, r1 @ matches loader number? beq 2f @ found add r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desc cmp r5, r6 blo 1b mov r5, #0 @ unknown machine2: mov pc, lr ENDPROC(__lookup_machine_type) 处理的过程和上⾯的__lookup_processor_type还是挺相似的。这个例程接收r1中传进来的机器号作为参数,然后,在⼀个由struct machine_desc结构体组成的段中查找和我们的机器号匹配的struct machine_desc结构体,这个结构体在arch/arm/include/asm/mach/arch.h⽂件中定义,⽤于保存机器的信息:struct machine_desc { /* * Note! The first four elements are used * by assembler code in head.S, head-common.S */ unsigned int nr; /* architecture number */ unsigned int phys_io; /* start of physical io */ unsigned int io_pg_offst; /* byte offset for io * page tabe entry */ const char *name; /* architecture name */ unsigned long boot_params; /* tagged list */ unsigned int video_start; /* start of video RAM */ unsigned int video_end; /* end of video RAM */ unsigned int reserve_lp0 :1; /* never has lp0 */ unsigned int reserve_lp1 :1; /* never has lp1 */ unsigned int reserve_lp2 :1; /* never has lp2 */ unsigned int soft_reboot :1; /* soft reboot */ void (*fixup)(struct machine_desc *, struct tag *, char **, struct meminfo *); void (*map_io)(void);/* IO mapping function */ void (*init_irq)(void); struct sys_timer *timer; /* system tick timer */ void (*init_machine)(void);}; 同样这个例程也⽤到了同上⾯很相似的⽅式来获得符号的地址: adr r3, 4b b代表back,即向后,这个符号为4,紧接着我们前⾯看到的那个为3的标号:4: .long . .long __arch_info_begin .long __arch_info_end 在⽂件arch/arm/kernel/vmlinux.lds.S中我们可以看到段的定义: __arch_info_begin = .; *(.arch.info.init) __arch_info_end = .; 这两个符号也是分别表⽰某种初始化的段的开始地址和结束地址。为了找到段的填充内容,还是得要了解⼀下到底都有哪些structmachine_desc结构体类型变量声明了要被放到这个段的。⽤关键字.arch.info.init 来搜索所有的内核源⽂件。在arch/arm/include/asm/mach/arch.h⽂件中我们看到:#define MACHINE_START(_type,_name) \\ static const struct machine_desc __mach_desc_##_type \\ __used \\ __attribute__((__section__(\".arch.info.init\"))) = { \\ .nr = MACH_TYPE_##_type, \\ .name = _name, #define MACHINE_END \\}; 定义机器结构体,也就是.arch.info.init段中的内容,都是要通过两个宏MACHINE_START和MACHINE_END来完成的 啊,MACHINE_START宏定义⼀个truct machine_desc结构体,并初始化它的机器号字段和机器名字段,可以在arch/arm/tools/mach-types⽂件中看到各种平台的机器号的定义。那接着我们来搜MACHINE_START吧,这是⼀个⽤于定义机器结构体的宏,所以可以看到这个符号好像都是在arch/arm/mach-*/mach-*.c这样的⽂件中出现的,我们感兴趣的应该是arch/arm/mach-s3c2440/ mach-mini2440.c⽂件中的这个符号: MACHINE_START(MINI2440, \"MINI2440\") /* Maintainer: Michel Pollet .io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc, .boot_params = S3C2410_SDRAM_PA + 0x100, .map_io = mini2440_map_io, .init_machine = mini2440_init, .init_irq = s3c24xx_init_irq, .timer = &s3c24xx_timer,MACHINE_END OK, __lookup_machine_type这个例程的我们也搞明⽩了。回忆⼀下,启动代码现在已经完成的⼯作,R10寄存器中为指向proc_info_list结构体的指针(物理地址空间),这个结构体包含有关于我们的处理器的⼀些重要信息。R8寄存器中为指向⼀个与我们的平台相匹配的machine_desc结构体的指针,这个结构体中保存有⼀些关于我们的平台的重要信息。 回来接着看arch/arm/kernel/head.S⽂件中的stext: bl __vet_atags 这个例程同样同样也是在arch/arm/kernel/head-common.S⽂件中定义:__vet_atags: tst r2, #0x3 @ aligned? bne 1f ldr r5, [r2, #0] @ is first tag ATAG_CORE? cmp r5, #ATAG_CORE_SIZE cmpne r5, #ATAG_CORE_SIZE_EMPTY bne 1f ldr r5, [r2, #4] ldr r6, =ATAG_CORE cmp r5, r6 bne 1f mov pc, lr @ atag pointer is ok 1: mov r2, #0 mov pc, lr ENDPROC(__vet_atags) 这个例程接收机器信息(R8寄存器)为参数,并检测r2中传⼊的ATAGS 指针的合法性。内核使⽤tag来作为bootloader传递内核参数的⽅式。系统要求r2中传进来的ATAGS指针式4字节对齐的,同时要求ATAGS列表的第⼀个tag是⼀个ATAG_CORE类型的。 此时R10寄存器中保存有指向CPU信息结构体的指针,R8寄存器中保存有指向机器结构体的指针,R2寄存器中保存有指向tag表的指针,R9中还保存有CPU ID信息。 回到arch/arm/kernel/head.S⽂件中的stext,之后就要进⼊初始化过程中⽐较关键的⼀步了,开始设置mmu,但⾸先要填充⼀个临时的内核页表,映射4m的内存,这在初始化过程中是⾜够了:bl __create_page_tables 这个例程设置初始页表,这⾥只设置最起码的数量,只要能使内核运⾏即可,r8 = machinfo,r9 = cpuid,r10 = procinfo,在r4寄存器中返回物理页表地址。 __create_page_tables例程在⽂件arch/arm/kernel/head.S中定义: __create_page_tables: pgtbl r4 @ page table address// pgtbl是⼀个宏,本⽂件的前⾯部分有定义:// .macro pgtbl, rd // ldr \\rd, =(KERNEL_RAM_PADDR - 0x4000)// .endm // KERNEL_RAM_PADDR在本⽂件的前⾯有定义,为(PHYS_OFFSET + TEXT_OFFSET)// PHYS_OFFSET在arch/arm/mach-s3c2410/include/mach/memory.h定义,// 为UL(0x30000000) // ⽽TEXT_OFFSET在arch/arm/Makefile中定义,为内核镜像在内存中到内存// 开始位置的偏移(字节),为$(textofs-y)// textofs-y也在⽂件arch/arm/Makefile中定义,// 为textofs-y := 0x00008000 // r4 = 30004000为临时页表的起始地址 // ⾸先即是初始化16K的页表,⾼12位虚拟地址为页表索引,所以为// 4K*4 = 16K,⼤页表,每⼀个页表项,映射1MB虚拟地址。// 这个地⽅还来了个循环展开,以优化性能。 mov r0, r4 mov r3, #0 add r6, r0, #0x40001: str r3, [r0], #4 str r3, [r0], #4 str r3, [r0], #4 str r3, [r0], #4 teq r0, r6 bne 1b ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags // PROCINFO_MM_MMUFLAGS在arch/arm/kernel/asm-offsets.c⽂件中定义,// 为DEFINE(PROCINFO_MM_MMUFLAGS, // offsetof(struct proc_info_list, __cpu_mm_mmu_flags)); // R10寄存器保存的指针指向是我们前⾯找到的proc_info_list结构嘛。 // 为内核的第⼀个MB创建⼀致的映射,以为打开MMU做准备,这个映射将会被// paging_init()移除,这⾥使⽤程序计数器来获得相应的段的基地址。// 这个地⽅是直接映射。 mov r6, pc mov r6, r6, lsr #20 @ start of kernel section orr r3, r7, r6, lsl #20 @ flags + kernel base str r3, [r4, r6, lsl #2] @ identity mapping // 接下来为内核的直接映射区设置页表。KERNEL_START在⽂件的前⾯定义,// 为KERNEL_RAM_VADDR,即内核的虚拟地址。 // ⽽KERNEL_RAM_VADDR在⽂件的前⾯定义,则为(PAGE_OFFSET + TEXT_OFFSET)// 映射完整的内核代码段,初始化数据段。// PAGE_OFFSET为内核镜像开始的虚拟地址,在 // arch/arm/include/asm/memory.h中定义。在配置内核时选定具体值,默认// 为0xC0000000。 // 因为最⾼12位的值是页表中的偏移地址,⽽第三⾼的四位必然为0, // 每个页表项为4字节,右移20位之后,还得再左移两位回来,所以,这⾥只// 是左移18位。// R3寄存器在经过了上⾯的操作之后,实际上是变成了指向内核镜像代码段// 的指针(物理地址),在这个地⽅,再⼀次为内核镜像的第⼀个MB做了映射。// R6随后指向了内核镜像的尾部。R0为页表项指针。// 这⾥以1MB为单位来映射内核镜像。 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 strls r3, [r0], #4 bls 1b // 为了使⽤启动参数,将物理内存的第⼀MB映射到内核虚拟地址空间的// 第⼀个MB,r4存放的是页表的地址。这⾥的PAGE_OFFSET的虚拟地址// ⽐上⾯的KERNEL_START要⼩0x8000 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] // 上⾯的这个步骤显得似乎有些多余。 // 总结⼀下,这个建⽴临时页表的过程:// 1、为内核镜像的第⼀个MB建⽴直接映射 // 2、为内核镜像完整的建⽴从虚拟地址到物理地址的映射 // 3、为物理内存的第⼀个MB建⽴到内核的虚拟地址空间的第⼀个MB的映射。 // OK,内核的临时页表建⽴完毕。整个初始化临时页表的过程都没有修改R8,// R9和R10。 mov pc, lr ENDPROC(__create_page_tables) 回到stext: ldr r13, __switch_data @ address to jump to after @ mmu has been enabled 这个地⽅实际上是在r13中保存了另⼀个例程的地址。后⾯的分析中,遇到执⾏到这个例程的情况时会有详细说明。 接着看stext: adr lr, BSYM(__enable_mmu) @ return (PIC) address BSYM()是⼀个宏,在⽂件arch/arm/include/asm/unified.h中定义,为:#define BSYM(sym) sym 也就是说这个语句也仅仅是把__enable_mmu例程的地址加载进lr寄存器中。为了⽅便之后调⽤的函数返回时,直接执⾏__enable_mmu例程。 接着看stext下⼀句: ARM( add pc, r10, #PROCINFO_INITFUNC ) ARM()也是⼀个宏,同样在⽂件arch/arm/include/asm/unified.h中定义,当配置内核为⽣成ARM镜像,则为:#define ARM(x...) x 所以这⼀条语句也就是在调⽤⼀个例程。R10中保存的是procinfo结构的地址。PROCINFO_INITFUNC符号在arch/arm/kernel/asm-offsets.c⽂件中定义,为: DEFINE(PROCINFO_INITFUNC, offsetof(struct proc_info_list, __cpu_flush)); 也就是调⽤结构体proc_info_list的__cpu_flush成员函数。回去查看arch/arm/mm/proc-arm920.S⽂件中struct proc_info_list结构体的变量的定义,可以看到这个成员为: b __arm920_setup 也就是说,在设置好内核临时页表之后调⽤了例程__arm920_setup,这个例程同样在arch/arm/mm/proc-arm920.S中:__arm920_setup: mov r0, #0 mcr p15, 0, r0, c7, c7 @ invalidate I,D caches on v4 mcr p15, 0, r0, c7, c10, 4 @ drain write buffer on v4#ifdef CONFIG_MMU mcr p15, 0, r0, c8, c7 @ invalidate I,D TLBs on v4#endif adr r5, arm920_crval ldmia r5, {r5, r6} mrc p15, 0, r0, c1, c0 @ get control register v4 bic r0, r0, r5 orr r0, r0, r6 mov pc, lr 这⼀段⾸先使i,d caches内容⽆效,然后清除write buffer,接着使TLB内容⽆效。接下来加载变量arm920_crval的地址,我们看到arm920_crval变量的内容为:rm920_crval: crval clear=0x00003f3f, mmuset=0x00003135, ucset=0x00001130 crval为⼀个宏,在arch/arm/mm/proc-macros.S中定义: .macro crval, clear, mmuset, ucset#ifdef CONFIG_MMU .word \\clear .word \\mmuset#else .word \\clear .word \#endif .endm 其实也就是定义两个变量⽽已。之后,在r0中,得到了我们想要往协处理器相应寄存器中写⼊的内容。 之后的 __arm920_setup返回,mov pc, lr,即是调⽤例程__enable_mmu,这个例程在⽂件arch/arm/kernel/head.S中:__enable_mmu: #ifdef CONFIG_ALIGNMENT_TRAP 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 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 b __turn_mmu_on 在这⼉设置了页⽬录地址(r4寄存器中保存),然后设置domain的保护,在前⾯建⽴页表的例程中,注意到,页表项的控制信息,是从struct proc_info_list结构体的某字段中取的,其页⽬录项的 domain都是0,domain寄存器中的domain 0对应的是0b11,表⽰访问模式为manager,不受限制。在这⾥同时也完成r0的某些位的进⼀步设置。 然后,__enable_mmu例程⼜调⽤了__turn_mmu_on,在同⼀个⽂件中定义:__turn_mmu_on: mov r0, r0 mcr p15, 0, r0, c1, c0, 0 @ write control reg mrc p15, 0, r3, c0, c0, 0 @ read id reg mov r3, r3 mov r3, r13 mov pc, r3 ENDPROC(__turn_mmu_on)接下来写控制寄存器:mcr p15, 0, r0, c1, c0 ,0 ⼀切设置就此⽣效,到此算是完成了打开d,icache和mmu的⼯作。 注意:arm的d cache必须和mmu⼀起打开,⽽i cache可以单独打开。其实,cache和mmu的关系实在是紧密,每⼀个页表项都有标志标⽰是否是cacheable的,可以说本来就是设计⼀起使⽤的 前⾯有提到过,r13中存放的其实是另外⼀个例程的地址,其值是变量__switch_data的第⼀个字段,即⼀个函数指针的值,__switch_data变量是在arch/arm/kernel/head-common.S中定义的:__switch_data: .long __mmap_switched .long __data_loc @ r4 .long _data @ 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 前⾯的ldr r13 __switch_data,实际上也就是加载符号__mmap_switched的地址,实际上__mmap_switched是⼀个arch/arm/kernel/head-common.S中定义的例程。接着来看这个例程的定义,在arch/arm/kernel/head-common.S⽂件中: __mmap_switched: adr r3, __switch_data + 4 ldmia r3!, {r4, r5, r6, r7} cmp r4, r5 @ Copy data segment if needed1: cmpne r5, r6 ldrne fp, [r4], #4 strne fp, [r5], #4 bne 1b mov fp, #0 @ Clear BSS (and zero fp)1: cmp r6, r7 strcc fp, [r6],#4 bcc 1b ldmia r3, {r4, r5, r6, r7, sp} 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)这个例程完成如下⼯作: 1、使r3指向__switch_data变量的第⼆个字段(从1开始计数)。 2、执⾏了⼀条加载指令,也就是在r4, r5, r6, r7寄存器中分别加载4个符号__data_loc,_data, __bss_start ,_end的地址,这四个符号都是在链接脚本arch/arm/kernel/vmlinux.lds.S中出现的,标识了镜像各个段的地址,我们应该不难猜出他们所代表的段。3、如果需要的话则复制数据段(数据段和BSS段是紧邻的)。 4、初始化BSS段,全部清零,BSS是未初始化的全局变量区域。 5、⼜看到⼀条加载指令,同样在⼀组寄存器中加载借个符号的地址,r4中为processor_id,r5中为__machine_arch_type, r6中为__atags_pointer, r7中为cr_alignment ,sp中为init_thread_union + THREAD_START_SP。 6、接着我们看到下⾯的⼏条语句,则是⽤前⾯获取的信息来初始化那些全局变量r9,机器号被保存到processor_id处;r1寄存器的值,机器号,被保存到变量__machine_arch_type中,其他的也⼀样。 7、重新设置堆栈指针,指向init_task的堆栈。init_task是系统的第⼀个任务,init_task的堆栈在task structure的后8K,我们后⾯会看到。 8、最后就要跳到C代码的 start_kernel。 b start_kernel 到此为⽌,汇编部分的初始化代码就结束了 O,My God.初始化代码的汇编部分终于结束。从⽽进⼊了与体系结构⽆关的Linux内核部分。start_kernel()会调⽤⼀系列初始化函数来设置中断,执⾏进⼀步的内存配置。 现在让我们来回忆⼀下⽬前的系统状态: 临时页表已经建⽴,在0X30004000处,映射了映像⽂件⼤⼩空间,虚地址0XC000000被映射到0X30000000。CACHE,MMU 都已经打开。堆栈⽤的是任务init_task的堆栈。 如果以为到了c代码可以松⼀⼝⽓的话,就⼤错特措了,linux的c也不⽐汇编好懂多少,相反倒掩盖了汇编的⼀些和机器相关的部分,有时候更难懂。其实作 为编写操作系统的c代码,只不过是汇编的另⼀种写法,和机器代码的联系是很紧密的。另外,这些start_kernel()中调⽤的C函数,每⼀个都具有举⾜轻重的地位,它们中的许多都肩负着初始化内核中的某个⼦系统的重要使命,⽽Linux内核中每⼀个⼦系统都错综复杂,牵涉到各种软件、硬件的复杂算法,所以理解起来倒真的是挺困难的。 start_kernel函数在 init/main.c中定义:528 asmlinkage void __init start_kernel(void)529 { 530 char * command_line; 531 extern struct kernel_param __start___param[], __stop___param[];532 533 smp_setup_processor_id();534535 /* 536 * Need to run as early as possible, to initialize the537 * lockdep hash:538 */ 539 lockdep_init(); 540 debug_objects_early_init();541542 /* 543 * Set up the the initial canary ASAP:544 */ 545 boot_init_stack_canary();546 547 cgroup_init_early();548 549 local_irq_disable();550 early_boot_irqs_off();551 early_init_irq_lock_class();552553 /* 554 * Interrupts are still disabled. Do necessary setups, then555 * enable them556 */ 557 lock_kernel();558 tick_init(); 559 boot_cpu_init();560 page_address_init(); 561 printk(KERN_NOTICE \"%s\562 setup_arch(&command_line);563 mm_init_owner(&init_mm, &init_task);564 setup_command_line(command_line);565 setup_nr_cpu_ids();566 setup_per_cpu_areas(); 567 smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */568 569 build_all_zonelists();570 page_alloc_init();571 572 printk(KERN_NOTICE \"Kernel command line: %s\\n\573 parse_early_param(); 574 parse_args(\"Booting kernel\ 575 __start___param, __stop___param - __start___param,576 &unknown_bootoption);577 /* 578 * These use large bootmem allocations and must precede579 * kmem_cache_init()580 */ 581 pidhash_init(); 582 vfs_caches_init_early();583 sort_main_extable();584 trap_init();585 mm_init();586 /* 587 * Set up the scheduler prior starting any interrupts (such as the588 * timer interrupt). Full topology setup happens at smp_init()589 * time - but meanwhile we still have a functioning scheduler.590 */ 591 sched_init();592 /* 593 * Disable preemption - early bootup scheduling is extremely594 * fragile until we cpu_idle() for the first time.595 */ 596 preempt_disable();597 if (!irqs_disabled()) { 598 printk(KERN_WARNING \"start_kernel(): bug: interrupts were \"599 \"enabled *very* early, fixing it\\n\");600 local_irq_disable();601 }602 rcu_init();603 radix_tree_init(); 604 /* init some links before init_ISA_irqs() */605 early_irq_init();606 init_IRQ();607 prio_tree_init();608 init_timers();609 hrtimers_init();610 softirq_init();611 timekeeping_init();612 time_init();613 profile_init();614 if (!irqs_disabled()) 615 printk(KERN_CRIT \"start_kernel(): bug: interrupts were \"616 \"enabled early\\n\");617 early_boot_irqs_on();618 local_irq_enable();619 620 /* Interrupts are enabled now so all GFP allocations are safe. */621 gfp_allowed_mask = __GFP_BITS_MASK;622 623 kmem_cache_init_late();624625 /* 626 * HACK ALERT! This is early. We're enabling the console before627 * we've done PCI setups etc, and console_init() must be aware of628 * this. But we do want output early, in case something goes wrong.629 */ 630 console_init();631 if (panic_later) 632 panic(panic_later, panic_param);633 634 lockdep_info();635636 /* 637 * Need to run this when irqs are enabled, because it wants638 * to self-test [hard/soft]-irqs on/off lock inversion bugs639 * too:640 */ 641 locking_selftest();642 643 #ifdef CONFIG_BLK_DEV_INITRD644 if (initrd_start && !initrd_below_start_ok && 645 page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {646 printk(KERN_CRIT \"initrd overwritten (0x%08lx < 0x%08lx) - \"647 \"disabling it.\\n\ 648 page_to_pfn(virt_to_page((void *)initrd_start)),649 min_low_pfn);650 initrd_start = 0;651 }652 #endif 653 page_cgroup_init();654 enable_debug_pagealloc();655 kmemtrace_init();656 kmemleak_init(); 657 debug_objects_mem_init();658 idr_init_cache(); 659 setup_per_cpu_pageset();660 numa_policy_init();661 if (late_time_init)662 late_time_init();663 sched_clock_init();664 calibrate_delay();665 pidmap_init();666 anon_vma_init();667 #ifdef CONFIG_X86668 if (efi_enabled) 669 efi_enter_virtual_mode();670 #endif 671 thread_info_cache_init();672 cred_init(); 673 fork_init(totalram_pages);674 proc_caches_init();675 buffer_init(); 676 key_init();677 security_init(); 678 vfs_caches_init(totalram_pages);679 signals_init(); 680 /* rootfs populating might need page-writeback */681 page_writeback_init();682 #ifdef CONFIG_PROC_FS683 proc_root_init();684 #endif685 cgroup_init();686 cpuset_init();687 taskstats_init_early();688 delayacct_init();689 690 check_bugs();691 692 acpi_early_init(); /* before LAPIC and SMP init */693 sfi_init_late();694 695 ftrace_init();696 697 /* Do the rest non-__init'ed, we're now alive */698 rest_init();699 } 接着我们来近距离的观察⼀下start_kernel函数中调⽤的这些重量级的函数。 ⾸先来看setup_arch(&command_line)函数,这个函数(对于我们的mini2440平台来说)在arch/arm/kernel/setup.c中定义:664 void __init setup_arch(char **cmdline_p)665 { 666 struct tag *tags = (struct tag *)&init_tags;667 struct machine_desc *mdesc;668 char *from = default_command_line;669 670 unwind_init();671 672 setup_processor(); 673 mdesc = setup_machine(machine_arch_type);674 machine_name = mdesc->name; 675 676 if (mdesc->soft_reboot)677 reboot_setup(\"s\");678 679 if (__atags_pointer) 680 tags = phys_to_virt(__atags_pointer);681 else if (mdesc->boot_params) 682 tags = phys_to_virt(mdesc->boot_params);683684 /* 685 * If we have the old style parameters, convert them to686 * a tag list.687 */ 688 if (tags->hdr.tag != ATAG_CORE)689 convert_to_tag_list(tags);690 if (tags->hdr.tag != ATAG_CORE)691 tags = (struct tag *)&init_tags;692 693 if (mdesc->fixup) 694 mdesc->fixup(mdesc, tags, &from, &meminfo);695 696 if (tags->hdr.tag == ATAG_CORE) {697 if (meminfo.nr_banks != 0)698 squash_mem_tags(tags);699 save_atags(tags);700 parse_tags(tags);701 }702 703 init_mm.start_code = (unsigned long) _text;704 init_mm.end_code = (unsigned long) _etext;705 init_mm.end_data = (unsigned long) _edata;706 init_mm.brk = (unsigned long) _end;707 708 /* parse_early_param needs a boot_command_line */709 strlcpy(boot_command_line, from, COMMAND_LINE_SIZE);710 711 /* populate cmd_line too for later use, preserving boot_command_line */712 strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);713 *cmdline_p = cmd_line; 714 715 parse_early_param();716 717 paging_init(mdesc); 718 request_standard_resources(&meminfo, mdesc);719 720 #ifdef CONFIG_SMP721 smp_init_cpus();722 #endif723 724 cpu_init();725 tcm_init();726727 /* 728 * Set up various architecture-specific pointers729 */ 730 init_arch_irq = mdesc->init_irq;731 system_timer = mdesc->timer;732 init_machine = mdesc->init_machine;733 734 #ifdef CONFIG_VT 735 #if defined(CONFIG_VGA_CONSOLE)736 conswitchp = &vga_con; 737 #elif defined(CONFIG_DUMMY_CONSOLE)738 conswitchp = &dummy_con;739 #endif740 #endif 741 early_trap_init();742 } 来看⼀些我们⽐较感兴趣的地⽅: 1、666⾏,struct tag指针类型的局部变量指向了默认的tag列表init_tags,该静态变量在setup_arch()定义同⽂件的前⾯有如下定义:636 /* 637 * This holds our defaults.638 */ 639 static struct init_tags {640 struct tag_header hdr1;641 struct tag_core core;642 struct tag_header hdr2;643 struct tag_mem32 mem; 644 struct tag_header hdr3;645 } init_tags __initdata = { 646 { tag_size(tag_core), ATAG_CORE },647 { 1, PAGE_SIZE, 0xff }, 648 { tag_size(tag_mem32), ATAG_MEM },649 { MEM_SIZE, PHYS_OFFSET },650 { 0, ATAG_NONE }651 }; 第679⾏检察__atags_pointer指针的有效性,这个指针是在前⾯,跳转到start_kernel函数的汇编例程最后设置的⼏个变量之⼀,⽤的是R2寄存器的值。如果bootloader通过R2传递了tag列表的话,⾃然是要使⽤bootloader穿的进来的tag列表的。 2、第688⾏的字符指针类型的局部变量from指向了default_command_line静态变量,这个变量同样在前⾯有定义:124 static char default_command_line[COMMAND_LINE_SIZE] __initdata = CONFIG_CMDLINE;传递给内核的命令⾏参数,是可以在内核配置的时候设置的。 3、第673⾏以machine_arch_type为参数调⽤了setup_machine()函数,⽽这个函数的定义为:369 static struct machine_desc * __init setup_machine(unsigned int nr)370 { 371 struct machine_desc *list;372373 /* 374 * locate machine in the list of supported machines.375 */ 376 list = lookup_machine_type(nr);377 if (!list) { 378 printk(\"Machine configuration botched (nr %d), \"379 \" unable to continue.\\n\380 while (1);381 }382 383 printk(\"Machine: %s\\n\384 385 return list;386 } 在arch/arm/kernel/head-common.S⽂件中,我们看到了⼀个对于__lookup_machine_type例程的封装的可被C语⾔程序调⽤的汇编语⾔编写的函数lookup_machine_type(),接收机器号,查表,然后返回匹配的struct machine_desc结构体的指针。在这⾥,对于我们的mini2440,返回的⾃然是arch/arm/mach-s3c2440/ mach-mini2440.c⽂件中定义的结构体了:MACHINE_START(MINI2440, \"MINI2440\") /* Maintainer: Michel Pollet .io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc, .boot_params = S3C2410_SDRAM_PA + 0x100, .map_io = mini2440_map_io, .init_machine = mini2440_init, .init_irq = s3c24xx_init_irq, .timer = &s3c24xx_timer,MACHINE_END 然后,machine_desc结构体的name成员的值被赋给全局变量machine_name。 第681⾏,若bootloader没有传递tag列表给内核,则检测machine_desc结构体的boot_params字段,看看特定的平台是否传递了标记列表。第730、731、732⾏分别将machine_desc结构体的init_irq、timer和init_machine成员值赋给了三个全局变量init_arch_irq、system_timer和init_machine,即是设置特定体系结构的指针。初始化的后⾯阶段⾃然会⽤到。 start_kernel()函数调⽤同⽂件下的rest_init(void)函数,rest_init(void)函数调⽤ kernel_thread()函数以启动第⼀个核⼼线程,该线程执⾏kernel_init()函数,⽽原执⾏序列会调⽤cpu_idle(),等待调度。 作为核⼼线程的kernel_init()函数继续完成⼀些设置,并在最后调⽤同⽂件下的init_post()函数,⽽该函数挂在根⽂件系统,打开/dev/console设备,重定向stdin、stdout和stderr到控制台。之后,它搜索⽂件系统中的init程序(也可以由“init=”命令⾏参数指定init程序),并使⽤run_init_process()函数执⾏init程序。(事实上,run_init_process()函数⼜调⽤了kernel_execve()来实际执⾏程序)。搜索init程序的顺序为/sbin/init、/etc/init、/bin/init、和/bin/sh。在嵌⼊式系统中,多数情况下,可以给内核传⼊⼀个简单的shell脚本来启动必需的嵌⼊式应⽤程序。 ⾄此,漫长的Linux内核引导和启动过程就结束了,⽽kernel_init()对应的由rest_init(void)函数创建的第⼀个线程也进⼊⽤户模式。 参考⽂献: arm 嵌⼊式LINUX启动过程:Linux设备驱动开发详解,宋宝华 因篇幅问题不能全部显示,请点此查看更多更全内容