Yanyg - SAN Software Engineer

深入理解 GNU GRUB - 03 diskboot.S

目录

本文最初发布于CSDN:https://blog.csdn.net/cppgp/article/details/6408231

diskboot.S位于目录boot/i386/pc/,最终生成diskboot.img。这部分指令被加载到0x8000~0x81FF。 diskboot.img加载GRUB内核到0x8200开始的内存位置,并将系统控制权交给GRUB内核。用到的BIOS 例程和boot.S中相同。因此本章只描述如下内容:

  1. diskboot.S执行时的环境
  2. diskboot.S代码结构
  3. diskboot.S详细注释
  4. diskboot.S模拟实现

本章最后也模拟一个diskboot的实现。

1 diskboot.S执行时的环境

boot.img跳转到bootdisk.img之前,已经配置好堆栈和一些寄存器及参数值。因此diskboot假设如下条件已经是满足的:

  1. 堆栈。SS和SP已配置好,有可用的堆栈。
  2. 寄存器DL。DL中保存正确的引导驱动器。
  3. 寄存器SI。SI中保存DAP地址。
  4. 寄存器DS。设置有正确的数据段DS。

事实上,在跳转到diskboot.img之前,boot.img确实做好了配置。堆栈SS:SP=0x0000:0x2000; DL中包含正确的引导驱动器;SI指向DAP地址,因此-1(%si)确定磁盘读取模式(LBA为1,CHS为0),如果是CHS读取,磁盘CHS参数含有正确值;数据段寄存器DS=0。

2 diskboot.S代码结构

diskboot.S生成512字节机器码,其中0x0~0x147共328字节是指令,0x148~0x1FF共184字节用来保存数据集,每个数据集占用12字节,可以保存15个数据集。对于每个数据集,其中0~7字节表示起始扇区数,在安装时候指定;8~9字节表示扇区数,10~11字节表示目的段地址,都在生成镜像(grub-mkimage)时确定。必须存在一个扇区数为0的数据集表示数据集读取结束,因此可以存在14个有效数据集。当前只使用了一个。我反解了一个安装后的diskboot.img,显示只用了一个数据集,其值如下:

  • 起始扇区LBA地址: 0x0000 0000 0000 0002
  • 扇区数: 0x002e
  • 目的段地址: 0x0820

可知,grub内核的起始扇区是2扇区,扇区数46(0x2E==46),加载到0x8200起始内存处。这个grub内核的大小为46*512=23KiB。

diskboot.S压栈首先保存驱动器。输出提示信息”loading”,设置第一个数据集的地址,然后进入循环读取数据。根据boot.img中设置的读取模式(LBA/CHS)和数据集中的起始扇区、扇区数、目的段地址,读取数据并保存到内存中(偏移量为0,因此目的段地址唯一确定内存地址)。每调用一次读中断,都提示一个”.”到终端,对于慢速存储设备,用户可以快速得到反馈,以免在加载数据期间,用户误以为死机而进行强制性关机措施等。在一个数据集内循环读取时,循环标签是LOCAL(setup_sectors),一个数据集完成后,设置处理上一数据集,并跳转到LOCAL(bootloop)开始处理,如果这个数据集的扇区数为0,则跳转到LOCAL(bootit),否则继续数据集内的循环LOCAL(setup_sectors)。

数据读取完毕后,执行LOCAL(bootit)处代码,这里将输出提示信息”\r\n”,还原DX寄存器(保存引导驱动器),并以CS:IP=0x0000:0x8200跳转执行,这里正是kernel.img所在内存地址。 BIOS读中断过程(LBA/CHS)参考boot.S中的注释,此处不再重复。

3 diskboot.S详细注释

CSDN:https://blog.csdn.net/cppgp/article/details/6408235

/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 1999,2000,2001,2002,2006,2007,2009,2010   Free Software Foundation, Inc.
 *
 *  GRUB is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  GRUB is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with GRUB.  If not, see <http://www.gnu.org/licenses/>.
 */
