分类: LINUX
2015-03-19 14:02:26
linux内核启动过程可以分为两个部分:架构/开发板相关代码的引导过程,后继的通用启动过程。本文将分析的是ARM架构处理器的linux内核vmlinux的启动过程,之所以强调vmlinux,是因为其它格式的内核在进行与vmlinux相同的流程之前会有一些独特的操作,比如对于压缩格式的zImage,它首先进行自解压得到vmlinux (它将调用函数decompress_kernel()解压,打印“Uncompressing Linux...”,调用gunzip(),打印"done, booting the kernel"。),然后再执行vmlinux的启动过程。
1 内核的引导阶段代码的分析
与U-Boot的类似,内核的引导阶段代码通常是使用汇编语言编写的,它首先检查内核是否支持当前的架构处理器,然后检查是否支持当前开发板。通过检查后,就为调用下一阶段的start_kernel函数作准备。主要分为两个步骤:由于连接内核时使用的是虚拟地址,故首先要设置页表,使能MMU;接着调用C函数start_kernel之前的常规工作,包括复制数据段、清除BSS段、调用start_kernel函数。
对于前面的Makfile的分析,知道内核代码入口在arch/arm/kernel/head.S是内核执行的第一个文件。U-Boot调用内核时,此时的状态MMU为off,D-cache为off,I-cache为dont care,on或off没有关系,r0为0 ,r1为“机器类型ID”,r2为atags指针。另外,协处理器CP15的寄存器C0中存放的是CPU ID,CPU ID中包含了处理器厂商的编号、产品子编号、ARM体系版本号、产品主编号和处理器版本号。下面分析其流程:
(1)设置CPU为系统管理(SVC)模式并且禁止中断
ENTRY(stext)
msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE
(2)查询CPU ID并检查合法性
mrc p15, 0, r9, c0, c0 /*读取CPU ID,存入r9寄存器*/
bl __lookup_processor_type /*调用函数__lookup_processor_type,输入参数r9=cpuid ,返回值r5=procinfo*/
movs r10, r5 /*如果不支持当前CPU,则返回值r5=0*/
beq __error_p /*如果r5=0,则打印错误*/
在内核映像中,定义了若干proc_info_list表示它支持的CPU,对于ARM架构的CPU,这些结构的源码在arch/arm/mm/目录下。比如proc-v6.S中有如下代码,它表示的所有ARMv6架构的CPU(s3c6410是ARMv6架构的)的proc_info_list结构。
.section ".proc.info.init", #alloc, #execinstr
/*Match any ARMv6 processor core.*/
.type __v6_proc_info, #object
__v6_proc_info:
.long 0x0007b000 /*cpu_val*/
.long 0x0007f000 /*cpu_mask*/
……
不同的proc_info_list结构被用来支持不同的CPU,它们都是定义在“.proc.info.init”段中,在连接内核时,这些结构被组织在一起。开始地址为__proc_info_begin,结束地址为__proc_info_end。在arm/kernel/vmlinux.lds.S中可以看到这样的代码:
__proc_info_begin = .; /* proc_info_list结构的开始地址*/
*(.proc.info.init)
__proc_info_end = .; /*proc_info_list结构的结束地址*/
__lookup_processor_type函数就是根据前面从协处理器CP15的寄存器C0中读取到CPU ID(存入r9寄存器),从这些proc_info_list结构中找出匹配。它是在arch/arm /kernel/head-common.S中定义的,代码如下:
.type __lookup_processor_type, %function
__lookup_processor_type:
adr r3, 3f /*将标号3的实际地址加载到r3*/
ldmda r3, {r5 - r7} /*然后将编译时生成的 __proc_info_begin虚拟地址载入到r5,__proc_info_end虚拟地址载入到r6,标号3的虚拟地址载入到r7*/
sub r3, r3, r7 /*得到物理地址和虚拟地址的差值*/
add r5, r5, r3 /* __proc_info_begin对应的物理地址*/
add r6, r6, r3 /*__proc_info_end对应的物理地址*/
1: ldmia r5, {r3, r4} /*将proc_info_list结构中cpu_val、cpu_mask分别存放在r3, r4中*/
and r4, r4, r9 /* r4 = cpu_mask&CPU_ID*/
teq r3, r4 /*比较*/
beq 2f /*如果相等,找到匹配的proc_info_list结构,跳转到标
号2处*/
add r5, r5, #PROC_INFO_SZ /*否则, r5指向下一个proc_info_list结构*/
cmp r5, r6 /*是否比较完所有的proc_info_list结构*/
blo 1b /*没有则跳转到标号1继续比较*/
mov r5, #0 /*比较完,但是没有匹配的proc_info_list结构,r5 =0*/
2: mov pc, lr /*函数调用完毕,返回*/
……
/*Look in include/asm-arm/procinfo.h and arch/arm/kernel/arch.[ch] for
* more information about the __proc_info and __arch_info structures.*/
.long __proc_info_begin
.long __proc_info_end
3: .long .
.long __arch_info_begin
.long __arch_info_end
__proc_info_begin、 __proc_info_end和“.”这三个数据都是在连接内核时确定的,它们都是虚拟地址,前两个表示proc_info_list结构的开始地址和结束地址,“.”表示当前行代码在编译连接后的虚拟地址。因为MMU没有开启,所以我们此时还不能直接使用这些地址。所以在访问proc_info_list结构前,需要先将它的虚拟地址转化为物理地址。
__lookup_processor_type函数首先将标号3的实际地址加载到r3,然后将编译时生成的 __proc_info_begin虚拟地址载入到r5,__proc_info_end虚拟地址载入到r6,标号3的虚拟地址载入到r7。由于r3和r7分别存储的是同一位置标号3的物理地址和虚拟地址,所以儿者相减即得到虚拟地址和物理地址之间的offset。利用此offset,将r5和r6中保存的虚拟地址转变为物理地址然后从proc_info中读出内核编译时写入的processor ID和之前从cpsr中读到的processor ID对比,查看代码和CPU硬件是否匹配。如果编译了多种处理器支持则会循环每种type依次检验,如果硬件读出的ID在内核中找不到匹配,则r5置0返回。
(3)查询machine ID并检查合法性
bl __lookup_machine_type /*调用函数__lookup_machine_type ,返回值r5=machinfo*/
movs r8, r5 /*如果不支持当前机器(即开发板),则返回值r5=0*/
beq __error_a /*如果r5=0,则打印错误*/
内核中对每种支持的开发板都会使用宏MACHINE_START、MACHINE_END来定义一个machine_desc结构,它定义了开发板相关的一些属性和函数,如机器的类型ID、起始I/O物理地址、Bootloader传入的参数地址、中断初始化函数、I/O映射函数等。对于UT-S3C6410开发板在arch/arm/mach-s3c6410/ mach-smdk6410.c有如下代码:
MACHINE_START(SMDK6410, "SMDK6410")
/* Maintainer: Samsung Electronics */
.phys_io = S3C24XX_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C_SDRAM_PA + 0x100,
.init_irq = s3c_init_irq,
.map_io = smdk6410_map_io,
.fixup = smdk6410_fixup,
.timer = &s3c_timer,
.init_machine = smdk6410_machine_init,
MACHINE_END
宏MACHINE_START、MACHINE_END在include/asm-arm/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 \
};
所以上面代码扩展开来就是
static const struct machine_desc __mach_desc_ SMDK6410
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_ SMDK6410, \
.name = “SMDK6410”,
……
};
不同的machine_desc结构被用来支持不同的开发板,它们都是定义在“.arch.info.init”段中,在连接内核时,这些结构被组织在一起。开始地址为__arch_info_begin,结束地址为__arch_info_end =。在arm/kernel/vmlinux.lds.S中可以看到这样的代码:
__arch_info_begin = .; /* machine_desc结构的开始地址*/
*(.arch.info.init)
__arch_info_end = .; /*machine_desc结构的结束地址*/
U-Boot调用内核时,会在r1寄存器中给出开发板的标记即机器类型ID。__lookup_machine_type函数将这个值与machine_desc结构的nr成员比较,如果两者相等则表示找到匹配的machine_desc结构,于是返回它的地址(存到r5中)。如果__arch_info_begin、__arch_info_end之间所有的machine_desc结构的nr成员都不等于r1寄存器中的值,则返回0,即r5中值为0 。编码方法与__lookup_processor_type函数完全一样,在此不再详述。
(4)检查atags合法性
bl __vet_atags
r2为中存放的是标记列表atags指针。__vet_atags用来检查atags合法性,
.type __vet_atags, %function
__vet_atags:
tst r2, #0x3 /*检测是否地址对齐*/
bne 1f /*没对齐则跳转到标号1处*/
ldr r5, [r2, #0] /*对齐,则将标记列表第一个标记ATAG_CORE存入r5中*/
subs r5, r5, #ATAG_CORE_SIZE /* r5指向下一个标记结构*/
bne 1f /* r5为0则跳转到标号1处*/
ldr r5, [r2, #4] /*否则将标记的类型放入r5中*/
ldr r6, =ATAG_CORE /*将代表ATAG_CORE值放入r6中*/
cmp r5, r6 /*比较 r5, r6*/
bne 1f /*不相等跳转到标号1处*/
mov pc, lr /* 相等则说明atag是合法性,函数调用完毕,返回*/
1: mov r2, #0 /* atag非法,将r2置为0*/
mov pc, lr/*函数调用完毕,返回*/
(5)创建一级页表
bl __create_page_tables
__create_page_tables:函数在arch/arm/kernel/head.S中实现,它完成一级页表的创建。代码如下:
/*首先将内核起始地址-0x4000到内核起始地址之间的16K存储器清0*/
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
/*将proc_info中的mmu_flags加载到r7*/
ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
/*将PC指针右移20位,得到内核第一个1MB空间的段地址存入r6,接着根据此值存入映射标识*/
mov r6, pc, lsr #20 @ start of kernel section
orr r3, r7, r6, lsl #20 @ flags + kernel base
str r3, [r4, r6, lsl #2] @ identity mapping
(6)设置跳转地址
ldr r13, __switch_data
adr lr, __enable_mmu
设置r13寄存器值为__switch_data,在mmu被使能即__turn_mmu_on函数执行完之后会跳转到该地址处。__switch_data段在arch/arm/kerne/ head-common.S有定义。代码如下:
__switch_data:
.long __mmap_switched
.long __data_loc @ r4
.long __data_start @ 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
设置lr寄存器值为__enable_mmu,执行完函数__v6_setup,函数会跳转到__enable_mmu地址处。
(7) 初始化 TLB, Caches, 和 MMU状态
add pc, r10, #PROCINFO_INITFUNC
执行这条指令后,程序将会跳转到函数__v6_setup处执行。__v6_setup是在文件arch/arm/mm/proc-v6.S中实现的,它的初始化 TLB, Caches, 和 MMU状态,为下一步开启MMU作准备。然后将原来保存在lr中的地址载入pc,跳转到arch/arm/kerne/ head.S的__enable_mmu处执行,代码不再详述。
(8)为开启MMU前做好准备工作
在开启MMU之前还要做好最后的准备工作,比如无效Icaches、Dcaches,设置域访问控制器和页表指针等。这些设置是在arch/arm/kerne/ head.S文件中__enable_mmu函数中实现的.
__enable_mmu:
#ifdef CONFIG_ALIGNMENT_TRAP /*开启地址对齐检查*/
orr r0, r0, #CR_A
#else
bic r0, r0, #CR_A
#endif
#ifdef CONFIG_CPU_DCACHE_DISABLE /*无效Dcaches*/
bic r0, r0, #CR_C
#endif
#ifdef CONFIG_CPU_BPREDICT_DISABLE
bic r0, r0, #CR_Z
#endif
#ifdef CONFIG_CPU_ICACHE_DISABLE/*无效Icaches*/
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 /*设置域访问控制器*/
mcr p15, 0, r4, c2, c0, 0 /*设置页表基址寄存器*/
(9)使能MMU
b __turn_mmu_on
当__enable_mmu执行完后,跳转到__turn_mmu_on,该函数会把MMU使能位写入MMU控制寄存器,使能MMU激活虚拟地址。然后将原来保存在sp中的地址载入pc,跳转到head-common.S的__mmap_switched,至此代码进入虚拟地址的世界。
.align 5
.type __turn_mmu_on, %function
__turn_mmu_on:
mov r0, r0
mcr p15, 0, r0, c1, c0, 0 /*使能MMU*/
mrc p15, 0, r3, c0, c0, 0
mov r3, r3
mov r3, r3
mov pc, r13 /*跳转到__mmap_switched*/
(10) 跳转到第二阶段代码的C入口点。
在跳转到第二阶段代码的C入口点之前,需要做一些准备工作。包括复制数据段、清除BSS段、保存一些重要的信息到寄存器和内存中。这些都在函数__mmap_switched中实现。这里只粘贴部分代码:
__mmap_switched:
.type __mmap_switched, %function
__mmap_switched:
adr r3, __switch_data + 4
ldmia r3!, {r4, r5, r6, r7}
cmp r4, r5 /*复制数据段*/
1: cmpne r5, r6
ldrne fp, [r4], #4
strne fp, [r5], #4
bne 1b
mov fp, #0 /*清除BSS段*/
1: cmp r6, r7
strcc fp, [r6],#4
bcc 1b
ldmia r3, {r4, r5, r6, r7, sp}
str r9, [r4] /* processor ID保存在r9*/
str r1, [r5] /* machine ID报存在r1*/
str r2, [r6] /* atags地址保存在r2*/
bic r4, r0, #CR_A /*Clear 'A' bit*/
stmia r7, {r0, r4} /*将控制寄存器保存到r7定义的内存地址*/
然后再接下来跳入/init/main.c的start_kernel函数,它是内核第二阶段代码的入口点。
b start_kernel /*调用start_kernel函数*/
2. start_kernel函数部分代码的分析
start_kernel函数代码如下:
asmlinkage void __init start_kernel(void)
{
char * command_line;
extern struct kernel_param __start___param[], __stop___param[];
smp_setup_processor_id();
/*Need to run as early as possible, to initialize thelockdep hash:*/
lockdep_init();
debug_objects_early_init();
cgroup_init_early();
local_irq_disable();
early_boot_irqs_off();
early_init_irq_lock_class();
/*Interrupts are still disabled. Do necessary setups, then enable them*/
lock_kernel();
tick_init();
boot_cpu_init();
page_address_init();
printk(KERN_NOTICE);
printk(linux_banner);
setup_arch(&command_line);/*后面会讲到*/
mm_init_owner(&init_mm, &init_task);
setup_command_line(command_line); /*后面会讲到*/
setup_per_cpu_areas();
setup_nr_cpu_ids();
smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */
/*Set up the scheduler prior starting any interrupts (such as the
* timer interrupt). Full topology setup happens at smp_init()
* time - but meanwhile we still have a functioning scheduler.
*/
sched_init();
/*
* Disable preemption - early bootup scheduling is extremely
* fragile until we cpu_idle() for the first time./
preempt_disable();
build_all_zonelists();
page_alloc_init();
printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line);
parse_early_param();/*后面会提到*/
parse_args("Booting kernel", static_command_line, __start___param,
__stop___param - __start___param,
&unknown_bootoption);
if (!irqs_disabled()) {
printk(KERN_WARNING "start_kernel(): bug: interrupts were "
"enabled *very* early, fixing it\n");
local_irq_disable();
}
sort_main_extable();
trap_init();/*设置异常处理函数,很重要,见韦东山《嵌入式LINUX应用开发完全手册》 p397*/
rcu_init();
/* init some links before init_ISA_irqs() */
early_irq_init();
init_IRQ();/*设置异常处理函数,很重要,见韦东山《嵌入式LINUX应用开发完全手册》 p397*/
pidhash_init();
init_timers();
hrtimers_init();
softirq_init();
timekeeping_init();
time_init();//后面会整理
sched_clock_init();
profile_init();
if (!irqs_disabled())
printk(KERN_CRIT "start_kernel(): bug: interrupts were "
"enabled early\n");
early_boot_irqs_on();
local_irq_enable();
/*
* HACK ALERT! This is early. We're enabling the console before
* we've done PCI setups etc, and console_init() must be aware of
* this. But we do want output early, in case something goes wrong. /
console_init();/*后面会讲到*/
if (panic_later)
panic(panic_later, panic_param);
lockdep_info();
/*
* Need to run this when irqs are enabled, because it wants
* to self-test [hard/soft]-irqs on/off lock inversion bugs
* too:
*/
locking_selftest();
#ifdef CONFIG_BLK_DEV_INITRD
if (initrd_start && !initrd_below_start_ok &&
page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {
printk(KERN_CRIT "initrd overwritten (0x%08lx < 0x%08lx) - "
"disabling it.\n",
page_to_pfn(virt_to_page((void *)initrd_start)),
min_low_pfn);
initrd_start = 0;
}
#endif
vmalloc_init();
vfs_caches_init_early();
cpuset_init_early();
page_cgroup_init();
mem_init();
enable_debug_pagealloc();
cpu_hotplug_init();
kmem_cache_init();
debug_objects_mem_init();
idr_init_cache();
setup_per_cpu_pageset();
numa_policy_init();
if (late_time_init)
late_time_init();
calibrate_delay();
pidmap_init();
pgtable_cache_init();
prio_tree_init();
anon_vma_init();
#ifdef CONFIG_X86
if (efi_enabled)
efi_enter_virtual_mode();
#endif
thread_info_cache_init();
cred_init();
fork_init(num_physpages);
proc_caches_init();
buffer_init();
key_init();
security_init();
vfs_caches_init(num_physpages);
radix_tree_init();
signals_init();
/* rootfs populating might need page-writeback */
page_writeback_init();
#ifdef CONFIG_PROC_FS
proc_root_init();
#endif
cgroup_init();
cpuset_init();
taskstats_init_early();
delayacct_init();
check_bugs();
acpi_early_init(); /* before LAPIC and SMP init */
ftrace_init();
/* Do the rest non-__init'ed, we're now alive */
rest_init();
}
在start_kernel()中完成了一系列系统初始化,由于代码过量大,本文将重点介绍与内核移植相关的几部分内容:内核又对u-boot传递过来的参数的接收、开发板的初始化(静态I/O映射过程的实现,时钟初始化、中断初始化、片上设备的注册)、控制台初始化过程、设备模型建立、系统初始化(包括设备,文件系统,内核模块等)和内核启动init进程过程。其它不再详细介绍。