深入理解 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中相同。因此本章只描述如下内容:
- diskboot.S执行时的环境
- diskboot.S代码结构
- diskboot.S详细注释
- diskboot.S模拟实现
本章最后也模拟一个diskboot的实现。
1 diskboot.S执行时的环境
boot.img跳转到bootdisk.img之前,已经配置好堆栈和一些寄存器及参数值。因此diskboot假设如下条件已经是满足的:
- 堆栈。SS和SP已配置好,有可用的堆栈。
- 寄存器DL。DL中保存正确的引导驱动器。
- 寄存器SI。SI中保存DAP地址。
- 寄存器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!!! */