以TI8168evm开发板为平台,以较新的内核linux-2.6.37版本为例,仅作说明之用。
当内核映像被加载到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开始映射的。
分析head.S中的启动代码;
ENTRY(stext)是这个文件的入口点。最初的几行是这样的:
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)?
这几行是查询处理器的类型的,我们知道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, __lookup_processor_type_data
ldmia r3, {r4 - r6}
sub r3, r3, r4 @get offset between virt&phys
add r5, r5, r3 @convert virt addresses to
add r6, r6, r3 @physical address space
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 processor
2: mov pc, lr
ENDPROC(__lookup_processor_type)
这个例程接受处理器ID(保存在寄存器r9中)为参数,查找链接器建立的支持的处理器表。此时此刻还不能使用__proc_info表的绝对地址,因为这时候MMU还没有开启,所以此时运行的程序没有在正确的地址空间中。所以不得不计算偏移量。若没有找到processor ID对应的处理器,则在r5寄存器中返回返回0,否则返回一个proc_info_list结构体的指针(在物理地址空间)。proc_info_list结构体在<asm/procinfo.h>文件中定义:
struct proc_info_list {
unsigned int cpu_val;
unsigned int cpu_mask;
unsignedlong __cpu_mm_mmu_flags; /*used by head.S */
unsignedlong __cpu_io_mmu_flags; /*used by head.S */
unsignedlong __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,__lookup_processor_type_data
加载一个符号的地址,这个符号在加载语句前面(下面)定义,forward嘛,这个符号为__lookup_processor_type_data,离这条语句最近的那个。在那个符号为__lookup_processor_type_data的位置我们看到这样的代码:
__lookup_processor_type_data:
.long .
.long __proc_info_begin
.long __proc_info_end
.size __lookup_processor_type_data, . -__lookup_processor_type_data
搜索这两个符号的值,在文件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文件,这些都是特定于处理器的汇编语言文件,
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
然后便是在那个段中逐个的检查struct proc_info_list结构体,以找到与我们的CPU相匹配的:
接下来我们继续看stext的代码:
movs r10, r5 @invalid processor (r5=0)?
THUMB( it eq ) @force fixup-able long branch encoding
beq __error_p @yes, error 'p'
bl __lookup_machine_type @r5=machinfo
在获得了处理器信息之后,则调用__lookup_machine_type来查找机器信息。这个例程同样也在arch/arm/kernel/head-common.S文件中定义。这个例程的定义如下:
__lookup_machine_type:
adr r3,__lookup_machine_type_data
ldmia r3, {r4, r5,r6}
sub r3, r3, r4 @ get offset betweenvirt&phys
add r5, r5, r3 @ convert virt addresses to
add r6, r6, r3 @ physical address space
1: ldr r3, [r5,#MACHINFO_TYPE] @ get machine type
teq r3, r1 @ matches loadernumber?
beq 2f @ found
add r5, r5,#SIZEOF_MACHINE_DESC @ next machine_desc
cmp r5, r6
blo 1b
mov r5, #0 @ unknown machine
2: mov pc, lr
ENDPROC(__lookup_machine_type)
处理的过程和上面的__lookup_processor_type还是挺相似的。这个例程接收r1中传进来的机器号作为参数,然后,在一个由structmachine_desc结构体组成的段中查找和我们的机器号匹配的structmachine_desc结构体,这个结构体在arch/arm/include/asm/mach/arch.h文件中定义,用于保存机器的信息:
struct machine_desc {
/*
* Note! The first twoelements are used
* by assembler code inhead.S, head-common.S
*/
unsigned int nr; /* architecture number */
const char *name; /* architecture name */
unsigned long boot_params; /* tagged list */
unsigned int nr_irqs; /* number of IRQs */
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)(structmachine_desc *,
struct tag *, char **,
struct meminfo *);
void (*reserve)(void);/*reserve mem blocks */
void (*map_io)(void);/*IO mapping function */
void (*init_irq)(void);
struct sys_timer *timer; /* system tick timer */
void (*init_machine)(void);
};
同样这个例程也用到了同上面很相似的方式来获得符号的地址:
adr r3, __lookup_machine_type_data
__lookup_machine_type_data:
.long .
.long __arch_info_begin
.long __arch_info_end
.size __lookup_machine_type_data, . -__lookup_machine_type_data
这两个符号也是分别表示某种初始化的段的开始地址和结束地址。为了找到段的填充内容,还是得要了解一下到底都有哪些structmachine_desc结构体类型变量声明了要被放到这个段的。用关键字.arch.info.init 来搜索所有的内核源文件。在arch/arm/include/asm/mach/arch.h文件中我们看到:
#defineMACHINE_START(_type,_name) \
static const structmachine_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这样的文件中出现的;
如: /home/yxz1295324/ti-ezsdk_dm816x-evm_5_05_02_00/board-support/linux-2.6.37-psp04.04.00.01/arch/arm/mach-omap2/board_ti8168evm.c中
MACHINE_START(TI8168EVM,"ti8168evm")
/* Maintainer: Texas Instruments */
.boot_params =0x80000100,
.map_io =ti8168_evm_map_io,
.reserve =ti81xx_reserve,
.init_irq =ti8168_evm_init_irq,
.init_machine =ti8168_evm_init,
.timer =&omap_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:
pgtbl r4, r8 @ 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-omap2/include/mach/memory.h定义,
// 为UL(0x80000000) #definePLAT_PHYS_OFFSET
// 而TEXT_OFFSET在arch/arm/Makefile中定义,为内核镜像在内存中到内存
// 开始位置的偏移(字节),为$(textofs-y)
// textofs-y也在文件arch/arm/Makefile中定义,
// 为textofs-y :=0x00008000
// r4 = 80008000为临时页表的起始地址
// 首先即是初始化16K的页表,高12位虚拟地址为页表索引,所以为
// 4K*4 = 16K,大页表,每一个页表项,映射1MB虚拟地址。
// 这个地方还来了个循环展开,以优化性能。
mov r0, r4
mov r3, #0
add r6, r0, #0x4000
1: 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(structproc_info_list, __cpu_mm_mmu_flags));
// R10寄存器保存的指针指向是我们前面找到的proc_info_list结构嘛。
// 为内核的第一个MB创建一致的映射,以为打开MMU做准备,这个映射 将会被
// paging_init()移除,这里使用程序计数器来获得相应的段的基地址。
// 这个地方是直接映射。
// 为内核的第一个MB创建一致的映射,以为打开MMU做准备,这个映射 将会被
// paging_init()移除,这里使用程序计数器来获得相应的段的基地址。
// 这个地方是直接映射。
// 为内核的第一个MB创建一致的映射,以为打开MMU做准备,这个映射 将会被
// paging_init()移除,这里使用程序计数器来获得相应的段的基地址。
// 这个地方是直接映射。
adr r0, __enable_mmu_loc
ldmia r0, {r3, r5, r6}
sub r0, r0, r3 @virt->phys offset
add r5, r5, r0 @phys __enable_mmu
add r6, r6, r0 @phys __enable_mmu_end
mov r5, r5, lsr #20
mov r6, r6, lsr #20
1: orr r3, r7, r5, lsl #20 @ flags + kernel base
str r3, [r4, r5, lsl #2] @ identity mapping
teq r5, r6
addne r5, r5, #1 @next section
bne 1b
/*
* Now setup the pagetables for our kerneldirect
* mapped region.
*/
mov r3, pc
mov r3, r3, lsr #20
orr r3, r7, r3, lsl #20
// 接下来为内核的直接映射区设置页表。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为单位来映射内核镜像。
mov r3, pc
mov r3, r3, lsr #20
orr r3, r7, r3, lsl #20
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
mov r0, r2, lsr #20
movs r0, r0, lsl #20
moveq r0, r8
sub r3, r0, r8
add r3, r3, #PAGE_OFFSET
add r3, r4, r3, lsr #18
orr r6, r7, r0
str r6, [r3]
// 上面的这个步骤显得似乎有些多余。
// 总结一下,这个建立临时页表的过程:
// 1、为内核镜像的第一个MB建立直接映射
// 2、为内核镜像完整的建立从虚拟地址到物理地址的映射
// 3、为物理内存的第一个MB建立到内核的虚拟地址空间的第一个MB的映射。
// OK,内核的临时页表建立完毕。整个初始化临时页表的过程都没有修改R8,
// R9和R10。
mov pc, lr
ENDPROC(__create_page_tables)
回到stext:
ldr r13, __mmap_switched @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-v7.S(ti8168为v7,通过.config可知)文件中struct proc_info_list结构体的变量的定义,可以看到这个成员为:
__v7_proc_info:
.long 0x000f0000 @Required ID value
.long 0x000f0000 @Mask for ID
ALT_SMP(.long\
PMD_TYPE_SECT| \
PMD_SECT_AP_WRITE| \
PMD_SECT_AP_READ| \
PMD_FLAGS_SMP)
ALT_UP(.long\
PMD_TYPE_SECT| \
PMD_SECT_AP_WRITE| \
PMD_SECT_AP_READ| \
PMD_FLAGS_UP)
.long PMD_TYPE_SECT | \
PMD_SECT_XN| \
PMD_SECT_AP_WRITE| \
PMD_SECT_AP_READ
W(b) __v7_setup
.long cpu_arch_name
.long cpu_elf_name
.long HWCAP_SWP|HWCAP_HALF|HWCAP_THUMB|HWCAP_FAST_MULT|HWCAP_EDSP|HWCAP_TLS
.long cpu_v7_name
.long v7_processor_functions
.long v7wbi_tlb_fns
.long v6_user_fns
.long v7_cache_fns
.size __v7_proc_info, . - __v7_proc_info
__cpu_flush成员函数即为
W(b) __v7_setup
也就是说,在设置好内核临时页表之后调用了例程__v7_setup,这个例程同样在arch/arm/mm/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 @ onlypresent 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 @ onlypresent 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 @ onlypresent 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
……
……
3: mov r10, #0
#ifdef HARVARD_CACHE
mcr p15, 0, r10, c7, c5, 0 @I+BTB cache invalidate
#endif
dsb
#ifdef CONFIG_MMU
mcr p15, 0, r10, c8, c7, 0 @invalidate I + D TLBs
mcr p15, 0, r10, c2, c0, 2 @TTB control register
ALT_SMP(orr r4, r4, #TTB_FLAGS_SMP)
ALT_UP(orr r4, r4, #TTB_FLAGS_UP)
mcr p15, 0, r4, c2, c0, 1 @load TTB1
mov r10, #0x1f @ domains 0,1 = manager
mcr p15, 0, r10, c3, c0, 0 @load domain access register
/* Cortex-A9 Errata */
这一段首先使i,d caches内容无效,然后清除write buffer,接着使TLB内容无效。
__v7_setup返回后,
b __enable_mmu
__enable_mmu:
#ifdefCONFIG_ALIGNMENT_TRAP
orr r0, r0, #CR_A
#else
bic r0, r0, #CR_A
#endif
#ifdefCONFIG_CPU_DCACHE_DISABLE
bic r0, r0, #CR_C
#endif
#ifdefCONFIG_CPU_BPREDICT_DISABLE
bic r0, r0, #CR_Z
#endif
#ifdefCONFIG_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
ENDPROC(__enable_mmu)
在这儿设置了页目录地址(r4寄存器中保存),然后设置domain的保护,在前面建立页表的例程中,注意到,页表项的控制信息,是从structproc_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
__enable_mmu_end:
接下来写控制寄存器:
mcr p15, 0, r0, c1, c0, 0 @write control reg
一切设置就此生效,到此算是完成了打开d,icache和mmu的工作。
注意:arm的d cache必须和mmu一起打开,而i cache可以单独打开。其实,cache和mmu的关系实在是紧密,每一个页表项都有标志标示是否是cacheable的,可以说本来就是设计一起使用的
mov r3, r13
mov pc, r3
前面有提到过,r13中存放的其实是另外一个例程的地址,其值是变量__mmap_switched的第一个字段,即一个函数指针的值,__mmap_switched变量是在arch/arm/kernel/head-common.S中定义的:
__mmap_switched:
adr r3,__mmap_switched_data
ldmia r3!, {r4, r5,r6, r7}
cmp r4, r5 @ Copy data segment ifneeded
1: cmpne r5, r6
ldrne fp, [r4], #4
strne fp, [r5], #4
bne 1b
mov fp, #0 @ Clear BSS (and zerofp)
1: cmp r6, r7
strcc fp, [r6],#4
bcc 1b
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)
__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、使r3指向__mmap_switched_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
到此为止,汇编部分的初始化代码就结束了
总结上述汇编代码:总共分为两个部分:
1. zImage解压和重定位(此汇编代码没有分析)
2. Image的操作:比较处理器ID,机器号,建立页表,开启MMU,保存机器号等参数;(本部分文档主要就是分析这部分)
如果以为到了c代码可以松一口气的话,就大错特措了,linux的c也不比汇编好懂多少,相反倒掩盖了汇编的一些和机器相关的部分,有时候更难懂。其实作为编写操作系统的c代码,只不过是汇编的另一种写法,和机器代码的联系是很紧密的。另外,这些start_kernel()中调用的C函数,每一个都具有举足轻重的地位,它们中的许多都肩负着初始化内核中的某个子系统的重要使命,而Linux内核中每一个子系统都错综复杂,牵涉到各种软件、硬件的复杂算法,所以理解起来倒真的是挺困难的。
start_kernel函数在 init/main.c中定义:
asmlinkage void __init start_kernel(void)
{
char * command_line;
extern const struct kernel_param__start___param[], __stop___param[];
smp_setup_processor_id();//只有一个cpu则什么都不用做,若有多个cpu则是返回当前的号码;
/*
* Need to run as early as possible, toinitialize the
* lockdep hash:
*/
lockdep_init();
debug_objects_early_init();
/*
* Set up the the initial canary ASAP:
*/
boot_init_stack_canary();
cgroup_init_early();
local_irq_disable();//关闭cpu中断
early_boot_irqs_off();
/*
*Interrupts are still disabled. Do necessary setups, then
* enablethem
*/
tick_init();//时钟事件初始化
boot_cpu_init();//激活第一个处理器
page_address_init();//页表地址初始化
printk(KERN_NOTICE"%s", linux_banner);//打印linux版本
setup_arch(&command_line);
/*********************************************************/
重点关注一下setup_arch函数
{
structtag *tags = (struct tag *)&init_tags;
structmachine_desc *mdesc;
char*from = default_command_line;
init_tags.mem.start= PHYS_OFFSET;
unwind_init();
setup_processor();
mdesc= setup_machine(machine_arch_type);
machine_name= mdesc->name;
if(mdesc->soft_reboot)
reboot_setup("s");
if(__atags_pointer)
tags= phys_to_virt(__atags_pointer);
elseif (mdesc->boot_params) {
#ifdef CONFIG_MMU
/*
* We still are executing with a minimal MMUmapping created
* with the presumption that the machinedefault for this
* is located in the first MB of RAM. Anything else will
* fault and silently hang the kernel at thispoint.
*/
if(mdesc->boot_params < PHYS_OFFSET ||
mdesc->boot_params >= PHYS_OFFSET +SZ_1M) {
printk(KERN_WARNING
"Default boot params at physical0x%08lx out of reach\n",
mdesc->boot_params);
}else
#endif
{
tags= phys_to_virt(mdesc->boot_params);
}
}
#if defined(CONFIG_DEPRECATED_PARAM_STRUCT)
/*
* If we have the old style parameters, convertthem to
* a tag list.
*/
if(tags->hdr.tag != ATAG_CORE)
convert_to_tag_list(tags);
#endif
if(tags->hdr.tag != ATAG_CORE)
tags= (struct tag *)&init_tags;
if(mdesc->fixup)
mdesc->fixup(mdesc,tags, &from, &meminfo);
if(tags->hdr.tag == ATAG_CORE) {
if(meminfo.nr_banks != 0)
squash_mem_tags(tags);
save_atags(tags);
parse_tags(tags);
}
init_mm.start_code= (unsigned long) _text;
init_mm.end_code = (unsigned long) _etext;
init_mm.end_data = (unsigned long) _edata;
init_mm.brk =(unsigned long) _end;
/*parse_early_param needs a boot_command_line */
strlcpy(boot_command_line,from, COMMAND_LINE_SIZE);
/*populate cmd_line too for later use, preserving boot_command_line */
strlcpy(cmd_line,boot_command_line, COMMAND_LINE_SIZE);
*cmdline_p= cmd_line;
parse_early_param();
arm_memblock_init(&meminfo,mdesc);
paging_init(mdesc);
request_standard_resources(&meminfo,mdesc);
#ifdef CONFIG_SMP
if(is_smp())
smp_init_cpus();
#endif
reserve_crashkernel();
cpu_init();
tcm_init();
/*
* Set up various architecture-specificpointers
*/
arch_nr_irqs= mdesc->nr_irqs;
init_arch_irq= mdesc->init_irq;
system_timer= mdesc->timer;
init_machine= mdesc->init_machine;
#ifdef CONFIG_VT
#if defined(CONFIG_VGA_CONSOLE)
conswitchp= &vga_con;
#elif defined(CONFIG_DUMMY_CONSOLE)
conswitchp= &dummy_con;
#endif
#endif
early_trap_init();
}
664 行struct tag指针类型的局部变量指向了默认的tag列表init_tags,该静态变量在setup_arch()定义同文件的前面有如下定义:
staticstruct init_tags {
structtag_header hdr1;
structtag_core core;
structtag_header hdr2;
structtag_mem32 mem;
structtag_header hdr3;
}init_tags __initdata = {
{tag_size(tag_core), ATAG_CORE },
{1, PAGE_SIZE, 0xff },
{tag_size(tag_mem32), ATAG_MEM },
{MEM_SIZE },
{0, ATAG_NONE }
};
第679行检察__atags_pointer指针的有效性,这个指针是在前面,跳转到start_kernel函数的汇编例程最后设置的几个变量之一,用的是R2寄存器的值。如果bootloader通过R2传递了tag列表的话,自然是要使用bootloader穿的进来的tag列表的。
第673行以machine_arch_type为参数调用了setup_machine()函数,而这个函数的定义为:
mdesc= setup_machine(machine_arch_type);
返回mdesc结构体,也就是具体处理器的体系相关的数据结构如:(定义在./arch/arm/mach-omap2/ti8168_evm.h/board-ti8168evm.c)
MACHINE_START(TI8168EVM,"ti8168evm")
/*Maintainer: Texas Instruments */
.boot_params = 0x80000100,
.map_io = ti8168_evm_map_io,
.reserve = ti81xx_reserve,
.init_irq = ti8168_evm_init_irq,
.init_machine = ti8168_evm_init,
.timer = &omap_timer,
MACHINE_END
然后,machine_desc结构体的name成员的值被赋给全局变量machine_name。
第681行,若bootloader没有传递tag列表给内核,则检测machine_desc结构体的boot_params字段,看看特定的平台是否传递了标记列表。
第753、754、755、756行分别将machine_desc结构体的init_irq、timer和init_machine成员值赋给了三个全局变量init_arch_irq、system_timer和init_machine,即是设置特定体系结构的指针。初始化的后面阶段自然会用到。
结束start_arch()函数分析
回到start_kernel分析
init_irq会在init_IRQ();中调用
timer会在init_timer();中调用
往下就是console_init();
主要是添加串口驱动
void__init console_init(void)
{
initcall_t *call;
/* Setup the default TTY line discipline.*/
tty_ldisc_begin();
/*
* set up the console device so that laterboot sequences can
*inform about problems etc..
*/
call = __con_initcall_start;
while (call < __con_initcall_end) {
(*call)();
call++;
}
}
__con_initcall_start= .;
*(.con_initcall.init)
__con_initcall_end = .;
因此我们调用的就是con_initcall.init这段代码
在我们串口驱动里面有这么一个注册语句:console_initcall(serial8250_console_init);
//
因此们的控制台初始化流程就是:start_kernel->console_init->serial8250_console_init
往下分析
proc_root_init();//初始化proc文件系统
{ //
为proc文件系统索引节点创建高速缓存内存描述结构
int err = proc_init_inodecache();
if (err)
return;
//
注册proc文件系统
err = register_filesystem(&proc_fs_type);
if (err)
return;
/*
该函数基本完成三个步骤,首先调用
read_super()
函数,在这个函数里, VFS将为proc文件系统分配一个超级块结构,并设置s_dev,s_flags等域,然后, 将调用proc文件系统的自己的read_super例程,对应proc文件系统,该例程是proc_read_super()
该例程将设置超级块结构的其他值。我们将在下一节进行分析。
其次,使用add_vfsmnt()函数建立proc文件系统的vfsmount结构,并将其加入到已装载文件系统的链表中(可参考图-xx)。 最后,返回该vfsmount
结构,并利用返回值,使用指针proc_mnt指向该vfsmount结构。*/ proc_mnt = kern_mount(&proc_fs_type);
err = PTR_ERR(proc_mnt);
if (IS_ERR(proc_mnt)) {
unregister_filesystem(&proc_fs_type);
return;
最后到了reset_init();
{
//新建进程1号进程,原来进程为进程0,执行cpu空闲操作,为所有进程的祖先;
因此在linux中进程流程:0号内核进程->1号内核进程->1号用户进程
kernel_thread(kernel_init,NULL, CLONE_FS | CLONE_SIGHAND);
static noinline void__init_refok rest_init(void)
{
int pid;
rcu_scheduler_starting();
/*
* Weneed to spawn init first so that it obtains pid 1, however
* theinit task will end up wanting to create kthreads, which, if
* weschedule it before we create kthreadd, will OOPS.
*/
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
//建立1号进程,进程名称为kernel_init;
numa_default_policy();
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
rcu_read_lock();
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
rcu_read_unlock();
complete(&kthreadd_done);
/*
* The boot idle threadmust execute schedule()
* atleast once to get things moving:
*/
init_idle_bootup_task(current);
preempt_enable_no_resched();
schedule();//进程调度
preempt_disable();
/* Call into cpu_idle with preempt disabled */
cpu_idle();//cpu空闲操作;
}
static int __initkernel_init(void * unused)
{
/*
* Wait until kthreadd isall set-up.
*/
wait_for_completion(&kthreadd_done);
/*
* init can allocatepages on any node
*/
set_mems_allowed(node_states[N_HIGH_MEMORY]);
/*
* init can run on anycpu.
*/
set_cpus_allowed_ptr(current, cpu_all_mask);
/*
* Tell the world thatwe're going to be the grim
*reaper of innocent orphaned children.
*
* We don't want peopleto have to make incorrect
*assumptions about where in the task array this
* canbe found.
*/
init_pid_ns.child_reaper =current;//child_reaper为1号进程;
cad_pid = task_pid(current);
smp_prepare_cpus(setup_max_cpus);
do_pre_smp_initcalls();
smp_init();
sched_init_smp();
do_basic_setup();//非常重要
{
/*
*Ok, the machine is now initialized. None of the devices
*have been touched yet, but the CPU subsystem is up and
*running, and memory and process management works.
*
*Now we can finally start doing some real work..
*/
static void __initdo_basic_setup(void)
{
cpuset_init_smp();
//启动用户态的khelper进程;
usermodehelper_init();
//安装交换分区的文件系统
init_tmpfs();
//驱动模型初始化;
driver_init();
init_irq_proc();
do_ctors();
//do_initcalls用来启动所有在__initcall_start和__initcall_end段的函
数,而静态编译的module也会放在将入口放在这段代码之间;
do_initcalls();
}
}
/* Open the /dev/console onthe rootfs, this should never fail */
if (sys_open((const char__user *) "/dev/console", O_RDWR, 0) < 0)
printk(KERN_WARNING"Warning: unable to open an initial console.\n");
(void) sys_dup(0);
(void) sys_dup(0);
/*
* check if there is an early userspaceinit. If yes, let it do all
* the work
*/
if (!ramdisk_execute_command)
ramdisk_execute_command= "/init";
if (sys_access((const char__user *) ramdisk_execute_command, 0) != 0) {
ramdisk_execute_command= NULL;
//这里是文件系统挂载的地方,很重要;
//所有的工作都是在这里完成;
prepare_namespace();
{
void __initprepare_namespace(void)
{
int is_floppy;
if (root_delay) {
printk(KERN_INFO "Waiting %dsec before mounting root device...\n",root_delay);
ssleep(root_delay);
}
/*
*wait for the known devices to complete their probing
*
*Note: this is a potential source of long boot delays.
*For example, it is not atypical to wait 5 seconds here
*for the touchpad of a laptop to initialize.
*/
wait_for_device_probe();
//只有定义了CONFIG_BLK_DEV_MD才会用到
md_run_setup();
if (saved_root_name[0]) {
root_device_name = saved_root_name;
if (!strncmp(root_device_name, "mtd", 3) ||
!strncmp(root_device_name, "ubi",3)) {
mount_block_root(root_device_name,root_mountflags);
goto out;
}
ROOT_DEV =name_to_dev_t(root_device_name);
{//这一步很重要,解析参数选项中的root参数,用来获取要挂载的 设备号
dev_tname_to_dev_t(char *name)
{
char s[32];
char *p;
dev_t res = 0;
int part;
if (strncmp(name,"/dev/", 5) != 0) {
//如果bootargs不是以root=/dev/**,则进入循环,所以也可 以通过设备号来处理要挂载的设备,如root=31:0,等价为 root=/dev/mtdblock0
unsigned maj, min;
if (sscanf(name, "%u:%u", &maj, &min) ==2) {
res = MKDEV(maj, min);
if (maj != MAJOR(res) || min !=MINOR(res))
goto fail;
} else {
res = new_decode_dev(simple_strtoul(name,&p, 16));
if (*p)
goto fail;
}
goto done;
}
name += 5;
res = Root_NFS;
if (strcmp(name, "nfs") == 0)
goto done;
res = Root_RAM0;
if (strcmp(name, "ram") == 0)
goto done;
//root参数名称不能过长,不能大于31个字节
if (strlen(name) > 31)
goto fail;
strcpy(s, name);
for (p = s; *p; p++)
if (*p == '/')
*p = '!';
//除了nfs和ram,其他的设备通过/sys/block/%s/dev/找到设备号
res = blk_lookup_devt(s, 0);
if (res)
goto done;
…………
…………
}
}
if (strncmp(root_device_name,"/dev/", 5) == 0)
root_device_name+= 5;
}
if (initrd_load())
goto out;
/* wait for any asynchronousscanning to complete */
if ((ROOT_DEV == 0)&& root_wait) {
printk(KERN_INFO"Waiting for root device %s...\n",
saved_root_name);
while(driver_probe_done() != 0 ||
(ROOT_DEV =name_to_dev_t(saved_root_name)) == 0)
msleep(100);
async_synchronize_full();
}
is_floppy = MAJOR(ROOT_DEV)== FLOPPY_MAJOR;
if (is_floppy &&rd_doload && rd_load_disk(0))
ROOT_DEV = Root_RAM0;
//挂载文件系统
mount_root();
out:
devtmpfs_mount("dev");
sys_mount(".","/", NULL, MS_MOVE, NULL);
sys_chroot((const char __user__force *)".");
}
}
}
/*
* Ok, we have completed the initial bootup,and
* we're essentially up and running. Get rid ofthe
* initmem segments and start the user-modestuff..
*/
//执行用户进程init,内核启动的第一个也是唯一的一个用户进程;
init_post();
{
/* This is a non __initfunction. Force it to be noinline otherwise gcc
*makes it inline to init() and it becomes part of init.text section
*/
static noinline intinit_post(void)
{
/* need to finish all async __init code before freeing thememory */
async_synchronize_full();
//以init表示的函数空间释放,也就是在启动的过程中,经常会 出现的Freeing init memory:116K
free_initmem();
mark_rodata_ro();
system_state = SYSTEM_RUNNING;
numa_default_policy();
current->signal->flags |= SIGNAL_UNKILLABLE;
if (ramdisk_execute_command) {
run_init_process(ramdisk_execute_command);
printk(KERN_WARNING "Failed to execute %s\n",
ramdisk_execute_command);
}
/*
* We try each of these until one succeeds.
*
* The Bourne shell can be used instead of initif we are
* trying to recover a really broken machine.
*/
//执行用户进程,直到有一个执行成功;
if (execute_command) {
//用户自定义的启动init进程;如root=/linuxrc
run_init_process(execute_command);
printk(KERN_WARNING"Failed to execute %s. Attempting"
"defaults...\n",execute_command);
}
//若没有则一个个执行默认用户进程,直到有一个成功为止;只要成功就不会返回
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
//上述脚本都会执行/etc/inittab脚本,inittab脚本函数都会执行/etc/init.d 中所有脚本;
panic("No init found. Try passing init= option to kernel. "
"See Linux Documentation/init.txtfor guidance.");
}
}
return 0;
}
所有的启动过程分析完毕;
Start_kernel()中调用了一系列初始化函数,以完成kernel本身的设置。这些动作有的是公共的,有的则是需要配置的才会执行的。
在start_kernel()函数中,
输出Linux版本信息(printk(_banner))
设置与体系结构相关的环境(setup_arch())
页表结构初始化(paging_init())
使用"arch/alpha/kernel/entry.S"中的入口点设置系统自陷入口(trap_init())
使用alpha_mv结构和entry.S入口初始化系统IRQ(init_IRQ())
核心进程调度器初始化(包括初始化几个缺省的Bottom-half,sched_init())
时间、定时器初始化(包括读取CMOS时钟、估测主频、初始化定时器中断等,time_init())
提取并分析核心启动参数(从环境变量中读取参数,设置相应标志位等待处理,(parse_options())
控制台初始化(为输出信息而先于PCI初始化,console_init())
剖析器数据结构初始化(prof_buffer和prof_len变量)
核心Cache初始化(描述Cache信息的Cache,kmem_cache_init())
延迟校准(获得时钟jiffies与CPU主频ticks的延迟,calibrate_delay())
内存初始化(设置内存上下界和页表项初始值,mem_init())
创建和设置内部及通用cache("slab_cache",kmem_cache_sizes_init())
创建uid taskcount SLAB cache("uid_cache",uidcache_init())
创建文件cache("files_cache",filescache_init())
创建目录cache("dentry_cache",dcache_init())
创建与虚存相关的cache("vm_area_struct","mm_struct",vma_init())
块设备读写缓冲区初始化(同时创建"buffer_head"cache用户加速访问,buffer_init())
创建页cache(内存页hash表初始化,page_cache_init())
创建信号队列cache("signal_queue",signals_init())
初始化内存inode表(inode_init())
创建内存文件描述符表("filp_cache",file_table_init())
检查体系结构漏洞(对于alpha,此函数为空,check_bugs())
SMP机器其余CPU(除当前引导CPU)初始化(对于没有配置SMP的内核,此函数为空,smp_init())
启动init过程(创建第一个核心线程,调用init()函数,原执行序列调用cpu_idle() 等待调度,init())
至此start_kernel()结束,基本的核心环境已经建立起来了。