话说看linux内核源码真是一件辛苦的事情啊,为了弄清楚操做系统,我从linux源码看到grub源码,再看到BIOS,真心是伤不起啊。我研究的linux内核版本是2.6.34.13,它不支持从直接从内核启动,须要一个bootloader。目前linux中使用最普遍的的bootloader是grub,本文就是对grub2.00(下文中简写为grub)在x86下的分析研究。
grub的源码主要分为四个部分,分别为grub-core/boot/i386/pc/boot.S,
grub-core/boot/i386/pc/
diskboot
.S,grub-core/boot/i386/pc/
startup_raw
.S,以及grub的核心代码
。
- boot.S是MBR中的512个字节,主要工做是加载diskboot.S。
- diskboot.S也是512字节的代码,它的做用是加载startup_raw.S和grub的核心代码。
- startup_raw.S的做用是解压grub的核心代码。
- grub的核心代码是grub真正功能实现的地方。
通常
boot.S是放在第一个扇区(即MBR),
diskboot.S是放在第二个扇区,startup_raw.S和grub核心代码放在接下来的几十个扇区(可是限于0-63扇区)。下面按照grub的执行流程来研究grub的工做原理。
1. grub-core/boot/i386/pc/boot.S
计算机启动后(略过BIOS部分)会将硬盘的第一个扇区加载到0000:7C00处,这个扇区的布局以下图所示。
此时IP寄存器的值为7C00,第一条指令为跳转指令,跳到BPB(BIOS Parameter Block)后面的代码处执行。这一部分比较简单,首先初始化段寄存器,而后从启动盘读取第二个扇区到0000:8000处。
/* boot kernel */
jmp *(kernel_address) /* kernel_address = 0000:8000 */
随这这一条指令的执行,boot.S完成了它的使命,将执行权交给了diskboot.S。
2. grub-core/boot/i386/pc/diskboot.S
diskboot.S的工做跟boot.S相似,它会根据配置信息从启动盘中读取很少于62个扇区的数据。它会将这些数据放在0000:8200处的一段内存中,其中开始部分是start_raw.S的代码,后一部分是压缩的grub核心代码。
ljmp $0, $(GRUB_BOOT_MACHINE_KERNEL_ADDR + 0x200)
这条跳转指令是diskboot的最后一条指令(不考虑意外状况),实际上就是跳转到0000:8200处。
3. grub-core/kern/i386/pc/startup_raw.S
startup_raw.S的部分关键代码以下,首先须要
设置数据段、堆栈段和扩展段寄存器,以及栈指针。
/* set up %ds, %ss, and %es */
xorw %ax, %ax
movw %ax, %ds
movw %ax, %ss
movw %ax, %es
/* set up the real mode/BIOS stack */
movl $GRUB_MEMORY_MACHINE_REAL_STACK, %ebp
movl %ebp, %esp
进行一些准备工做后开始进入保护模式,
real_to_prot
这个函数的代码在
grub-core/kern/i386/realmode
.S中
。
/* transition to protected mode */
DATA32 call real_to_prot
进入保护模式后就调用
_LzmaDecodeA解压压缩的核心代码,
_LzmaDecodeA这个函数在
lzma_decode.S中定义
。紧接着的几条指令的做用是设置参数,而后跳转到核心代码。 movl $GRUB_MEMORY_MACHINE_DECOMPRESSION_ADDR, %edi
...
popl %esi
movl LOCAL(boot_dev), %edx
movl $prot_to_real, %edi
movl $real_to_prot, %ecx
movl $LOCAL(realidt), %eax
jmp *%esi
解压后的核心最开始放在0x00100000处,在上面的指令中,esi寄存器的值就是核心代码的地址,edx、edi、ecx、eax分别是启动设备、从保护模式进入实模式函数的地址、从实模式进入保护模式函数的地址、实模式中断描述符表的地址。
4. grub-core/kern/i386/pc/startup.S
startup.S首先保存传递过来的参数,具体实现是下面的三条指令。 movl %ecx, (LOCAL(real_to_prot_addr) - _start) (%esi)
movl %edi, (LOCAL(prot_to_real_addr) - _start) (%esi)
movl %eax, (EXT_C(grub_realidt) - _start) (%esi)
为何不直接使用这几个变量的地址呢?这是由于startup.S当前所在地址为0x00100000,而代码的目标地址为0x00008200,因此须要转换一下变量的地址。
如今须要将核心代码移动到目标地址,下面的代码完成了移动的工做。 /* copy back the decompressed part (except the modules) */
movl $(_edata - _start), %ecx
movl $(_start), %edi
rep
movsb
movl $LOCAL (cont), %esi
jmp *%esi
LOCAL(cont):
ecx的值是核心代码的大小,由_edata减_start得来。edi的值为_start,也就是目标地址。esi是核心代码如今所在的地址,在startup_raw.S中设置,到如今一直未有变更。
移动完成之后,jmp指令完成了到目标地址的跳转,这个跳转很巧妙,虽然整个代码移动了位置,但看起来就像没有移动同样。 /*
* Call the start of main body of C code.
*/
call EXT_C(grub_main)
接下来清空BSS(Block Started by Symbol
),调用
grub_main进入grub的主函数,这个函数在
grub-core/kern/main.c中
。到这来咱们已经来到了grub的核心代码部分,这部分主要是grub的模块化框架的初始化,各类命令的注册,各类模块的加载等等。
我比较关心的是grub在用户选择指定的系统后怎么加载linux的,精力有限,其它代码暂时就不去阅读了。当用户选择指定的内核条目后,
grub-core/commands/boot.c中的命令函数
g
rub_cmd_boot开始执行
。
加载linux的代码在grub-core/loader/i386目录下,grub支持16位、32位、64位三种模式启动linux内核。16位模式下会从新回到实模式,再启动linux内核。32位和64位两种模式下不须要再进入实模式,直接在保护模式启动。
linux.c文件中的
grub_cmd_linux函数就是处理linux内核加载的
。函数
grub_cmd_linux的做用是根据命令行参数生成相关配置数据,而后设置一个回调函数,由
g
rub_cmd_boot调用。这样作的好处是减少了代码的耦合度。
g
rub_cmd_boot和
grub_linux_boot两个函数
将linux内核从文件系统中读取到内存中,内核代码分为两部分,一部分是实模式代码,一部分是保护模式代码。实模式代码通常放在0x00009000处,保护模式代码通常放在0x00100000处,在
32位和64位两种模式启动实际上不须要实模式部分代码
。此外,
g
rub为linux内核设置了
linux_kernel_params
参数,这些参数在linux内核中会用到。
5.
grub_relocatorXX_boot
离进入linux内核就差最后一步了,
grub_linux_boot最后调用
grub_relocatorXX_boot(.../
lib/.../
relocator.c
)
进入内核。顾名思义,最后所要作的事情是重定位。
grub_cmd_linux只是将加载了内核的代码
,这部分代码要能使用还必须对代码的每一个chunk进行重定位。
重定义这部分代码不是很复杂,须要结合ELF文件格式阅读,这里就很少说了。调用
grub_relocator_prepare_relocs重定位好以后,以下面代码所示,先关闭中断,而后调用
relst()
就进入到了linux内核,
relst()后面的代码是永远不会执行的
。
asm volatile ("cli");
((void (*) (void)) relst) ();
/* Not reached. */
return GRUB_ERR_NONE;
到这里为止,本文就算是把grub启动linux简要叙述了一遍,其中还有不少细节这里没有讲,之后有时间再好好看看。