Yanyg - Software Engineer

深入理解 GNU GRUB - 02 boot.S 2.1 相关BIOS例程

目录

1 boot.S: GRUB引导第一步

boot.S位于目录boot/i386/pc/。这部分指令被加载到0x7C00~0x7DFF。主要工作包括:配置寄存器;设置堆栈;检测引导盘;检测引导盘读取模式;读取另一扇区指令。这个过程用到几个BIOS例程,并且对主引导记录(MBR, Master Boot Record)结构有很大的依赖。因此boot.S我们分作四步进行详细描述:

  1. 相关BIOS例程
  2. 主引导记录MBR结构
  3. boot.S代码结构
  4. boot.S详细注释

在本节最后,我们实现一个简单的boot.S,这个boot.S仅仅通过BIOS例程,向终端输出文本。

1.1 相关BIOS例程

boot.S用到五个磁盘相关的BIOS例程。分别是:探测磁盘扩展读支持、LBA方式读、CHS参数获取、CHS方式读、磁盘复位。磁盘读取出现LBA/CHS两种模式(其实LBA又分为28-bit、48-bit、64-bit三种),是因为在磁盘的发展历史上,由于对磁盘容量的错误估量,导致容量屏障的出现,而不得不进行接口更改、扩展。历史上出现的容量屏障主要有四次:

  1. 504MiB限制。也称作物理CHS寻址限制。早期的IDE/ATA磁盘接口规定柱面(C=Cylinder)、磁头(H=Head)、扇区(S=Sector)的位宽分别为C/H/S=16/4/8;而BIOS INT 13 H/02H读调用接口规定C/H/S=10/8/6。两者结合取位宽较少者,磁盘寻址参数C/H/S=10/4/6。扇区的起始编号是1而不是0,每扇区数据512字节,因此可寻址容量为210*24*(26-1)*512=528482304bytes=504MiB。
  2. 7.88GiB限制。也称作逻辑CHS寻址限制。从1) 可以看出,BIOS INT 13 H/02H接口磁头H有4位是空闲的,因此逻辑上可以扩展磁头H为8位(而实际上连接到IDE/ATA时,磁头H有4位映射到柱面C,或者映射到柱面C和扇区S)。在BIOS接口上看,现在C/H/S寻址范围扩展到210*28*(26-1)*512=8455716864bytes=7.88GiB。
  3. 128GiB限制。也称作LBA-28bits限制。在 2) 中,BISO接口的CHS寻址已经到了极限,无法再扩展了,而IDE/ATA是28 位(IDE/ATA内部C/H/S=16:4:8共28位)的,理论寻址容量为228*512=128GB,因此出现了LBA (Logical Block Addressing) 寻址。LBA是一个一维地址,从0~2N-1,其中N是地址宽度,在这里是28,而LBA到CHS的转换由BIOS和磁盘完成。28位LBA寻址容量为228*512=128GiB。
  4. 2TiB限制。为了提供更大范围的寻址,Western Digital和Phoenix Technologies制定了EDD (BIOS Enhanced Disk Drive Services) 标准。它使用64位LBA寻址,同时也支持48位和28位寻址。48位LBA寻址容量为128PiB,而64位LBA寻址容量更是高达8ZiB,无论48位LBA或者64位LBA,在当前或可以预见的将来应该是足够的。但是历史悠久的MBR中保存有磁盘分区表DPT,而分区表中分区绝对起始扇区和分区总扇区数都是32位的,因此对于传统分区的磁盘,最大寻址范围由这两个32位值决定,大小为232*512=2TiB。为了解决该问题,引进了GPT (GUID Partition Table) 和EFI (Extensible Firmware Interface) 技术,本文不对其做详细描述,有兴趣的读者可以Google相关主题。

在磁盘容量的计算上,软件按照1024进行单位换算,而厂商按照1000进行单位换算,因此上述的504MiB限制、7.85GiB限制、128GiB限制又称为522MB限制、8.46GB限制、137GB限制。除了上述限制,历史上还出现过一些软件导致的容量屏障,例如一些BIOS BUG或文件系统体系导致的容量屏障,本文 不再对其做进一步描述,有兴趣可以Google相关细节。BIOS例程及容量屏障描述参考如下网络资源: http://en.wikipedia.org/wiki/INT_13H http://en.wikipedia.org/wiki/INT_10H http://www.pcguide.com/ref/hdd/bios/index.htm