/*
 * cppgp 注释
 *
 * 转载请注明原作者
 *
 * 日期: 2011-04-22
 *
 * Email
 *    [email protected],
 *    [email protected]
 *
 * GRUB version:
 *    gnu grub-1.98
 */
 /*
 * diskboot.S
 *
 * diskboot.S生成diskboot.img, 共512字节
 *
 * 安装程序根据实际情况, 改写blocklist数据集. 最多支持
 * 15个数据集, 每个数据集占用12字节, 0~7字节表示起始
 * 扇区, 8~9字节表示该数据集扇区数, 10~11表示目的
 * 缓冲区起始段地址. 必须有一个数据集的扇区数字段
 * 为0, 程序根据此判断读取结束.
 *
 *
 * 安装在硬盘上时,DPT部分(0x1BE~0x1FD)保留不变,
 *
 * 安装在软盘上时, DPT部分(0x1BE~0x1FD)是软盘复位和
 * 扇区探测代码. 扇区末尾两字节
 *
 * 扇区最后两字节 写入0xAA55 (小端表示, 0x1FE位置为0x55,
 * 0x1FF位置为0xAA).
 *
 * diskboot.img开机时加载到0x8000~0x81FF, 并以CS:IP=0x0000:0x8000
 * 跳转执行, 它将加载kernel.img到内存0x8200开始位置,并以
 * CS:IP=0x0000:0x8200跳转执行. kernel.img扇区数可能有多个,
 * 在生成影像(grub-mkinage)时确定.
 *
 * diskboot.img可以使用boot.img设置的堆栈和寄存器值,但是在
 * 跳转到0x8200之前,要确保没有更改这些设置, 因为kernel.img
 * 还会用到这些寄存器.
 *
 * diskboot.img当前只用到一个数据集. 在我的机器上, 反解
 * 安装以后的diskboot.img, 得到的值如下:
 *
 *  起始扇区: 0x0000 0000 0000 0002
 *  扇区数      : 0x002e
 *  段地址      : 0x0820
 */
#include <grub/symbol.h>
#include <grub/machine/boot.h>
/*
 *  defines for the code go here
 */
#define MSG(x)  movw $x, %si; call LOCAL(message)
        .file   "diskboot.S"
        .text
        /* Tell GAS to generate 16-bit instructions so that this code works
           in real mode. */
        /*
         * 告诉汇编器产生16位代码
         */
        .code16
        /*
         * 如同在boot.S所指明的,
         * 此处的代码被加载在内存0x8000处
         * 并且以CS:IP=0x0000:0x8000跳转执行
         */
        .globl  start, _start
start:
_start:
        /*
         * _start is loaded at 0x2000 and is jumped to with
         * CS:IP 0:0x2000 in kernel.
         */
        /*
         * we continue to use the stack for boot.img and assume that
         * some registers are set to correct values. See boot.S
         * for more information.
         */
        /*
         * boot.img中已设置堆栈(SS:SP=0x0000:0x2000)
         * 并且假定一些寄存器被设置为正确值
         * 其中包括:
         *  DL: 引导驱动器
         *  SI: DAP地址(BPB数据块,
         *       -1(%si)可获取读取模式LBA/CHS)
         *       (%si)LBA读的DAP参数起始地址
         *       或CHS参数存放地址
         *  DS=SS=0: 数据段/堆栈段地址
         */
        /* save drive reference first thing! */
        /* 驱动器压栈 */
        pushw   %dx
        /* print a notification message on the screen */
        /*
         * 向终端输出提示信息"loading"
         * notification_string="loading"
         */
        pushw   %si
        MSG(notification_string)
        popw    %si
        /* this sets up for the first run through "bootloop" */
        /*
         * GRUB_BOOT_MACHINE_LIST_SIZE=12
         * (fitstlist-12) ~ firstlist共12字节空间, 指定GRUB内核起始扇区,扇区数,拷贝时的代码段
         * 起始扇区8字节, 扇区数2字节,代码段2字节
         * 代码中的标签分别是:blocklist_default_start, blocklist_default_len, blocklist_default_seg
         *  blocklist_default_start默认值2,在安装时候指定(grub-install), 决定GRUB内核起始扇区的LBA地址
         *  blocklist_default_len默认值0, 在制作引导镜像时候生成 (grub-mkimage), 设定为正确的GRUB内核长度
         *  blocklist_default_seg跳转到GRUB内核时的默认代码段值, 默认值CS=0x820,
         *     它仅仅在LOCAL(copy_buffer)中用到, 最终跳转时, 使用CS:IP=0x0000:0x8200 (blocklist_default_seg<<4)
         */
        movw    $(firstlist - GRUB_BOOT_MACHINE_LIST_SIZE), %di
        /* save the sector number of the second sector in %ebp */
        /*
         * 8字节起始扇区的低地址(%si)保存到ESP寄存器
         *   LBA寻址用到高4字节.
         *   CHS寻址,如果高4字节不为0生成错误,因为CHS寻址不可达(CHS寻址范围7.88GiB, 参考2.1节)
         */
        movl    (%di), %ebp
        /* this is the loop for reading the rest of the kernel in */
