u-boot内核启动
扫描二维码
随时随地手机看文章
我们从u-boot启动内核可知道,uboot通过这条命令theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
来启动内核。
那么我们可以内核启动第一步肯定是处理u-boot传入的参数(机器ID、启动参数),再通过一系列的步骤达到最终目的:挂接根文件系统来运行应用程序
我们来看一下整体流程图:
1.内核引导阶段 启动文件head.S和head-common.S
.section ".text.head", "ax" .type stext, %function ENTRY(stext) msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode @ and irqs disabled 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' bl __lookup_machine_type @ r5=machinfo movs r8, r5 @ invalid machine (r5=0)? beq __error_a @ yes, error 'a' bl __create_page_tables ldr r13, __switch_data @ address to jump to after @ mmu has been enabled adr lr, __enable_mmu @ return (PIC) address add pc, r10, #PROCINFO_INITFUNC
首先看这段汇编代码,它主要是用来做一些内核启动前的检测:
__lookup_processor_type 检测内核是否支持当前CPU、__lookup_machine_type检测是否支持当前单板,并且__create_page_tables创建页表,__enable_mmu使能MMU。
这里我们首先打开__lookup_machine_type。
.long __proc_info_begin .long __proc_info_end 3: .long . .long __arch_info_begin .long __arch_info_end /* * Lookup machine architecture in the linker-build list of architectures. * Note that we can't use the absolute addresses for the __arch_info * lists since we aren't running with the MMU on (and therefore, we are * not in the correct address space). We have to calculate the offset. * * r1 = machine architecture number * Returns: * r3, r4, r6 corrupted * r5 = mach_info pointer in physical address space */ .type __lookup_machine_type, %function __lookup_machine_type: adr r3, 3b @ r3 = address of 3,real address,phy address ldmia r3, {r4, r5, r6}@ r4 = "." virtual address of 3,r5 = __arch_info_begin,r6 = __arch_info_end sub r3, r3, r4 @ get offset between virt&phys add r5, r5, r3 @ convert virt addresses to add r6, r6, r3 @ physical address space /* __arch_info_begin = .; *(.arch.info.init) __arch_info_end = .; */ 1: 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 machine 2: mov pc, lr
我们在archarmkernel找到__lookup_machine_type被定义在head-common.S文件中。
开始分析代码:
首先,读出3物理地址给r3。
然后用ldmia指令将r3对应的3条指令的虚拟地址分别存入r4,r5,r6。
所以现在r4=. ; r5=__arch_info_begin ; r6=__arch_info_end
然后用r3-r4求出物理地址和虚拟地址的偏移值,再利用这个偏移值求出r5和r6的实际物理地址。
其中__arch_info_begin
和__arch_info_end
定义在内核目录archarmkernel下vmlinux.lds文件中,
经过起始虚拟地址= (0xc0000000) + 0x00008000逐层叠加得到。
SECTIONS { . = (0xc0000000) + 0x00008000; .text.head : { _stext = .; _sinittext = .; *(.text.head) } .init : { /* Init code and data */ *(.init.text) _einittext = .; __proc_info_begin = .; *(.proc.info.init) __proc_info_end = .; __arch_info_begin = .;// *(.arch.info.init) __arch_info_end = .;//
这里的__arch_info_begin和__arch_info_end中间存放的是段属性为.arch.info.init的结构体。
这里我们可以直接在linux下查询内核中包含.arch.info.init的文件。
/*************Direction:include/asm-arm/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 }; /*************Direction:arch/arm/mach-s3c2440/mach-smdk2440.c******************/ MACHINE_START(S3C2440, "SMDK2440") /* Maintainer: Ben Dooks*/ .phys_io = S3C2410_PA_UART, .io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc, .boot_params = S3C2410_SDRAM_PA + 0x100, .init_irq = s3c24xx_init_irq, .map_io = smdk2440_map_io, .init_machine = smdk2440_machine_init, .timer = &s3c24xx_timer, MACHINE_END
如上:在include/asm-arm/arch.h中找到了定义的结构体类型machine_desc,并且在代码中它的段属性被强制定义成了.arch.info.init。
这样做的目的是在刚刚我们看到的vmlinux.lds链接脚本文件中,可以将具有.arch.info.init段属性的结构体统一放在__arch_info_begin和__arch_info_end之间。
非常便于处理。那么现在我们将这个结构体展开,看看它的内容。也就是将arch/arm/mach-s3c2440/mach-smdk2440.c中的参数传入。展开后如下:
static const struct machine_desc __mach_desc_S3C2440 __used __attribute__((__section__(".arch.info.init"))) = { .nr = MACH_TYPE_S3C2440, .name = "SMDK2440", /* Maintainer: Ben Dooks*/ .phys_io = S3C2410_PA_UART, .io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc, .boot_params = S3C2410_SDRAM_PA + 0x100, //0x30000100 .init_irq = s3c24xx_init_irq, .map_io = smdk2440_map_io, .init_machine = smdk2440_machine_init, .timer = &s3c24xx_timer, };
现在我们看到,定义的结构体类型machine_desc,内容为.nr到.timer。
我们可以看出这个结构体大概是存储硬件信息。
nr存放机器ID,name存放单板名称,phys_io存放输入输出口,io_pg_offst存放IO的偏移地址,boot_params存放uboot传给内核的启动参数(TAG),init_irq存放的是中断初始化信息,map_io为IO的映射表,init_machine存放的是单板的初始化信息,timer存放的是单板的定时器信息。
struct machine_desc { /* * Note! The first four elements are used * by assembler code in head-armv.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); };
我们打开arch.h文件,看到对machine_desc结构体的定义确实和我们刚刚所说的一样。
到此处理u-boot传来的机器ID结束。再回到head-common.S文件,这里对mmap_switch定义:
.type __switch_data, %object __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 cr_alignment @ r6 .long init_thread_union + THREAD_START_SP @ sp /* * The following fragment of code is executed with the MMU on in MMU mode, * and uses absolute addresses; this is not position independent. * * r0 = cp#15 control register * r1 = machine ID * r9 = processor ID */ .type __mmap_switched, %function __mmap_switched: adr r3, __switch_data + 4 ldmia r3!, {r4, r5, r6, r7} cmp r4, r5 @ Copy data segment if needed 1: 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, sp} str r9, [r4] @ Save processor ID str r1, [r5] @ Save machine type bic r4, r0, #CR_A @ Clear 'A' bit stmia r6, {r0, r4} @ Save control register values b start_kernel
mmap_switch做了很多工作,这里我们看到有复制数据段,清BSS段,保存CPU的ID,保存机器ID,清‘A’位,保存控制寄存器的值,然后就到了C语言段——start_kernel函数。
2.内核启动的第二阶段 C语言段—start_kernel
asmlinkage void __init start_kernel(void) { 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); setup_command_line(command_line); printk(KERN_NOTICE "Kernel command line: %sn", boot_command_line); parse_early_param(); parse_args("Booting kernel", static_command_line, __start___param, __stop___param - __start___param, &unknown_bootoption); init_IRQ(); profile_init(); if (!irqs_disabled()) printk("start_kernel(): bug: interrupts were enabled earlyn"); early_boot_irqs_on(); local_irq_enable(); console_init(); rest_init(); }
接下来进入start_kernel启动内核的C函数。
上面是start_kernel的部分代码。
这部分代码的主要作用是处理uboot传递来的参数,设置与体系结构相关的环境,初始化控制台,最后执行应用程序,实现功能。
start_kernel函数框架如下。
内核启动流程: arch/arm/kernel/head.S start_kernel setup_arch(&command_line) // 解析Uboot传入的启动参数 setup_command_line(command_line) // 解析Uboot传入的启动参数 parse_early_param do_early_param 从__setup_start到__setup_end,调用early函数 unknown_bootoption obsolete_checksetup 从__setup_start到__setup_end,调用非early函数 rest_init kernel_init prepare_namespace mount_root //挂接根文件系统 init_post //执行应用程序
这里每一个退格(TAB)都代表此函数被上一个函数调用(例如obsolete_checksetup是unknown_bootoption调用的函数)。 setup_arch(&command_line)和setup_command_line(command_line)就是用来处理uboot传递进来的启动参数的(处理TAG)。 do_early_param从__setup_start到 __setup_end,调用用early标识的函数(但因为__setup_param(str, fn, fn, 0)中early赋值为0,所以不在这里调用),所以我们主要用obsolete_checksetup,后面会提及。 obsolete_checksetup从__setup_start到 __setup_end,调用用非early标识的函数。 mount_root是挂载根文件系统,因为Linux上的应用程序最终要在根文件系统上运行。最后是init_post中运行应用程序。
那么现在就有一个问题,Linux内核是如何接收uboot传来的根文件系统信息的呢?
bootcmd=nand read.jffs2 0x30007FC0 kernel; bootm 0x30007FC0 bootargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0
上面是uboot启动时打印的环境变量。其中我们能够看到根文件系统挂载到第4个分区:root=/dev/mtdblock3 (从0分区开始)。
上面我们提到过,setup_arch(&command_line)和setup_command_line(command_line)就是用来处理uboot传递进来的启动参数的(处理TAG)。但这个处理只是简单的复制粘贴而已,这两个函数将TAG保存,但并未进行真正的处理。那么真正告诉内核在哪里挂载的函数是什么呢?
我们通过查看rest_init->kernel_init->prepare_namespace可以看到一个saved_root_name。查找saved_root_name,发现在Do_mounts.c文件中有对它的调用:
static int __init root_dev_setup(char *line) { strlcpy(saved_root_name, line, sizeof(saved_root_name)); return 1; } __setup("root=", root_dev_setup);//传入一个字符串,一个函数
根据我们之前的经验,我们可以猜测这个__setup宏,也是定义了一个结构体。通过查找__setup我们找到了它的宏定义:
Dir:init.h #define __setup(str, fn) __setup_param(str, fn, fn, 0) #define __setup_param(str, unique_id, fn, early) static char __setup_str_##unique_id[] __initdata = str; static struct obs_kernel_param __setup_##unique_id __attribute_used__ __attribute__((__section__(".init.setup"))) __attribute__((aligned((sizeof(long))))) = { __setup_str_##unique_id, fn, early }
在init.h文件里,定义__setup等于__setup_param。那么在__setup_param的宏定义里,我们可以知道:
它先定义了一个字符串,然后定义了一个结构体类型obs_kernel_param __setup。
这个结构体的段属性为.init.setup,内容为一个字符串,一个函数,还有early。
具备这个属性的结构体被链接脚本文件放到一起,从__setup_start到 __setup_end搜索调用。
在vmlinux.lds中
__setup_start = .; *(.init.setup) __setup_end = .;
但是在Flash里没有分区,只能和uboot一样,将分区在代码里写死。一般在启动Linux的时候,Linux会自动打印出分区的信息。这里我的分区是这样的:
Creating 4 MTD partitions on "NAND 256MiB 3,3V 8-bit": 0x00000000-0x00040000 : "bootloader" 0x00040000-0x00060000 : "params" 0x00060000-0x00260000 : "kernel" 0x00260000-0x10000000 : "root"
我们搜索这个分区名grep ""bootloader"" * -nR
.在arch/arm/plat-s3c24xx/Common-smdk.c中找到分区代码:
static struct mtd_partition smdk_default_nand_part[] = { [0] = { .name = "bootloader", .size = 0x00040000, .offset = 0, }, [1] = { .name = "params", .offset = MTDPART_OFS_APPEND, .size = 0x00020000, }, [2] = { .name = "kernel", .offset = MTDPART_OFS_APPEND, .size = 0x00200000, }, [3] = { .name = "root", .offset = MTDPART_OFS_APPEND, .size = MTDPART_SIZ_FULL, } };
至此,处理完uboot传递的参数,进行CPU和单板的校验,挂载根文件系统等一系列操作后,最终内核执行init_post()中的应用程序。