1.1.1 磁盘扩展探测: INT 13H, AH=41H

检测磁盘扩展读(LBA/CHS)支持情况。详细描述如下。参数:寄存器 描述 AH=0x41 扩展检测函数序号 DL 驱动器编号(第一块硬盘为0x80,第二块为0x81,依次类推) BX 0x55AA

结果:寄存器 描述 CF 支持清零,不支持置1 AH 错误码或者主版本号 BX 0x55AA CX 接口支持掩码 1 – 使用打包结构体存取设备 2 – 驱动器加锁和弹出 4 – 支持增强型磁盘驱动器(EDD)

使用AT&T语法实现这个调用的例子如下(默认使用0x80作为驱动器):

boot_driver:
.byte 0x80
movb boot_driver, %dl
movb $0x41, %ah
movw $0x55aa, %bx
int $0x13
movb boot_driver, %dl
jc chs_mode
cmpw $0xaa55, %bx
jne chs_mode
andw $1, %cx
lba_mode:
// Do LBA operation
Jmp end
chs_mode:
// Do CHS operation
end:
// Do Normal Works

调用结束后,如果CF置1、或者BX不等于0xAA55、或者CX不等于1,都表示不支持LBA,因此进入CHS处理。注意,BIOS调用可能更改掉DL寄存器值,因此需要重置。

1.1.2 LBA模式读: INT 13H, AH=42H

LBA模式的读采用打包的数据结构作为参数。参数:寄存器 描述 AH=42H 扩展读函数序号 DL 驱动器编号(第一块硬盘为0x80,第二块为0x81,依次类推) DS:SI segment:offset指针,指向磁盘地址包DAP (Disk Address Packet)

DAP结构体的格式描述如下:偏移量 大小 描述 00H 1 Byte DAP大小=16=0x10 01H 1 Byte 未用,必须置0 02H~03H 2 Bytes 需要读的扇区数(有些BIOS限制不能超过127扇区) 04H~07H 4 Bytes segment:offset指针,指向内存缓冲区,读取到的扇区内容放置在该缓冲区 08H~0FH 8 Bytes 需要读的连续扇区的起始扇区编号(第一个扇区的编号是0)

结果:寄存器 描述 CF 失败置1,成功清零 AH 返回码

使用AT&T语法实现这个调用的例子如下(假定我们使用0x80作为驱动器,从第2个扇区开始连续读取4个扇区):

boot_driver:
.byte 0x80
sector_start:
.long 2, 0
sector_num:
.word 4
dap:
/* reserved 16 bytes to hold dap, or disk address packet */
. = dap + 0x10
read_buffer:
/* reserved 512*4bytes to save data */
. = read_buffer + 512*4
movb boot_driver, %dl
movw $dap, %si
xorw %ax, %ax
movw %ax, %ds
movw %ax, 4(%si)
movb $0x0010, (%si)
movw sector_num, %ax
movw %ax, 2(%si)
movw $read_buffer, 6(%si)
movl sector_start, %eax
movl %eax, 8(%si)
movl sector_start+4, %eax
movl %eax, 12(%si)
movb $0x42, %ah
int $0x13
jc lba_fail
// Do LBA-read success operations
Jmp end
lba_fail:
// Do LBA-read fail operations
end:
// Do Normal Works

调用结束后,通过检测CF来判断是否成功。需要注意字节序的问题,80x86采用小端序。

1.1.3 CHS参数获取: INT 13H, AH=08H

获取驱动器CHS参数。如下。参数:寄存器 描述 AH=0x08 驱动器CHS参数读取函数序号 DL 驱动器编号(第一块硬盘为0x80,第二块为0x81,依次类推) BX 0x55AA

结果:寄存器 描述 CF 成功清零,失败置1 AH 返回码 DL 驱动器号 DH 逻辑磁头最大索引值(number_of-1因为索引从0开始) CX 逻辑柱面索引最大值和逻辑扇区数逻辑柱面最大索引=number_of-,因为索引从0开始逻辑扇区数=number_of,因为索引从1开始柱面占10位,CH表示柱面低8位,CL高2位表示柱面9~10位扇区占6位,CL低6位表示扇区

使用AT&T语法实现这个调用的例子很简单:

boot_driver:
.byte 0x80
movb boot_driver, %dl
movb $0x08, %ah
int $0x13
jc chs_para_fail
// Do CHS-para success operations
jmp end
chs_para_fail:
// Do CHS-para fail operation
end:
// Do Normal Works

