kernel启动流程之内核启动
内核最终目的: 挂接根文件系统, 运行应用程序(在根文件系统)
因为kernel是由uboot来引导的, 而uboot将控制权交给kernel前在约定地址设置了TAG参数,
kernel启动首先使要去处理uboot传来的参数
从arch/arm/kernel/head.S开始分析:
根据链接脚本vmlinux.lds可以知道, 内核vmlinux首先放所有文件的.tex.head段,
这些段排放的先后顺序是根据的vmlinux依赖文件的顺序来决定的, 首先放arch/arm/kernel/head.o文件的.text.head段
所以kernel代码一开始是运行arch/arm/kernel/head.o文件.tex.head段的代码
.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 @读寄存器获取CPUID
bl __lookup_processor_type @ r5=procinfo r9=cpuid @判断这个版本的kernel是否支持这个处理器
movs r10, r5 @ invalid processor (r5=0)?
beq __error_p @ yes, error 'p' @如果kernel不支持这个处理器, 跳到__error_p
@判断这个版本的kernel是否支持uboot传进来的机器ID bi_arch_number
@对于S3C2440来说 MACH_TYPE_S3C2440 = 362
bl __lookup_machine_type @ r5=machinfo
movs r8, r5 @ invalid machine (r5=0)?
beq __error_a @ yes, error 'a'
@ 创建页表
@ 因为vmlinux.lds对应的地址是虚拟地址,并不对应真实的物理地址,所以要创建页表
bl __create_page_tables
下面分析__lookup_processor_type函数:
uboot启动内核时是调用theKernel函数:
theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
==> 对于2440来说
theKernel (0, 362, bd->bi_boot_params); // bd->bi_boot_params是uboot设置的一些kernel启动参数
传进来的3个参数会放在r0,r1,r2三个寄存器中
r0 = 0, r1 = 362, r2 = bd->bi_boot_params
3: .long .
.long __arch_info_begin
.long __arch_info_end
@这个函数时判断这个版本的kernel是否支持uboot传进来的机器ID
__lookup_machine_type:
adr r3, 3b @r3的地址等于上面3:标签的地址, 实际存在的地址
@ r4 = "." 是虚拟地址, r5 = __arch_info_begin, r6 = __arch_info_end
@ __arch_info_begin和__arch_info_end是在链接脚本vmlinux.lds中定义的
ldmia r3, {r4, r5, r6}
@因为r3是物理地址, r4是虚拟地址, 这里是计算物理地址和虚拟地址的偏差
@r5 = __arch_info_begin加上偏差得到__arch_info_begin的物理地址
@r5 = __arch_info_end加上偏差得到__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
@循环查找machine type是否和uboot传进来的匹配
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
下面是对链接脚本.arch.info.init段的说明:
__arch_info_begin = .;
*(.arch.info.init) // 所有文件的.arch.info.init段, 表示所有架构相关的初始化信息放到一起
__arch_info_end = .;
整个内核查找: grep ".arch.info.init" * -nR
发现在include/asm-xxx/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 \
};
搜索MACHINE_START宏看在哪里使用了, 发现很多架构相关的.c文件中有使用到
比如: arch/arm/mach-s3c2440/mach-smdk2440.c中有使用
MACHINE_START(S3C2440, "SMDK2440")
/* Maintainer: Ben Dooks <ben@fluff.org> */
.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
将其展开:
static const struct machine_desc __mach_desc_S3C2440 \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_S3C2440, \
.name = "SMDK2440",
/* Maintainer: Ben Dooks <ben@fluff.org> */
.phys_io = S3C2410_PA_UART, // 0x50000000 就是UART0控制寄存器的地址
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100, // 0x30000000 + 0x100 = 就是存放内核启动参数的地址
.init_irq = s3c24xx_init_irq,
.map_io = smdk2440_map_io,
.init_machine = smdk2440_machine_init,
.timer = &s3c24xx_timer,
};
可以发现MACHINE_START宏是定义了一个struct machine_desc类型的结构体,
这个结构体的特殊地方就在于它的属性被强制定义为了.arch.info.init,
当链接脚本链接时就会把所有架构已经定义了的struct machine_desc结构体组合在一起
------------------------------------------------------
下面继续分析head.S
@ 当MMU使能之后会跳到__switch_data去执行
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
下面分析跳转__switch_data:
.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) 清BSS段
1: cmp r6, r7
strcc fp, [r6],#4
bcc 1b
ldmia r3, {r4, r5, r6, sp}
str r9, [r4] @ Save processor ID 保存处理器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 @ 最终会调用start_kernel启动内核, 内核第一个C函数
综上: 内核的启动过程
1. __lookup_processor_type 确定这个版本的kernel是否支持这个处理器
2. __lookup_machine_type 确定这个版本的kernel是否支持uboot传进来的机器ID
3. 创建页表, 因为vmlinux.lds对应的地址是虚拟地址,并不对应真实的物理地址,所以要创建页表
4. __enable_mmu 使能MMU
5. 复制数据段
6. 清BSS段
7.保存处理器ID, uboot传进来的机器ID
8. 调用start_kernel
----------------------------------------------
下面分析init/main.c文件里的start_kernel:
asmlinkage void __init start_kernel(void)
{
....
printk(linux_banner); // 输出内核版本信息
setup_arch(&command_line); // 解析uboot传进入的kernel启动参数
setup_command_line(command_line); // 解析uboot传进入的kernel启动参数
...
parse_early_param();
-->do_early_param 早期的参数处理
--> 从__setup_start到__setup_end调用setup_func函数, setup_func就是用来保存命令行参数的
unknown_bootoption
-->obsolete_checksetup
--> 从__setup_start到__setup_end调用setup_func函数, setup_func就是用来保存命令行参数的
...
rest_init();
--> kernel_init()
// 上面的setup_arch和setup_command_line这两个函数只不过是把uboot传进来的参数记录下来而已
--> prepare_namespace()// 确定/根文件系统在哪里挂接, 确定挂接哪个根文件系统
--> mount_root(); // 挂接根文件系统
--> init_post(); // 打开/dev/console, 执行应用程序/sbin/init等等
}
下面分析setup_arch: 对uboot传进来的参数做解析
void __init setup_arch(char **cmdline_p)
{
...
struct machine_desc *mdesc;
char *from = default_command_line; // 默认命令行参数
mdesc = setup_machine(machine_arch_type); // 拿到当前架构定义的struct machine_desc类型的结构体,
if (mdesc->boot_params)
tags = phys_to_virt(mdesc->boot_params); // 拿到uboot设置的kernel启动参数tag
if (tags->hdr.tag == ATAG_CORE) {
...
parse_tags(tags); // 解析TAG
}
parse_cmdline(cmdline_p, from); // 解析命令行
}
----------------------------------------------------------------------------
在mount_root挂接根文件系统之前肯定要先知道在哪里挂接根文件系统,是由prepare_namespace函数决定的
下面分析prepare_namespace函数:
void __init prepare_namespace(void)
{
...
if (saved_root_name[0]) {
root_device_name = saved_root_name;
...
ROOT_DEV = name_to_dev_t(root_device_name); // ROOT设备在这里被设置
...
}
}
查找saved_root_name:
static int __init root_dev_setup(char *line)
{
strlcpy(saved_root_name, line, sizeof(saved_root_name));
return 1;
}
__setup("root=", root_dev_setup); // 这是一个宏
#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 }
展开:
static char __setup_str_root_dev_setup[] __initdata = "root="; \
static struct obs_kernel_param __setup_root_dev_setup \
__attribute_used__ \
__attribute__((__section__(".init.setup"))) \
__attribute__((aligned((sizeof(long))))) \
= { __setup_str_root_dev_setup, root_dev_setup, 0 }
可以看到这个宏展开后
定义了一个__initdata字符串和结构体struct obs_kernel_param结构体(属性是.init.setup)
.init.setup属性在vmlinux.lds中用到, 会把所有.init.setup属性的结构放到一个段
__setup_start = .;
*(.init.setup)
__setup_end = .;
搜索__setup_start是被谁调用就知道命令行是怎么被使用的
发现do_early_param和obsolete_checksetup两个函数调用了__setup_start
do_early_param函数进行早期参数的初始化,
从__setup_start到__setup_end调用setup_func函数, setup_func就是用来保存命令行参数的
综上:
bootargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0
mount_root挂接根文件系统是由命令行参数root=/dev/mtdblock3决定的,
uboot传进来的命令行参数是由__setup定义的结构体去保存和定义处理函数,被链接脚本放到__setup_start到__setup_end之间,
由do_early_param和obsolete_checksetup函数从__setup_start到__setup_end一一调用处理函数做处理的,
----------------------------------------------------------------------------
分析start_kernel函数中可以看出最终调用init_post函数
在调用init_post函数之前kernel的启动初始化(包括挂接根文件系统等)已经基本完成,
init_post函数启动用户模式,开始运行应用程序
下面分析init_post函数:
static int noinline init_post(void)
{
...
// 打开/dev/console设备文件, 这个文件称为终端, 对应UART0
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); // 重定向, 复制一个打开的文件号, 指向/dev/console
(void) sys_dup(0); // 重定向, 复制一个打开的文件号, 指向/dev/console
...
run_init_process("/sbin/init"); // 执行/sbin/init应用程序
...
}
kernel目的是挂接根文件系统, 运行根文件系统上的应用程序, 根文件系统哪里来, 可以通过busybox构建