http://blog.chinaunix.net/uid-24774106-id-3497929.html
引言
玩Linux的人,肯定会听说过Grub这个神奇的东西,就是开机启动时候下拉一个菜单让我们选操作系统的那个东东。自己比较懒,一直没深入琢磨这个Grub的工作原理流程。最近工作遇到了Grub相关的问题,就花了一些时间学习了一下Grub。
闲言少叙。我们首先看下Linux的启动过程流程图:
这个流程图是大牛M. Tim Jones在Inside Linux boot process中绘制的。清楚的show出了我们从摁开机键开始,计算机所做的事情。今天我要分享的是是stage1 bootloader部分,以及stage2 bootloader的部分。
需知,刚刚进入stage1 bootloader的时候,操作系统还没有开始运行(废话,GRBU他老人家就是召唤操作系统的),也没有文件系统的概念,认为直接运行操作系统可执行文件的,就可以洗洗睡了,GRUB他老人家面临的是一穷二白的局面啊。
下面我开始学习GRUB。当然了 ,现在主流的桌面都已经使用GRUB 2,GRUB 0.9x系统的目前统称为GRUB legacy,只修复bug,不再开发新的feature了。但是很多公司的服务器上跑的还是GRUB。比如我们产品用的还是GRUB。下面我做的实验都是在基于2.6.24内核的操作系统上做的,和家用的Ubuntu操作系统不同。
MBR和stage1
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严格的说有三部分组成
- [0,0x1be)前446字节是Bootstrap code area,是一段程序
- ·[0x1be,0x1fe),64个字节,是分区表信息。我前面的博文有重点分析。
- [0x1fe,0x1ff] ,签名信息,是这两个字节是55AA。
细心的筒子可以发现,/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部分是一样的,至于不一样的地方是
- [0x1b8, 0x1bb)这个部分叫做optional disk signature,Windows系的产品会用到这4个字节,对于Linux和grub是用不到这4个字节的。
2 [0x40 ,0x50]中有部分不同,我暂时不懂
结论是/boot/grub/stage1文件和主引导记录MBR的code部分是相同的。事实上这份代码是从grub源代码的stage1/stage1.S汇编出来的。stage1.S是grub的第一个文件,便以后编译后产生的代码,正好是512字节,不是正好,是必须,否则无法放入1个扇区。
这个MBR的信息是grub安装上去的,方法如下:
stage1 源码分析
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位置呢?我边访名医,终于找到了一篇相关的博文《为嘛BIOS将MBR读入0x7C00地址处(x86平台下)》,这兄弟对系统启动也颇有兴趣,有好多博文写的很优秀,我跟他学了很多。英文好的筒子可以直接看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.
- The boot sector was 512 bytes, and stack/data area for boot program needed more 512 bytes. So, 0x7C00, the last 1024B of 32KiB was chosen.
跑了半天题,我们继续。我们把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 1f movb %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 $0x13
notification_string是GRUB,这一段截至到MSG是显示GRUB到屏幕上,因为这个MSG是细节,我们按下不表。总是作用是屏幕显示GRUB。我们还记得,MBR内容里面有如下信息:
testb 这部分是探测drive是否是硬盘,如果不是硬盘是软盘,直接采用CHS_MODE,就不用费事判断了。我们知道,80h和81h是硬盘,所以探测对应bit位。如果我们的启动设备是硬盘,按么我们需要检测LBA是否支持。
通过 BIOS 调用 INT 0x13 来确定是否支持扩展,LBA 扩展功能分两个子集 , 如下 :
第一个子集提供了访问大硬盘所必须的功能 , 包括: **************************************************************** 1.检查扩展是否存在 : ah = 41h , bx = 0x55aa , dl = drive( 0×80 ~ 0xff ) 2.扩展读 : ah = 42h 3.扩展写 : ah = 43h 4.校验扇区 : ah = 44h 5.扩展定位 : ah = 47h 6.取得驱动器参数 : ah = 48h **************************************************************** 第二个子集提供了对软件控制驱动器锁定和弹出的支持 ,包括: **************************************************************** 1.检查扩展 : ah = 41h 2.锁定/解锁驱动器 : ah = 45h 3.弹出驱动器 : ah = 46h 4.取得驱动器参数 : ah = 48h 5.取得扩展驱动器改变状态: ah = 49h ****************************************************************
我们采用的是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表示Cylinders
H表示Heads
S表示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), %ecx /* 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), %ebx /* the size and the reserved byte */ movw $0x0010, (%si) /* the blocks */ movw $1, 2(%si) /* the absolute address (low 32 bits) */ movl %ebx, 8(%si) /* the segment of buffer address */ movw $STAGE1_BUFFERSEG, 6(%si) xorl %eax, %eax movw %ax, 4(%si) movl %eax, 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), %ebx,把要加载或拷贝的扇区数传给ebx寄存器。
由si及其偏移量指向的内存保存着磁盘参数块,如下: ****************************************************************** 偏移量 大小 位数 描述 00h BYTE 8 数据块的大小 (10h or 18h) 01h BYTE 8 保留,必须为0 02h WORD 16 传输数据块数,传输完成后保存传输的块数 04h DWORD 32 传输时的数据缓存地址 08h QWORD 64 起始绝对扇区号(即起始扇区的LBA号码) ******************************************************************
或者如下图所示:
movw $0x0010, (%si) 执行的结果是si[0] =10h, si[1]=00h movw $1, 2(%si) 执行的结果是si[2] =01h, si[3]=00h /* the segment of buffer address */ movw $STAGE1_BUFFERSEG, 6(%si) 执行的记过是si·[6]=00h si[7]=07h movl ABS(stage2_sector), %ebx movl %ebx, 8(%si) stage2_sector: .long 1
这个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是怎么回事,且听下回分解,我是累了,不写了。
我做了PDF文档格式,需要的筒子可以下载后,慢慢看
参考文献:
1 Stage1.s源代码分析 (这篇文章非常棒,很多内容都是受惠于这篇博文)
2 维基百科
3 GRUB 源代码分析 (很棒的一个文档)
4 The mysteries arround "0x7C00" in x86 architecture bios bootloader
5 Linux/Unix系统的引导过程(从加电到操作系统运行)