调用结束后,通过检测CF来判断参数获取调用是否成功。

1.1.4 CHS模式读: INT 13H, AH=02H

CHS模式读扇区。需要注意缓冲区没有超出该段寻址范围。参数:寄存器 描述 AH=0x02 CHS模式读扇区函数序号 AL 需读取扇区数 CX 柱面和扇区,柱面占10位,扇区占6位 CH表示柱面低8位,CL高2位表示柱面9~10位 CL低6位表示扇区(从1~63) DH 磁头 DL 驱动器编号第一软盘为0x00,第二软盘为0x01,依次类推第一硬盘为0x80,第二硬盘为0x81,依次类推 ES:BX segment:offset缓冲区地址指针

结果:寄存器 描述 CF 成功清零,失败置1 AH 返回码 AL 实际读取的扇区数

CHS读存在7.88GiB容量限制,现在几乎没有硬盘使用这种模式读,并且也很少有人使用软盘(你还有软驱吗?),因此CHS读扇区只是作为残留机制存在,很少用到。使用AT&T语法实现这个调用的例子如下,因为要用到移位等,汇编代码看起来稍微有点麻烦。这里我们依然假定使用0x80作为驱动器,以C/H/S=0/0/1开始连续读取4个扇区。另外,需要用到2.1.3描述的CHS参数获取的结果。

boot_driver:
.byte 0x80
cylinder_start:
.word 0
head_start:
.byte 0
sector_start:
.byte 1
sector_num:
.word 4
chs_sectors:
.byte 0
chs_heads:
.word 0
chs_cylinders:
.word 0
read_buffer:
/* reserved 512*4bytes to save data */
. = read_buffer + 512*4
movb boot_driver, %dl
movb $0x08, %ah
int $0x13
jc chs_para_fail
/* saved the CHS parameters */
/*
  * dh : numbers_of_heads-1, 8-bits
  * cx : numbers_of_cylinders-1, number_of_sectors
  */
movzbl %dh, %eax
incw  %ax
movw %ax, chs_head
movzbw %dl, %dx
shlw $2, %dx
movb %ch, %al
movb %dh, %ah
incw %ax
movw %ax, chs_cylinders
movzbw %dl, %ax
shrw $2, %ax
movw %ax, chs_sectors
/* CHS read */
movb sectors_start, %al
movb %al, %cl
movw cylinder_start, %ax
movb %al, %ch
xorb %al, %al
shrw $2, %ax
orb %al, %cl
movb head_start, %al
movb %al, %dh
movb boot_drive, %dl
xorw %ax, %ax
movw %ax, %es
movw $read_buffer, %bx
movb sector_num, %al
movb $2, %ah
int $0x13
jc chs_read_fail
// Do CHS-read success operations
jmp end
chs_para_fail:
// Do CHS-para fail operations
jmp end
chs_read_fail:
// Do CHS-read fail operations
jmp end
end:
// Do Normal Works

CHS读结束后,通过CF判断是否成功。

1.1.5 复位磁盘驱动器: INT 13H, AH=00H

复位磁盘驱动器。如下。参数:寄存器 描述 AH=0x00 驱动器复位函数序号 DL 驱动器编号(第一块硬盘为0x80,第二块为0x81,依次类推)

结果:寄存器 描述 CF 成功清零,失败置1 AH 状态

使用AT&T语法实现这个调用的例子非常简单:

boot_driver:
.byte 0x80
xorb %ah, %ah
movb boot_driver, %dl
int $0x13
jc reset_fail
// Do Disk Reset success operations
jmp end
reset_fail:
// Do Disk Reset fail operations
end:
// Do Normal Works

通过CF判断复位是否成功。软盘的读取和CHS模式的读

1.1.6 字符输出: INT 10H, AH=0EH

BIOS使用INT 10H图形服务,可以用来设置图形模式、输出字符或字符串、以及基本图形(图形模式下读取/写入像素)。 AH=0EH提供用来向终端输出单一字符。如下。参数:寄存器 描述 AH=0x0E 电传输出函数序号 AL=Character 需要输出的字符 BL=Color 输出字符颜色,只对图形终端有效

结果:无返回

使用AT&T语法实现这个调用的例子如下:

char_value:
.byte ‘A’
char_color:
.byte 0
movb char_value, %al
movb char_color, %bl
movb $0x0E, %ah
int $0x10

INT 10H, AH=0EH调用无返回值。