/* 加载所有数据集, 当前只使用一个数据集*/
LOCAL(bootloop):
        /* check the number of sectors to read */
        /*
         * 8(%di)确定GRUB内核扇区数
         * grub-mkimage生成镜像时改写为正确的内核长度
         * 每次循环加载减去已读取的扇区
         * 为0时表示已经加载完毕, 跳转到LOCAL(bootit)
         */
        cmpw    $0, 8(%di)
        /* if zero, go to the start function */
        /* 所有数据集加载完毕跳转到LOCAL(bootit) */
        je      LOCAL(bootit)
/* LOCAL(setup_sectors)只加载当前数据集 */
LOCAL(setup_sectors):
        /* check if we use LBA or CHS */
        /*
         * boot.img中已经设置读取模式: 1表示LBA, 0表示CHS
         */
        cmpb    $0, -1(%si)
        /* use CHS if zero, LBA otherwise */
        /* CHS模式读 */
        je      LOCAL(chs_mode)
        /* load logical sector start */
        movl    (%di), %ebx
        movl    4(%di), %ecx
        /* the maximum is limited to 0x7f because of Phoenix EDD */
        /* Phoenix EDD限制单次读不能超过0x7F扇区 */
        xorl    %eax, %eax
        movb    $0x7f, %al
        /* how many do we really want to read? */
        cmpw    %ax, 8(%di)     /* compare against total number of sectors */
        /* which is greater? */
        jg      1f
        /* if less than, set to total */
        /* 如果到达这里,表示剩余扇区不足0x7F扇区,这次就可以加载完毕了 */
        movw    8(%di), %ax
