; Hello, I'm ictxiangxin !!!
; 是一名computer programmer。
; 最近发现很多对系统有特殊爱好的同学都有研究minix的冲动,可是由于很多原因,使得大部分人很难入门。
; 其中第一个疑问就是,minix从哪启动的?大部分肯定知道开机之后加载mbr的过程,这里就不介绍了。
; 可是mbr是干什么的,概念都知道,到底做了什么?这就是一大难点。
; 为了使更多同学入门minix,我将断断续续地出一些minix引导级的代码教程,以便都能很容易进入minix内核研究。
; 由于minix的历史原因,使得它的汇编代码还是用古老的cc编译器,这样的汇编代码看起来是非常古怪的。
; 所以,我将minix的汇编代码翻译成了nasm版的,以便更容易接受。
; 下面的代码就是minix的mbr2.0,作者是:Kees J. Bot
LOADOFF equ 0x7C00 ; 默认加载地址,次级引导也会加载到这。
BUFFER equ 0x0600 ; 这个地址被认为是空闲地址的开始,因为前面有0x400的中断向量和0x100的BIOS参数空间。
PART_TABLE equ 0x1be ; 分区列表的地址,因为BIOS的int 19h会将整个0扇区加载进0x7c00,所以分区列表也同时被加载了。
PENTRYSIZE equ 0x10 ; 每个分区项为16字节,总共分区列表是64字节。
MAGIC equ 0x1fe ; 这个就是传说中的魔数0Xaa55的地址,用来检验可引导性。
; <ibm/partition>.h:
bootind equ 0 ; 这是分区项中可引导性标志的偏移地址。
sysind equ 4 ; 分区类型的偏移地址。
lowsec equ 8 ; LBA的偏移地址。
; minix的mbr的工作很简单,到活动分区,然后加载进内存,最后跳转。
master:
xor ax, ax
mov ds, ax
mov es, ax
; 这里关中断是因为下面的堆栈初始化很重要,以防受到干扰。
cli
mov ss, ax ; ds = es = ss = 0 这样做就使得下面的段偏移地址直接映射为物理地址。
mov sp, LOADOFF
sti
; 下面就是转移代码,它会将这些代码移动到0x0600的位置,来给后续的加载空出位置。
mov si, sp ; si=sp=LOADOFF,这时的si就是代码开始的地址
;
这个入栈很重要,这将会将控制权交给次级引导,原文是:“Also where we'll return to eventually”
push si
mov di, BUFFER ; 移动到BUFFER的位置。
mov cx, 0x200 ; 移动256次,每次1字,一共一个扇区(512字节)
cld
rep movsw
jmp 0 : BUFFER + migrate ; 转移完成,跳转到0x600位置的migrate偏移位置。
migrate:
; 下面的代码就是这段代码真正的作用,寻可引导分区并
findactive:
; 这里很多人看了都会奇怪,我们前面都没动驱动器,怎么就开始通过dl来判断驱动器了,dl的值哪来的?
; 为了说明这个,不得不提到一个行为很低调的中断:int 19h
; 这个中断就BIOS在POST用来加载0sector的中断,它没有任
何入口参数,但会在dl中留下点东西。
; 我们来看一下它的描述:
; “DL = physical drive where boot sector is located”这一句道破天机!
; 在执行完int 19h之后,dl中会保留被加载的驱动器号。
; 很多人都知道,BIOS会将可引导的0扇区加载到0x7c00,熟不知就是这个中断完成的。
test dl, dl
; 如果是硬盘,驱动器号会大于等于0x80所以就不会跳转,如果非负,则是软盘,则需要寻可引导的其他驱动器来完成引导工作。
jns nextdisk
公司介绍源码; 已经确定是硬盘了,那么就在硬盘的分区列表中寻活动分区
mov si, BUFFER + PART_TABLE ; 将分区列表的入口传给si,下面的代码将通过[si+偏移]来获取分区信息
find:
cmp byte [si + sysind], 0x0 ; 分区的状态,如果不为0,则这个分区是以使用的。
jz nextpart ; 分区没被使用,检查下一个分区。
test byte [si + bootind], 0x80 ; 检查可引导标志,如果7位是1,则是可引导的。
jz nextpart ; 不是引导分区,继续检查下一个分区。
loadpart:
call load ; 已确定是活动分区,加载它上面的次级引导程序。
jc error1 ; int 13h加载失败,则cf标志是0,这里判断一下是否成功。
bootstrap:
ret ; 这就是关键一跳,向次级引导交出控制权!!!
nextpart:
add si, PENTRYSIZE ; si自增16字节,目的是跳过一个分区项。
cmp si, BUFFER + PART_TABLE + 0x4 * PENTRYSIZE ; 这里判断一下是否已经搜索完4个分区。
jb find ; 如果没搜索完,那么继续搜索。
; 搜索完4个,没有活动分区,报出错误,然后重启。
call print ; 这里的print会输出紧跟在它后面的字符串,原理很巧妙,稍后解释。
db "No active partition\0"
jmp reboot
; 在下一个驱动器寻活动分区:
nextdisk:
inc dl ; 前面已经提到,dl里装的是驱动器号,这里自增,意为下一个驱动器。
test dl, dl
js nexthd ; 如果是负数,则为硬盘,跳转到相应的处理子过程。
int 0x11 ; 这个中断可以活动设备的信息。
shl ax, 0x1 ; 因为软盘的信息在6、7位上,为了判断,下面进行位移。
shl ax, 0x1 ; 左移2次后,软盘信息已经被转移到ah的0、1位上了。
and ah, 0x03 ; 去掉ah除0、1位以外的部分。
cmp dl, ah ; 必须满足 dl <= ah 才能认为这个软盘驱动器存在。
ja nextdisk ; 不满足继续搜索。
call load0 ; 软盘驱动器存在,加载它的引导程序,让它完成引导。
jc nextdisk ; 加载失败,寻下一个。
ret ; 加载成功,将控制权交给软盘引导。
nexthd:
call load0 ; 加载硬盘的引导程序。
error1:
jc error ; 加载失败,跳转的错误处理子过程。
ret ; 加载成功,向硬盘引导交出控制权。
; 加载硬盘上的引导程序,也可以是软盘上的第0扇区
。
load0:
; 程序后面有个变量si = [si + lowsec]就是那个变量:zero,它的默认值是0。
; 这里为什么不直接用si = BUFFER + zero呢?是因为后面的load:需要同时兼容2种加载。
mov si, BUFFER + zero - lowsec
;
将当前驱动器的 [si + lowsec] 加载进内存.
load:
mov di, 0x3 ; 3次尝试的机会。
retry:
push dx ; 保护驱动器号,因为后面会动用dx
push es
push di ; es和di同样也会在后面的中断被修改,需要保护。
; 获取磁盘信息的中断:int 0x13 ( ah = 0x8 )
; ch是柱面数的低8位,cl[6..7] 是柱面数的高2位。
; cl[0..5]是每磁道的扇区数。
; dh = 磁头数
;
dl = 驱动器号。
mov ah, 0x08 ; 调用int 0x13 ( ah = 0x8 )
int 0x13
pop di ; 还原di
pop es ; 还原es
and cl, 0x3f ; cl = 获得每磁道的扇区数 (1为起点)。
inc dh ; dh原位磁头数,但起点为0,所以要加1才是正确的磁头数。
mov al, cl ; al = cl = 每磁道的扇区数
mul dh ; dh = 磁头数, ax 等于 磁头数×每磁道的扇区数=一个柱面的扇区数。
mov bx, ax ; 将每柱面的扇区数存入bx。
; 这里需要重点说明一下,由于这里需要兼容2中加载,所以,尽管代码一样,加载的参数是完全不同的。
; 如果加载的次级引导就在这段代码的硬盘上,那么si就是BUFFER,所以[si + lowsec]就是分区的LBA地址入口。
; 如果是其他驱动器,在前面可以看到。si = BUFFER + zero - lowsec,那么:
; [si +lowsec]就变成了[BUFFER + zero]。也就是变量zero,而它的值是0,意为这要加载的就是0扇区。
mov ax, word [si + lowsec + 0x0]
mov dx, word [si + lowsec + 0x2] ; dx:ax = 需要加载的次级引导的偏移扇区。
; 比较8G的高位和偏移扇区的高位,来判断读取的范围是否超过8G,如果超过了,就需要用int 13h的扩展读来读取。
cmp dx, ( 0x16390 * 0xff * 0x3f - 0xff ) >> 0x10
jae bigdisk ; 确定为8G以外的范围,跳转到扩展读子过程。
; bx存的是每柱面的扇区数,做除法后商为柱面号,存入ax。余数为柱面偏移扇区数,存入dx
div bx
xchg ax, dx ; 交换之后:ax = 柱面偏移扇区数, dx = 柱面号
mov ch, dl ; ch为柱面数的第8位
; 因为cl为每磁道的扇区数,则ax除以cl,商为磁头号存入al, 余数为磁道偏移扇区存入ah(以0为起点)
div cl
xor dl, dl ; dl清理,来为dh低2位移入dl高2位做准备。
shr dx, 0x1 ; 开始移动。
shr dx, 0x1 ; 移动完成dl[6..7] = 柱面数的高2位。
or dl, ah ; dl的低5位装入磁道偏移扇区 (0为起点)
mov cl, dl ; dl传入cl,则cl高2位为柱面号的高2位, cl的低5位是磁道偏移扇区数。
inc cl ; 由于原来是以0为起点,为了和中断兼容,转换至1为起点。
pop dx ; 恢复驱动器号dl
mov dh, al ; 磁头号传入dh
mov bx, LOADOFF ; es
:bx 是装入位置,此处为 0:0x7c00
mov ax, 0x0201 ; 读磁盘扇区的入口参数
int 0x13 ; int 13h加载次级引导。
jmp rdeval ; 检查读取是否成功。
; 这里介绍一下,对于大硬盘,BIOS如何读取它的扇区。
; 常规的,我们用BIOS读扇区都会使用3D来做定位,也就是磁头、柱面、扇区。
; 但是这种方式的寻址有限,它最多只能到8G以内的空间。
; 为了寻址到8G以外,BIOS进行了一次扩展,并给了我们一个接口:扩展读
; 扩展读会用到一个叫做DAP(disk address packet)的数据结构,中文:磁盘地址数据包
;
顾名思义,这个数据结构包涵了需要读取扇区的全部内容,所以,扩展读的入口很简单。
; 我们只需要设置好这个DAP就行了,本程序的DAP被存放在代码末尾。
bigdisk:
mov bx, dx ; bx:ax = dx:ax 要读取的扇区的线性偏移。
pop dx ; 恢复驱动器号dl
push si ; 保护si
mov si, BUFFER + ext_rw ; si此时为DAP的偏移地址。
mov word [si + 0x8], ax ; 将要读取的扇区的线性偏移存入DAP的blocknum中
mov word [si + 0xa], bx ; ax存低位,bx存高位。
mov ah, 0x42 ; 传入扩展读参数
int 0x13
pop si ; 恢复si,让它重新指向分区列表。
; 下面代码将判断是否读取成功,或者是否为可引导的扇区。
rdeval:
jnc rdok ; cf标志为0,则读取成功,进入可引导判断。
cmp ah, 0x80 ; 响应超时? (或者软盘被拔出)
je rdbad ; 跳到读取失败
dec di ; 并非响应超时,忽略此次操作,计数器恢复。
jl rdbad
xor ah, ah
int 0x13 ; 驱动器复位。
jnc retry ; 再次尝试。
rdbad:
stc ; 设置cf标志,表示读取出错。
; 其实这里有个BUG,它返回前没有清空zero变量,这使得在多硬盘的机器上,引导失败一次后,就无法再正确引导下一个硬盘了。
ret
; 检查读取扇区的可引导性。
rdok:
cmp dword [LOADOFF + MAGIC], 0xaa55 ; 比较魔数是否为0xaa55
jne nosig ; 如果不是,则不可引导
ret ; 可引导,成功返回,cf=0
; 打印不可引导的信息。
nosig:
call print
db "Not bootable\0"
jmp reboot
; 输出错误信息,然后准备重启。
error:
mov si, LOADOFF + errno + 0x1 ; 将错误码变量的数学低位存入si
prnum:
mov al, ah ; int 0x13的错误码存放在ah中,现在转入al
and al, 0x0f ; 取其低4位,因为错误码为16位数字,ascii码不会超过4位。
cmp al, 0xa ; 判断是否为大写字母A-F
jb digit ; 如果是0-9的数字,直接存入。
add al, 0x7 ; 如果是'A' - ':',则进行转换,add 0x7是因为ascii码正好隔了7位。
digit:
add byte [si], al ; 修改字符串errno,因为原来是'0',add之后就会得到相应数值。
dec si ; si减1,指向数学高位。
mov cl, 0x4
shr ah, cl ; 右移4位,这就是16进制的优势,只
需右移4位,就能得到高位。
jnz prnum ; 判断其是字符还是数字。
call print ; 打印错误码
db "Read error "
errno db "00\0"
;
jmp reboot
reboot:
call print
db ". Hit any key to reboot.\0"
xor ah, ah
int 0x16 ; 这个中断来等待用户键入
call print
db "\r\n\0" ; 换行
; 传说中的int 19h,在这用它来实现重启,不过现在来看,这么重启不是最好的做法。
int 0x19
; 下面就是print函数,这个mbr的输出全靠它了。
print:
; 由于是call过来的,那么堆栈中存有call下面指令的偏移,也就是ip。
pop si ; 获得call print下面的字符串地址。
prnext:
lodsb ; al = *si++ 逐个装入字符到al
test al, al ; 判断是否为'\0'
jz prdone ; 字符串结束,准备返回。
mov ah, 0x0e ; 打印字符的参数
mov bx, 0x0001 ; 第0页,黑底白字。
int 0x10 ; 打印字符。
jmp prnext
prdone:
jmp si ; 此时si指向字符'\0'之后的地方,而那正好是下一条语句。
; 磁盘地址数据包(DAP).
ext_rw:
PacketSize db 0x10 ; 这个数据结构的大小,固定为0X10
Reserved db 0x0 ; 保留
BlockCount dw 0x1 ; 要传送的扇区数,次级引导也是一个扇区。
BufferOffset dw LOADOFF ; 传送目的的偏移地址
BufferSegment dw 0x0 ; 传送目的的段地址
BlockNumLow dd 0x0 ; 线性偏移扇区的低32位
zero:
BlockNumHigh dd 0x0 ; 线性偏移扇区的高32位,这里又是变量zero
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论