PC上Linux的启动过程通常是:BIOS初始化并加载MBR,跳转到MBR所处位置执行bootloader(例如grub)的代码,bootloader启动后加载内核镜像文件并跳转到镜像文件处执行,此时才真正进入到linux kernel的启动过程。本文主要分析Linux kernel的启动过程,代码均基于linux-3.4.4。
Linux Kernel镜像文件的组成
Linux Kernel镜像文件的组成可以理解为由512字节的boot sector区、kernel setup代码区及vmlinux组成。512字节的boot sector区包含一些参数,其中一个很重要的参数指出了kernel setup代码区大小;kernel setup代码区(setup.bin)主要完成系统检测及运行环境初始化的工作;vmlinux包含解压缩代码区以及被压缩的内核。
Linux Kernel启动过程
Bootloader将boot sector区、kernel setup代码区加载到0x001000后的某个"low memory"处,并将 vmlinux加载到内存0x100000,最后将控制权交给kernel setup。
kernel setup的入口在arch/x86/boot/header.S文件第256行的_start处,此处跳转到该文件的第389行start_of_setup处准备C代码运行环境(包括堆栈设置、setup标签检查、BSS段清零等),然后在该文件的449行调用arch/x86/boot/main.c文件的main函数。
在arch/x86/boot/main.c文件的main函数中,拷贝boot_params,初始化硬件(console的初始化、设置BIOS模式、内存布局检测、video模式设置、GDT和IDT的初始化等),最后调用go_to_protected_mode函数以进入保护模式。
go_to_protected_mode设置好IDT、GDT后,调用arch/x86/boot/pmjump.S文件中第26行的protected_mode_jump,调用参数分别是vmlinux的入口地址以及启动参数所处位置的地址。
在protected_mode_jump中设置cr0进入保护模式,并跳转到51行in_pm32处。in_pm32设置好寄存器后,跳转到vmlinux的入口处。
vmlinux的入口地址在arch/x86/boot/compressed/head_32.S文件第34行startup_32处。startup_32将被压缩的内核解压到某处,最后第214行跳转到解压后的内核的入口处。
解压后的内核的入口处在arch/x86/kernel/head_32.S文件第87行startup_32处。startup_32将寄存器设置适当的值,并在第202行开始建立初始页表,如下所示:
202 page_pde_offset = (__PAGE_OFFSET >> 20);
203
204 movl $pa(__brk_base), %edi // 首个页表的首地址为pa(__brk_base)
205 movl $pa(initial_page_table), %edx // 将PGD的首地址保存在edx
206 movl $PTE_IDENT_ATTR, %eax // eax初始化为物理地址0x0 #define PTE_IDENT_ATTR 0x003 /* PRESENT+RW */
// #define PDE_IDENT_ATTR 0x067 /* PRESENT+RW+USER+DIRTY+ACCESSED */
207 10:
208 leal PDE_IDENT_ATTR(%edi),%ecx /* Create PDE entry */
209 movl %ecx,(%edx) /* Store identity PDE entry */ // 从PGD的第 0项开始保存新建的页表
210 movl %ecx,page_pde_offset(%edx) /* Store kernel PDE entry */ // 从PGD的第768项开始保存新建的页表
211 addl $4,%edx // edx指向PGD的下一项
212 movl $1024, %ecx // 设置ecx为1024
213 11:
214 stosl // 将eax的值保存到edi指向的地址中,并使edi自增4(即初始化新建页表的每一项,完成1024次后edi将指向下一个页表的首地址)
215 addl $0x1000,%eax // eax + 4K, 每页映射4KB内存
216 loop 11b
217 /*
218 * End condition: we must map up to the end + MAPPING_BEYOND_END.
219 */
// MAPPING_BEYOND_END: 映射内核空间所需的所有页表的大小(若内核空间是1G,其大小为 256 * 4kB,256个页表,每个页表4KB)
// 此处映射的总内存大小为:从物理地址0x0 处开始,到物理地址 “pa(_end) + 映射内核空间所需的所有页表的大小”结束
220 movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp
221 cmpl %ebp,%eax
222 jb 10b
223 addl $__PAGE_OFFSET, %edi
224 movl %edi, pa(_brk_end)
225 shrl $12, %eax
226 movl %eax, pa(max_pfn_mapped)
227
228 /* Do early initialization of the fixmap area */
229 movl $pa(initial_pg_fixmap)+PDE_IDENT_ATTR,%eax
230 movl %eax,pa(initial_page_table+0xffc)
在这之后跳转到291行default_entry处。从348行开始准备启用分页机制:
345 /*
346 * Enable paging
347 */
348 movl $pa(initial_page_table), %eax
349 movl %eax,%cr3 /* set the page table pointer.. */
350 movl %cr0,%eax
351 orl $X86_CR0_PG,%eax
352 movl %eax,%cr0 /* ..and set paging (PG) bit */ // 启用分页
353 ljmp $__BOOT_CS,$1f /* Clear prefetch and normalize %eip */
354 1:
355 /* Shift the stack pointer to a virtual address */
356 addl $__PAGE_OFFSET, %esp //校正esp, 旧的esp保存的是物理地址,现在转成虚拟地址
357
358 /*
359 * Initialize eflags. Some BIOS's leave bits like NT set. This would
360 * confuse the debugger if this code is traced.
361 * XXX - best to initialize before switching to protected mode.
362 */
363 pushl $0
364 popfl
365
366 #ifdef CONFIG_SMP
367 cmpb $0, ready
368 jnz checkCPUtype
369 #endif /* CONFIG_SMP */
370
371 /*
372 * start system 32-bit setup. We need to re-do some of the things done
373 * in 16-bit mode for the "real" operations.
374 */
375 call setup_idt //再次初始化IDT
376
377 checkCPUtype:
378
379 movl $-1,X86_CPUID # -1 for no CPUID initially
380
... ...
468 movl $(__KERNEL_STACK_CANARY),%eax
469 movl %eax,%gs
470
471 xorl %eax,%eax # Clear LDT
472 lldt %ax
473
474 cld # gcc2 wants the direction flag cleared at all times
475 pushl $0 # fake return address for unwinder
476 movb $1, ready
477 jmp *(initial_code) //跳转到 i386_start_kernel
478
... ...
617 __REFDATA
618 .align 4
619 ENTRY(initial_code)
620 .long i386_start_kernel
621
start_kernel()中调用了一系列初始化函数,以完成kernel本身的设置。这些动作有的是公共的,有的则是需要配置的才会执行的。
在start_kernel()函数中,
start_kernel最后调用rest_init。rest_init 创建第一个核心线程来执行kernel_init(),原执行序列调用cpu_idle()等待调度。
kernel_init调用smp_init()来初始化SMP机器其余CPU(除当前引导CPU),最后调用init_post()来启动init进程。
至此,内核启动完成。
启动过程的调用关系
_start (arch/x86/boot/header.S)
->start_of_setup(arch/x86/boot/header.S)
->main (arch/x86/boot/main.c)
->go_to_protected_mode (arch/x86/boot/pm.c)
->protected_mode_jump (arch/x86/boot/pmjump.S)
->in_pm32(arch/x86/boot/pmjump.S)
->startup_32 (arch/x86/boot/compressed/head_32.S)
->startup_32 (arch/x86/kernel/head_32.S)
->i386_start_kernel (arch/x86/kernel/head32.c)
->start_kernel (init/main.c)
->rest_init
->kernel_init init进程
参考文章:
1. Linux启动过程综述