1:
        /* subtract from total */
        /* 减去这次要读取的扇区数, 代码用8(%di) 是否为0来判断有否加载完毕 */
        subw    %ax, 8(%di)
        /* add into logical sector start */
        /*
         * 起始扇区要加上这次将要读取的扇区数
         * 注意是8字节的起始扇区
         * 高4字节的加要注意进位
         */
        addl    %eax, (%di)
        adcl    $0, 4(%di)
        /* set up disk address packet */
        /*
         * 设置LBA读参数并调用BIOS中断
         * 参考boot.S LBA读注释
         */
        /* the size and the reserved byte */
        movw    $0x0010, (%si)
        /* the number of sectors */
        movw    %ax, 2(%si)
        /* the absolute address */
        movl    %ebx, 8(%si)
        movl    %ecx, 12(%si)
        /* the segment of buffer address */
        movw    $GRUB_BOOT_MACHINE_BUFFER_SEG, 6(%si)
        /* save %ax from destruction! */
        /* 保存这次要读取的扇区数 */
        pushw   %ax
        /* the offset of buffer address */
        movw    $0, 4(%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
        jc      LOCAL(read_error)
        movw    $GRUB_BOOT_MACHINE_BUFFER_SEG, %bx
        jmp     LOCAL(copy_buffer)
LOCAL(chs_mode):
        /*
         * CHS读
         * 判断C/H/S参数是否越界
         * 调用CHS读的BIOS中断
         * 参考boot.S CHS读注释
         */
        /* load logical sector start (top half) */
        movl    4(%di), %eax
        orl     %eax, %eax
        jnz     LOCAL(geometry_error)
        /* load logical sector start (bottom half) */
        movl    (%di), %eax
        /* zero %edx */
        xorl    %edx, %edx
        /* divide by number of sectors */
        divl    (%si)
        /* save sector start */
        movb    %dl, 10(%si)
        xorl    %edx, %edx      /* zero %edx */
        divl    4(%si)          /* divide by number of heads */
        /* save head start */
        movb    %dl, 11(%si)
        /* save cylinder start */
        movw    %ax, 12(%si)
        /* do we need too many cylinders? */
        cmpw    8(%si), %ax
        jge     LOCAL(geometry_error)
        /* determine the maximum sector length of this read */
        movw    (%si), %ax      /* get number of sectors per track/head */
        /* subtract sector start */
        subb    10(%si), %al
        /* how many do we really want to read? */
        cmpw    %ax, 8(%di)     /* compare against total number of sectors */
        /* which is greater? */
        jg      2f
        /* if less than, set to total */
        movw    8(%di), %ax
2:
        /* subtract from total */
        subw    %ax, 8(%di)
        /* add into logical sector start */
        addl    %eax, (%di)
        adcl    $0, 4(%di)
/*
 *  This is the loop for taking care of BIOS geometry translation (ugh!)
 */
        /* get high bits of cylinder */
        movb    13(%si), %dl
        shlb    $6, %dl         /* shift left by 6 bits */
        movb    10(%si), %cl    /* get sector */
        incb    %cl             /* normalize sector (sectors go
                                        from 1-N, not 0-(N-1) ) */
        orb     %dl, %cl        /* composite together */
        movb    12(%si), %ch    /* sector+hcyl in cl, cylinder in ch */
        /* restore %dx */
        popw    %dx
        pushw   %dx
        /* head number */
        movb    11(%si), %dh
        pushw   %ax     /* save %ax from destruction! */
/*
 * BIOS call "INT 0x13 Function 0x2" to read sectors from disk into memory
 *      Call with       %ah = 0x2
 *                      %al = number of sectors
 *                      %ch = cylinder
 *                      %cl = sector (bits 6-7 are high bits of "cylinder")
 *                      %dh = head
 *                      %dl = drive (0x80 for hard disk, 0x0 for floppy disk)
 *                      %es:%bx = segment:offset of buffer
 *      Return:
 *                      %al = 0x0 on success; err code on failure
 */
        movw    $GRUB_BOOT_MACHINE_BUFFER_SEG, %bx
        movw    %bx, %es        /* load %es segment with disk buffer */
        xorw    %bx, %bx        /* %bx = 0, put it at 0 in the segment */
        movb    $0x2, %ah       /* function 2 */
        int     $0x13
        jc      LOCAL(read_error)
        /* save source segment */
        movw    %es, %bx
LOCAL(copy_buffer):
        /*
         * 无论CHS/LBA, 缓冲区地址为segment:offset=%bx:0x0=0x7000:0x0处
         */
        /* load addresses for copy from disk buffer to destination */
        /* 设置目的段 */
        movw    10(%di), %es    /* load destination segment */
        /* restore %ax */
        /*
         * 调用BIOS中断前已经把这次要读取的扇区数压栈
         * 每个扇区512字节即2^9
         *   然后%ax左移5位并加到段地址上,
         *   段地址要左移4位放到地址线上,
         *   因此,等价于%ax左移9位放到地址线上
         *   这正好是一个扇区数据占用的内存空间
         */
        popw    %ax
        /* determine the next possible destination address (presuming
                512 byte sectors!) */
        /*
         * 下一次可能的目的段地址(强制要求512字节扇区)
         * 本次使用的目的段在此之前已经保存到%es了
         */
        shlw    $5, %ax         /* shift %ax five bits to the left */
        addw    %ax, 10(%di)    /* add the corrected value to the destination
                                   address for next time */
        /* save addressing regs */
        pusha
        pushw   %ds
        /* get the copy length */
        /*
         * %ax左移3位, 共左移8位
         * 因此%ax中是这次读取的字数(word, 1-word=2-byte)
         * 设置%cx为需要拷贝的字数,rep使用%cx作为循环计数器
         */
        shlw    $3, %ax
        movw    %ax, %cx
        /*
         * 以扇区为单位,源/目的串偏移量都是0
         */
        xorw    %di, %di        /* zero offset of destination addresses */
        xorw    %si, %si        /* zero offset of source addresses */
        /* 进入LOCAL(copy_buffer)之前, %bx持有缓冲区段地址 */
        movw    %bx, %ds        /* restore the source segment */
        /* 字符串操作指针移动方向为前向 */
        cld             /* sets the copy direction to forward */
        /* perform copy */
        /* 完成拷贝 */
        rep             /* sets a repeat */
        movsw           /* this runs the actual copy */
        /* restore addressing regs and print a dot with correct DS
           (MSG modifies SI, which is saved, and unused AX and BX) */
        /* MSG不用到DS,
        /*
         * 设置正确的数据段地址%ds
         * 调用MSG显示"."
         * 对于慢速存储设备,
         * 提供用户反馈, 以免被当做死机
         */
        popw    %ds
        MSG(notification_step)
        popa
        /* check if finished with this dataset */
        /* 检测是否读取完毕, 8(%di)持有需要读取的剩余扇区数 */
        cmpw    $0, 8(%di)
        jne     LOCAL(setup_sectors)
        /* update position to load from */
        /*
         * 更新为上一个数据集
         * 意指(firstlist-(x+1)*12)~(firstlist-x*12)位置, x从0开始递增
         * 可以有多个数据集
         * 但是不能和LOCAL(message)重叠
         * 多个数据集数据最终是否连续, 依赖于该数据集合上一个数据集
         * 缓冲区段地址
         *
         * 否则diskboot会出错
         * 反汇编显示LOCAL(message)结束处地址0x8147
         * 因此0x148~0x1FF可全部用作数据集
         * 每个数据集12字节,最多可有(0x200-0x148)/12=15个数据集
         * 最开始处数据集扇区数必须为0 (否则重叠到LOCAL(message)去啦)
         * ==> 最多可有14个有效数据集
         * 反汇编安装后的diskboot.img的结果, 显示只用了一个数据集
         *
         * 推导:
         *   每个数据集采用2字节描述扇区数,因此不能超过0xFFFF个扇区
         *   14个有效数据集位置, 共计扇区数0xFFFF*14
         *   共计字节数= 0xFFFF*14*512=469754880bytes=447MB远超实模式下的寻址空间了
         */
        subw    $GRUB_BOOT_MACHINE_LIST_SIZE, %di
        /* jump to bootloop */
        /* 跳转到bootloop处理下一数据集 */
        jmp     LOCAL(bootloop)
/* END OF MAIN LOOP */
/*
 * 所有数据读取完毕
 * 单个数据集内的数据总是连续放置在内存中
 * 当前只使用一个数据集,放置在0x8200起始的内存中
 * LOCAL(bootit)打印提示信息并跳转执行
 * GRUB_BOOT_MACHINE_KERNEL_ADDR=0x8000
 * CS:IP=0x0000:0x8200
 * startup.S生成的指令开始执行
 */
LOCAL(bootit):
        /* print a newline */
        MSG(notification_done)
        popw    %dx     /* this makes sure %dl is our "boot" drive */
        ljmp    $0, $(GRUB_BOOT_MACHINE_KERNEL_ADDR + 0x200)
/*
 * 错误提示字符串和输出函数
 * 参考boot.S注释
 */
/*
 * BIOS Geometry translation error (past the end of the disk geometry!).
 */
LOCAL(geometry_error):
        MSG(geometry_error_string)
        jmp     LOCAL(general_error)
/*
 * Read error on the disk.
 */
LOCAL(read_error):
        MSG(read_error_string)
LOCAL(general_error):
        MSG(general_error_string)
/* go here when you need to stop the machine hard after an error condition */
LOCAL(stop):    jmp     LOCAL(stop)
notification_string:    .asciz "loading"
notification_step:      .asciz "."
notification_done:      .asciz "/r/n"
geometry_error_string:  .asciz "Geom"
read_error_string:      .asciz "Read"
general_error_string:   .asciz " Error"
/*
 * message: write the string pointed to by %si
 *
 *   WARNING: trashes %si, %ax, and %bx
 */
        /*
         * Use BIOS "int 10H Function 0Eh" to write character in teletype mode
         *      %ah = 0xe       %al = character
         *      %bh = page      %bl = foreground color (graphics modes)
         */
1:
        movw    $0x0001, %bx
        movb    $0xe, %ah
        int     $0x10           /* display a byte */
        incw    %si
LOCAL(message):
        movb    (%si), %al
        cmpb    $0, %al
        jne     1b      /* if not end of string, jmp to display */
        ret
/*
 * 反汇编结果显示, 此处地址是0x8148
 * 可在此处添加. = _start + 0x148 测试
 *
 * 0x148~0x1FF共计184字节,可存放15个12字节的数据集
 * 按照从下到上的顺序存放数据集
 * 最后一个有效数据集的扇区数(数据集内偏移量8)必须为0
 */

/*
 *  This area is an empty space between the main body of code below which
 *  grows up (fixed after compilation, but between releases it may change
 *  in size easily), and the lists of sectors to read, which grows down
 *  from a fixed top location.
 */
        .word 0
        .word 0
        . = _start + 0x200 - GRUB_BOOT_MACHINE_LIST_SIZE
        /* fill the first data listing with the default */
blocklist_default_start:
        /* this is the sector start parameter, in logical sectors from
           the start of the disk, sector 0 */
        .long 2, 0
blocklist_default_len:
        /* this is the number of sectors to read.  grub-mkimage
           will fill this up */
        .word 0
blocklist_default_seg:
        /* this is the segment of the starting address to load the data into */
        .word (GRUB_BOOT_MACHINE_KERNEL_SEG + 0x20)
firstlist:      /* this label has to be after the list data!!! */