摘要:这个流程图是大牛M. Tim Jones在Inside Linux boot process中绘制的。清楚的show出了我们从摁开机键开始,计算机所做的事情。今天我要分享的是是stage1 bootloader部分,以及stage2 bootloader的部分
玩Linux的人,肯定会听说过Grub这个神奇的东西,就是开机启动时候下拉一个菜单让我们选操作系统的那个东东。
闲言少叙。我们首先看下Linux的启动过程流程图:
这个流程图是大牛M. Tim Jones在Inside Linux boot process中绘制的。清楚的show出了我们从摁开机键开始,计算机所做的事情。今天我要分享的是是stage1 bootloader部分,以及stage2 bootloader的部分。
需知,刚刚进入stage1 bootloader的时候,操作系统还没有开始运行(废话,GRBU他老人家就是召唤操作系统的),也没有文件系统的概念,认为直接运行操作系统可执行文件的,就可以洗洗睡了,GRUB他老人家面临的是一穷二白的局面啊。
下面的实验都是在基于2.6.24内核的操作系统上做的,和家用的Ubuntu操作系统不同。
MBR 是master boot record的缩写,也就是主引导记录。我前面写过一篇分析MBR里面分区信息的文章。
我们跳到/boot/grub/grub目录下,可以看到有个文件叫stage1,当然了还有stage2,稍安勿燥,我们会慢慢提到。我们下载一份GRUB 0.95或者grub 0.97的代码,我们可以看到在stage1目录下有个stage1.S的汇编文件。他们之间是什么关系。
先说MBR。MBR是磁盘的0柱面,0磁道,1扇区(扇区从1开始计数),既然是一个扇区,大小就确定了,就是512个字节。MBR严格的说有三部分组成:
细心的同学可以发现,/boot/grub/stage1文件的大小也是512字节一个扇区那么大。 我们可以比较下MBR和/boot/grub/stage1文件的内容。获取MBR方法比较简单,dd就可以了,如下:
dd if=/dev/sda of=mbr_0_512 bs=512 count=1这样,我们就获得了磁盘的0柱面,0磁道 1扇区的内容,即MBR,存放在了mbr_0_512文件中: 看一下文件的内容:
再看下/boot/grub/目录下的是stage1文件。(不要被图片误导,我只是将stage1文件拷贝到了我的工作目录)
可以看到这两个文件大部分是相同的,其实这部分code部分是一样的,至于不一样的地方是:
不同处 1[0x1b8, 0x1bb)这个部分叫做optional disk signature,Windows系的产品会用到这4个字节,对于Linux和grub是用不到这4个字节的。
结论是/boot/grub/stage1文件和主引导记录MBR的code部分是相同的。事实上这份代码是从grub源代码的stage1/stage1.S汇编出来的。stage1.S是grub的第一个文件,便以后编译后产生的代码,正好是512字节,不是正好,是必须,否则无法放入1个扇区。
这个MBR的信息是grub安装上去的,方法如下:
stage1阶段的源代码就是grub源码中stage/stage1.S,这段汇编是at&t风格的汇编,折磨的我七荤八素,死去活来。
故事从哪里讲起呢,还是从我们按下电源开关开始讲起。呵呵。
当我们按下开机键,进入系统启动阶段,什么BIOS, POST(Power-On Self Test加电自检),反正是一陀名词一陀事,这些我们统统不管,我们就从系统BIOS做的最后一件事开始讲起,BIOS最后一件事:根据用户指定的启动顺序从软盘、硬盘或光驱启动MBR。在这个过程中会按照启动顺序顺序比较其放置MBR的位置的结尾两位是否为0xAA55,通过这种方式判断从哪个引导设备进行引导。在确定之后,将该引导设备的MBR内容读入到0x7C00的位置,并再次判断其最后两位,当检测正确之后,进行阶段1的引导,从此进入第二阶段 stage1 bootloader阶段。
简单地说,就是BIOS执行INT 0x19,加载MBR内容至0x7c00,然后跳转执行。
为啥是0x7c00位置呢?我找到了一篇文章:Why BIOS loads MBR into 0x7C00 in x86 ?英文好的同学可以自行搜索阅读。
简单的说,0x7c00=32KB-1024B,是32K的最后一个KB,这个magic number不是intel决定的,所以我们在X86相关的文档中无法找到这个magic number的说明,这个magic number属于 BIOS specifiction 。这个0x7c00 是 IBM PC 5150 BIOS developer team 决定的。
BIOS developer team decided 0x7C00 because:
They wanted to leave as much room as possible for the OS to load itself within the 32KiB.
8086/8088 used 0x0 - 0x3FF for interrupts vector, and BIOS data area was after it.
跑了半天题,我们继续。我们把MBR的code加载到了0x7c00,开始执行MBR处的代码,下面重点分析MBR处的代码,即grub源码中的stage1/stage1.S
jmp after_BPB nop /* do I care about this ??? */ . = _start + 4一开始是个跳转指令,直接跳转到after_BPB,后面的NOP就执行不到了。ater_BPB在后面有定义:对于mbr二进制文件而言:
头两个字节0xeb48,eb是JMP指令,第三个字节是0x90,这个字节是NOP指令。
after_BPB: /* general setup */ cli /* we're not safe here! */ /* * This is a workaround for buggy BIOSes which don't pass boot * drive correctly. If GRUB is installed into a HDD, do * "orb $0x80, %dl", otherwise "orb $0x00, %dl" (i.e. nop). */ .byte 0x80, 0xca首先是cli指令,禁用中断然后是显示80ca这个二进制码。看下注释,这个80ca的含义是orb $0x80,%dl.意思是给dl寄存器的赋值80。
DL = 00h 1st floppy disk ( “drive A:” ) DL = 01h 2nd floppy disk ( “drive B:” ) DL = 80h 1st hard disk DL = 81h 2nd hard disk因为我们是磁盘加载的MBR,所以我们dl里面存的是0x80。接下来分析:
ljmp $0, $ABS(real_start) real_start: /* set up %ds and %ss as offset from 0 */xorw %ax, %ax movw %ax, %ds movw %ax, %ss /* set up the REAL stack */movw $STAGE1_STACKSEG, %sp sti /* we're safe again */MOV_MEM_TO_AL(ABS(boot_drive)) /* movb ABS(boot_drive), %al */cmpb $GRUB_INVALID_DRIVE, %al je 1fmovb %al, %dl进入real_start了,ax清零,ds赋值0,ss赋值0,将STAGE1_STACKSEG(0×2000)赋值给sp,这样就设置了实模式下的堆栈段地址(栈顶位置)ss:sp = 0×0000:0×2000。接着置中断允许位。然后将dl寄存器中的值拷贝到al寄存器,然后将al寄存器的值和0xFF比较,对于我们的场景来说,我们是al里面存的是0x80,所以,不等于0xFF,不用跳转,继续执行将al的0x80拷贝到dl寄存器中。
1: /* save drive reference first thing! */pushw %dx /* print a notification message on the screen */MSG(notification_string) /* do not probe LBA if the drive is a floppy */testb $STAGE1_BIOS_HD_FLAG, %dl jz chs_mode /* check if LBA is supported */movb $0x41, %ah movw $0x55aa, %bx int $0x13notification_string是GRUB,这一段截至到MSG是显示GRUB到屏幕上,因为这个MSG是细节,我们按下不表。总是作用是屏幕显示GRUB。我们还记得,MBR内容里面有如下信息:
testb 这部分是探测drive是否是硬盘,如果不是硬盘是软盘,直接采用CHS_MODE,就不用费事判断了。我们知道,80h和81h是硬盘,所以探测对应bit位。如果我们的启动设备是硬盘,按么我们需要检测LBA是否支持。
通过 BIOS 调用 INT 0x13 来确定是否支持扩展,LBA 扩展功能分两个子集 , 如下 :
我们采用的是ah=41h,bx=0x55aa,dl=0x80,所以是检查扩展是否存在。这个操作会改变CF标志位的值。如果支持LBA,那么CF=0,否则CF=1。
* use CHS if fails */ jc chs_mode cmpw $0xaa55, %bx jne chs_mode /* check if AH=0x42 is supported if FORCE_LBA is zero */MOV_MEM_TO_AL(ABS(force_lba)) /* movb ABS(force_lba), %al */ testb %al, %al jnz lba_mode andw $1, %cx jz chs_mode我们刚才BIOS用 INT 0x13探查是否采用LBA模式。存在下面几种情况:
启动设备不是0x80,0x81,压根不探查,直接采用CHS 模式探查结果 CF=1,二话不说,跳转到CHS模式CF=0是否就采用LBA呢?也不一定,还需要判断bx==0x55aa,bx==0x55aa,采用LBA模式,否则CHS模式有一个FORCE_LBA Byte,如果这个位是1,那么直接采用LBA MODE,这个位是哪个呢?
0x41对应的00,表示FORCE_LBA是zero。
接下来,就是花开两朵,各表一枝,一枝叫CHS,另一枝叫LBA模式。 CHS已经人老珠黄,它是硬盘容量很小的那个时代留下的遗产。
C表示CylindersH表示HeadsS表示Sectors其中:
磁头数(Heads)表示硬盘总共有几个磁头,也就是有几面盘片, 最大数为 255 (用 8 个二进制位存储)。从0开始编号。
柱面数(Cylinders) 表示硬盘每一面盘片上有几条磁道,最大数为 1023(用 10 个二进制位存储)。从0开始编号。
扇区数(Sectors) 表示每一条磁道上有几个扇区, 最大数为 63(用 6个二进制位存储)。从1始编号。
而现在的硬盘远大于8.414 GB(按照硬盘厂商常用的单位的计算) ,CHS寻址方式已不能满足要求。可到目前为止, 人们常说的硬盘参数还是这古老的 CHS参数。那么为什么还要使用这些参数?向下兼容。
既然CHS已经人老珠黄,我们也没必要在它身上浪费时间了(这话说的,怎么和陈世美这么像?!)我们关注的重点是LBA MODE.
lba_mode: /* save the total number of sectors */ movl 0x10(%si), �x /* set %si to the disk address packet */ movw $ABS(disk_address_packet), %si /* set the mode to non-zero */ movb $1, -1(%si) movl ABS(stage2_sector), �x /* the size and the reserved byte */movw $0x0010, (%si)/* the blocks */movw $1, 2(%si)/* the absolute address (low 32 bits) */movl �x, 8(%si)/* the segment of buffer address */movw $STAGE1_BUFFERSEG, 6(%si)xorl �x, �x movw %ax, 4(%si)movl �x, 12(%si)/* * BIOS call "INT 0x13 Function 0x42" to read sectors from disk into memory * Call with %ah = 0x42 * %dl = drive number * %ds:%si = segment:offset of disk address packet * Return: * %al = 0x0 on success; err code on failure */movb $0x42, %ah int $0x13 /* LBA read is not supported, so fallback to CHS. */jc chs_mode movw $STAGE1_BUFFERSEG, %bx jmp copy_buffer然后将标号disk_address_packet处的地址赋给si,再接着将[si-1]内存处置1(也就是mode被置1,表示LBA扩展读;如果是0,就是CHS寻址读).
movl ABS(stage2_sector), �x,把要加载或拷贝的扇区数传给ebx寄存器。
或者如下图所示:
这个stage2_sector在二进制文件中的偏移量是0x44
我们si[8]存储的long类型是起始扇区的LBA号码,从1号扇区也就是0柱面,0磁道,2扇区。,si·[2]记录着要传输多少个扇区,值为1,只传输一个数据块,读取后,将扇区的内容存储到si偏移量为04h 05h 6h、7h确定的内存区域0x7000:0x0000上了。这是int 13h(42)的作用。
最后一段代码是:
copy_buffer: movw ABS(stage2_segment), %es /* * We need to save %cx and %si because the startup code in * stage2 uses them without initializing them. */pusha pushw %ds movw $0x100, %cx //循环0x100次,即256次 movw %bx, %ds xorw %si, %si xorw %di, %di cld rep movsw //每次拷贝2字节,一个word popw %ds popa /* boot stage2 */jmp *(stage2_address)这段代码的含义是将刚才搬到0x7000:000的512字节,再次搬到0x8000:0000
OK,我们很痛苦的跟踪了stage1.S的代码,最后得到的结论是: stage1.S这汇编出来的512个字节代码的作用是将0柱面,0磁道,2扇区的512字节copy到0x8000处。
很失望吧,费了半天劲,最后只得到这么一点点结论。人生就是如此,付出不一定有回报,对于我们而言,只管努力,莫问前程,才能活得心平气和。
我们读到的512字节是干嘛的呢?啥时候才能看到GRUB的选择OS的界面呢?江湖传说的stage2到底是怎么回事,江湖传说的stage1.5是怎么回事,且看下一篇文章。
来源:心平氣和