在内核启动的时候首先执行的是header.S(arch/i386/boot/header.S),它使用AT&T的汇编格式编写,以前内核的启动时最开始执行的是一个成为bootsect的程序,它就是MBR的前512字节,会被BIOS程序装载到0x00007C00处然后执行,这样装入过程完全由内核自己完成,但是随着越来越复杂的bootloader的使用,内核也放弃了从软盘启动的方式。
header.S的前512字节的内容是为了兼容使用软驱启动而存在的,他正好存放在磁盘的第一个扇区之内。如果将其作为入口代码被执行的时候会在屏幕上打印出错代码“Direct booting from floppy is no longer supported. Please use a boot loader program instead. Remove disk and press any key to reboot . . .",512字节中还包括一些kernel的属性,如下:
# Kernel attributes; used by setup. This is part 1 of the # header, from the old boot sector. .section ".header", "a" .globl hdr hdr: setup_sects: .byte 0 /* Filled in by build.c */ root_flags: .word ROOT_RDONLY syssize: .long 0 /* Filled in by build.c */ ram_size: .word 0 /* Obsolete */ vid_mode: .word SVGA_MODE root_dev: .word 0 /* Filled in by build.c */ boot_flag: .word 0xAA55 # offset 512, entry point这些字段对应着struct setup_header 结构体的内容,真正的入口是从第二个512字节开始的,bootloader会将控制权交给它,在这里是一条跳转指令,跳转到start_of_setup。 P.S.header.S之后的部分还是对应着 struct setup_header 结构体的内容。
.globl _start _start: # Explicitly enter this as bytes, or the assembler # tries to generate a 3-byte jump here, which causes # everything else to push off to the wrong offset. .byte 0xeb # short (2-byte) jump .byte start_of_setup-1f它对应的是 struct setup_header 结构的jump字段,它是一条16位的指令,前8位是操作码(jmp),后面8位是地址,于是从这条指令开始执行会跳转到start_of_setup处开始执行。它完成的操作如下:
·使用BIOS的0x13H中断复位磁盘系统(入口参数为AH = 0x00H,DL = 0x80H表示硬盘)。
·初始化寄存器
·检查setup最后面的signature
·初始化bss段(清零)
·跳转到main函数执行。
# Jump to C code (should not return) calll main从这里就脱离了header.S的执行了,来到了main函数的执行,位于arch/i386/boot/main.c中。它依次完成如下操作:
·copy_boot_params() /* First, copy the boot header into the "zeropage" */
·console_init() /* Initialize the early-boot console */
·init_heap() /* End of heap check */
·validate_cpu() /* Make sure we have all the proper CPU support */
·set_bios_mode() /* Tell the BIOS what CPU mode we intend to run in. */
·detect_memory() /* Detect memory layout */
·keyboard_set_repeat() /* Set keyboard repeat rate (why?) */
·query_mca() /* Query MCA information */
·query_ist() /* Query Intel SpeedStep (IST) information */
·set_video() /* Set the video mode */
·go_to_protected_mode() /* Do the last things and invoke protected mode */
因为当前系统运行在实模式之下(这是因为CPU启动的时候自动进入实模式),以上的操作是为了进入保护模式做准备,前面的每个函数是对硬件系统的检查和测试,并将测试得到的保存在一个全局的struct boot_params类型的变量中,定义如下:
struct boot_params boot_params __attribute__((aligned(16)));go_to_protected_mode函数之前的每个函数都会填充这个结构体,最后调用go_to_protected_mode函数进入保护模式,它位于arch/i386/boot/pm.c中。它依次完成如下操作:
·realmode_switch_hook() /* Hook before leaving real mode, also disables interrupts */ /*这里主要是关中断(包括外部中断和NMI)*/
·enable_a20() /* Enable the A20 gate */ /* 打开A20地址线 */
·reset_coprocessor() /* Reset coprocessor (IGNNE#) */ /* 重置协处理器 */
·mask_all_interrupts() /* Mask all interrupts in the PIC */ /* 屏蔽PIC的所有中断线 */
·setup_idt(); setup_gdt() /* Actual transition to protected mode... */ /* 设置IDT和GDT */
·protected_mode_jump(boot_params.hdr.code32_start,(u32)&boot_params + (ds() << 4)) /* 跳转并开启保护模式 */
对于保护模式有两个至关重要的表:GDT(全局描述符表)和IDT(中断描述符表),它们的初始化如下:
static void setup_gdt(void) { /* There are machines which are known to not boot with the GDT being 8-byte unaligned. Intel recommends 16 byte alignment. */ static const u64 boot_gdt[] __attribute__((aligned(16))) = { /* CS: code, read/execute, 4 GB, base 0 */ [GDT_ENTRY_BOOT_CS] = GDT_ENTRY(0xc09b, 0, 0xfffff), /* DS: data, read/write, 4 GB, base 0 */ [GDT_ENTRY_BOOT_DS] = GDT_ENTRY(0xc093, 0, 0xfffff), /* TSS: 32-bit tss, 104 bytes, base 4096 */ /* We only have a TSS here to keep Intel VT happy; we don't actually use it for anything. */ [GDT_ENTRY_BOOT_TSS] = GDT_ENTRY(0x0089, 4096, 103), }; /* Xen HVM incorrectly stores a pointer to the gdt_ptr, instead of the gdt_ptr contents. Thus, make it static so it will stay in memory, at least long enough that we switch to the proper kernel GDT. */ static struct gdt_ptr gdt; gdt.len = sizeof(boot_gdt)-1; gdt.ptr = (u32)&boot_gdt + (ds() << 4); asm volatile("lgdtl %0" : : "m" (gdt)); }这里设置了三个描述符,分别是(标志:0xC09B , 基地址:0 , 限长:0xFFFFF)、 (标志:0xC093 , 基地址:0 , 限长:0xFFFFF)和(标志:0x0089 , 基地址:4096 , 限长:103),它们分别用于代码段、数据段和TSS段的描述符,然后将这个表的内容装入到GDTR中,完成了GDT的初步初始化。
static void setup_idt(void) { static const struct gdt_ptr null_idt = {0, 0}; asm volatile("lidtl %0" : : "m" (null_idt)); }这个函数初始化了IDT,可以看出初始化的IDT表中不保存任何内容,在后来的执行过程中再进一步初始化,因为这时候所有的中断都已经被关闭了。
接下来在protected_mode_jump函数中,切换到保护模式,它位于arch/i386/boot/pmjump.S中
movl %cr0, %edx orb $X86_CR0_PE, %dl # Protected mode movl %edx, %cr0这里开启了保护模式,然后在初始化各个段寄存器,最后再跳转到 code32_start段保存的地址中处执行,这个是全局的参数表,它的初始化在header.S中,如下:
疑问:这里的参数是怎么传递的?为什么不是在堆栈上,而是通过eax传递呢?
code32_start: # here loaders can put a different # start address for 32-bit code. .long 0x100000 # 0x100000 = default for big kernel
所以这时候内核的控制权交付到了0x100000的地方开始执行,这个地址存放的是什么代码呢?它在arch/i386/boot/compressed/head_32.S,它完成以下操作:
·初始化段寄存器和一个临时堆栈