GRUB启动分析之stage1

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严格的说有三部分组成

  1. [0,0x1be)前446字节是Bootstrap code area,是一段程序
  2. ·[0x1be,0x1fe),64个字节,是分区表信息。我前面的博文有重点分析。
  3. [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部分是一样的,至于不一样的地方是

  1. [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:

  1. They wanted to leave as much room as possible for the OS to load itself within the 32KiB.
  2. 8086/8088 used 0x0 - 0x3FF for interrupts vector, and BIOS data area was after it.
  3. 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模式。存在下面集中情况:

  1. 启动设备不是0x80,0x81,压根不探查,直接采用CHS 模式
  2. 探查结果 CF=1,二话不说,跳转到CHS模式
  3. 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文档格式,需要的筒子可以下载后,慢慢看

grub 启动分析之stage1.pdf



参考文献:

Stage1.s源代码分析 (这篇文章非常棒,很多内容都是受惠于这篇博文)

2 维基百科

3 GRUB 源代码分析 (很棒的一个文档)

The mysteries arround "0x7C00" in x86 architecture bios bootloader

5  Linux/Unix系统的引导过程(从加电到操作系统运行)


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值