转载注明出处(cppgp: http://blog.youkuaiyun.com/cppgp )
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的实现。
3.0 对boot.s而言,diskboot.s进入内核默认是在1扇区,而grubkernel默认是在“2扇区开始”,但“2扇区开始”,这个信息不是直接指定的,而是通过blocklist这样的结构编译生成时写入的。读取blocklist中的元素,就可以知道int中断去哪里把kernel读到8200处
3.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。
3.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
数据集:blocklist,表示一串连续的扇区;blocklist是一个链表
可知,grub内核的起始扇区是2扇区(note:一般情况下,mbr【boot.s】在0扇区,kerneladdress【diskboot.s】在第1扇区,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中的注释,此处不再重复。
转载注明出处(cppgp: http://blog.youkuaiyun.com/cppgp )
diskboot.S位于grub-1.98/boot/i386/pc/目录,采用AT&T汇编语法编写。详细注释如下:
struct BlockListNode{
LONGLONG StartSectorNumber;
WORD Length;//有多少个连续扇区属于这个结点
WORD TargetCs;//本node将被读到的内存cs位置
}
- /*
- * 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
- *
- * cppgp@qq.com,
- * yanyg02@163.com
- *
- * 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
- /*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,因为起始扇区号是8字节,所以用两个寄存器ecx:ebx */
- movl (%di), %ebx
- movl 4(%di), %ecx
- /* the maximum is limited to 0x7f because of Phoenix EDD */
- /* Phoenix EDD限制单次读不能超过0x7F扇区,如果超过7f个扇区,则只读7f个,剩下的下次再读 */
- 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来判断有否加载完毕,len-此次要读的扇区个数 */
- 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读注释
- */
- /* 以下是设置中断DAP参数,同boot中一样,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!!! */