到保护模式的代码了,最先执行的代码就是arch/x86/boot/compressed/head_32.S中的startup_32,对于bzImage,grub把它加载到0x100000的位置。
首先,来到34行ENTRY(startup_32),在这里,第一条保护模式的指令开始了。注意,前面的set_gdt仅仅是为了进入保护模式后的ljmp,意义并不大。
34ENTRY(startup_32)
35 cld
36 /*
37 * Test KEEP_SEGMENTS flag to see if the bootloader is asking
38 * us to not reload segments
39 */
40 testb $(1<<6), BP_loadflags(%esi)
41 jnz 1f
42
43 cli
44 movl $__BOOT_DS, %eax
45 movl %eax, %ds
46 movl %eax, %es
47 movl %eax, %fs
48 movl %eax, %gs
49 movl %eax, %ss
501:
51
52/*
53 * Calculate the delta between where we were compiled to run
54 * at and where we were actually loaded at. This can only be done
55 * with a short local call on x86. Nothing else will tell us what
56 * address we are running at. The reserved chunk of the real-mode
57 * data at 0x1e4 (defined as a scratch field) are used as the stack
58 * for this calculation. Only 4 bytes are needed.
59 */
60 leal (BP_scratch+4)(%esi), %esp
61 call 1f
35行,第一条进入32位保护模式后的指令,清除方向标志指令。随后根据hdr的loadflags的第7位是否被设置,我们看到根本没有设置,所以经过44~49行设置各个段寄存器。注意40行有一个%esi寄存器,我们在protected_mode_jump汇编代码中看到了,它存放着刚才c语言代码中设置好了的boot_params变量的地址。这个地址在传入protected_mode_jump之后变成了32位:(u32)&boot_params + (ds() << 4)。这个东西就是所谓0号页面的内容。
再一个就是BP_loadflags,后面会看到很多BP开头的常量。这些常量都是编译的时候设置的某些数据结构中一些字段地址偏移,大家可以去看看arch/x86/kernel/asm-offsets_32.c的最后几行,BP开头的常量就是boot_params结构的某字段的偏移。BP_loadflags就是hdr.loadflags相对于boot_params结构首部的偏移,于是BP_loadflags+%esi就是loadflags的地址。
再看到60行,BP_scratch是scratch字段相对于boot_params结构首部的偏移,其实就是让栈顶指向(BP_scratch+4)(%esi)地址,根据注释来讲这是一个“刮刮卡”区域。这个“刮刮卡”是用来干什么的呢?前面main.c中没有对它赋值啊。原来,它是用来计算当期内核映像位置的。60行lea指令得到boot_params.scratch的32位物理地址,存放到esp寄存器中。虽然前面说了的那个X,grub加载linuz的时候把这个值设置成0x90000,但为了对所有boot loader兼容,所有代码在这里做了一个通用化处理,现场计算这个值。但请注意,这里并不是0x90000,而是boot_params.scratch的32位物理地址,这样可以把前面已经执行过的代码略过,减少待会儿的拷贝量。然后到62行:
621: popl %ebp
63 subl $1b, %ebp
64
65/*
66 * %ebp contains the address we are loaded at by the boot loader and %ebx
67 * contains the address where we should move the kernel image temporarily
68 * for safe in-place decompression.
69 */
70
71#ifdef CONFIG_RELOCATABLE
72 movl %ebp, %ebx
73 movl BP_kernel_alignment(%esi), %eax
74 decl %eax
75 addl %eax, %ebx
76 notl %eax
77 andl %eax, %ebx
78#else
79 movl $LOAD_PHYSICAL_ADDR, %ebx
80#endif
81
82 /* Target address to relocate to for decompression */
83 addl $z_extract_offset, %ebx
84
85 /* Set up the stack */
86 leal boot_stack_end(%ebx), %esp
87
88 /* Zero EFLAGS */
89 pushl $0
90 popfl
62行出栈,于是ebp寄存器就存放boot_params.scratch开始的32位的地址,随后再减4,把scratch再略过。再来,由于我们在顶层.config文件中看见CONFIG_RELOCATABLE=y,所以进入72行,经过72到77行的处理,ebx就指向了BP_kernel_alignment偏移的位置,按照注释讲是将要存放内核映像位置的32位首地址。因为BP_kernel_alignment正是boot_params.hdr.kernel_alignment,在我们的header.S的第200行把它赋值成了CONFIG_PHYSICAL_ALIGN,查看我们的.config文件,这个值是0x1000000。现在保护模式打开了,这个32位地址是在1MB以后的。所有后面的拷贝主要是把内核映像其余部分拷贝到1MB以后。
随后,对ebx进行调整,增加一个z_extract_offset的值,这个值我不太清楚是怎样来的,好像是编译的时候产生的。然后再把ebx偏移boot_stack_end的地址赋值给esp,这样就可以使用栈了。
92/*
93 * Copy the compressed kernel to the end of our buffer
94 * where decompression in place becomes safe.
95 */
96 pushl %esi
97 leal (_bss-4)(%ebp), %esi
98 leal (_bss-4)(%ebx), %edi
99 movl $(_bss - startup_32), %ecx
100 shrl $2, %ecx
101 std
102 rep movsl
103 cld
104 popl %esi
然后进入96行代码,先把boot_params首地址压栈保存起来,然后将内核映像从ebp偏移_bss-4开始的内容拷贝到ebx偏移_bss-4开始对应的内存单元中,拷贝的长度是_bss- startup_32。注意,_bss 是待会看到的解压缩程序的BSS段,_bss - startup_32涵盖了linuz从startup_32起整个以后的代码长度,包括整个待解压内核,然后再右移2位,即除以4,这样102行rep的时候按4个字节32位一组地进行拷贝,方便快捷。
注意第101行,设置了方向标志置位,所有rep的时候是由高地址向低地址进行,从_bss到现在我们正在进行的startup_32。
做这么一个拷贝的目的是把内核映像拷贝到0x1000000以后的内存单元中,进入保护模式后,下一步主要是解压缩内核映像,而这样就可以很安全的执行解压缩的c代码了。
105
106/*
107 * Jump to the relocated address.
108 */
109 leal relocated(%ebx), %eax
110 jmp *%eax
111ENDPROC(startup_32)
拷贝完成后,加载存在于0x1000000以后的内存单元中新的relocated位置。所用的是%ebx对应的地址,而不是直接jmp relocated。所以,看到114行的relocated,执行到此时此刻,已经离开了刚才grub加载linuz的位置了,来到了新的保护模式位置。
113 .text
114relocated:
115
116/*
117 * Clear BSS (stack is currently empty)
118 */
119 xorl %eax, %eax
120 leal _bss(%ebx), %edi
121 leal _ebss(%ebx), %ecx
122 subl %edi, %ecx
123 shrl $2, %ecx
124 rep stosl
125
relocated一来首先清洗BSS段,使用的rep指令,很简单因为刚才没有拷贝BSS段,而在这里必须建立一个新的,继续走:
126/*
127 * Do the decompression, and jump to the new kernel..
128 */
129 leal z_extract_offset_negative(%ebx), %ebp
130 /* push arguments for decompress_kernel: */
131 pushl %ebp /* output address */
132 pushl $z_input_len /* input_len */
133 leal input_data(%ebx), %eax
134 pushl %eax /* input_data */
135 leal boot_heap(%ebx), %eax
136 pushl %eax /* heap area */
137 pushl %esi /* real mode pointer */
138 call decompress_kernel
139 addl $20, %esp
140
这段代码全是pushl,即压栈操作,为将进行的解压缩程序decompress_kernel函数准备参数,我们将在下一节予以详细分析。