《一个64位操作系统的实现》代码3-2 Boot和Loader加载程序

Posted by 橙叶 on Sat, Jun 13, 2020

由于对intel体系结构的不熟悉……导致我在接下来的一章里前进得相当困难。遂采取最笨的方法,把代码3-2的新内容的每一条汇编逐一加上注释,尽可能去理解新的内容。收获当然是有的,但是我很怀疑是否有能力应付后面的内容。

软盘读取模块

boot启动后,要去搜索根目录中的loader.bin程序,本质上是搜索根目录部分中的loader.bin目录项,搜索到后读取目录项中记录的簇号,但是这个簇号只是起始簇号,因为loader可能会占用1个以上的簇,所以还要根据这个簇号到FAT表中去查找下一个簇号。然后根据每一次读到的簇号,最终将整个loader加载到内存中。

书中对读取软盘的功能做了封装,因为BIOS中断提供的读取功能只接受CHS (Cylinder/Head/Sector,柱面/磁头/扇区),而我们所用的是LBA(逻辑区块地址)是扇区的形式,所以将这个转换过程包装在Func_ReadeOneSector里。

以下内容摘自书中对BIOS磁盘读取功能的描述

INT 13h,AH=02h 功能:读取磁盘扇区。
AL=读入的扇区数(必须非0); CH=磁道号(柱面号)的低8位; CL=扇区号1~63(bit 0~5),磁道号(柱面号)的高2位(bit 6~7, 只对硬盘有效); DH=磁头号; DL=驱动器号(如果操作的是硬盘驱动器,bit 7必须被置位); ES:BX=>数据缓冲区。

LBA到CHS的转换公式:

\rm LBA扇区号 \div 每磁道扇区数 = \left\{
\begin{aligned}
商Q \rightarrow \left\{
    \begin{aligned}
    柱面号 & = Q  >> 1 \\
    磁头号 & = Q \ \&  \ 1
    \end{aligned}
    \right.\\
余数R \rightarrow 起始扇区号 = \rm R + 1
\end{aligned}
\right.
;=======    read one sector from floppy
; 软盘读取功能
Func_ReadOneSector:
push    bp                      ; 栈帧寄存器压栈
mov bp, sp                      ; 
sub esp,    2                   ; esp - 2 在在栈中开辟两个字节的新空间
mov byte    [bp - 2],   cl      ; 参数cl=读入扇区的数量 存栈
push    bx                      ; BX 存栈
; 用LBA扇区号计算CHS (Cylinder/Head/Sector,柱面/磁头/扇区)
mov bl, [BPB_SecPerTrk]         ; 每磁道扇区数装入bl
; div 指令根据除数位数选择寄存器,8位为AX, 16位DX:AX,32位EDX:EAX,
div bl                          ; AX/BL = AL ... AH , LBA扇区号/每磁道扇区数 = 磁道号……磁道内扇区号
inc ah                          ; 磁道内起始扇区号从1开始,LBA从0开始,所以要+1
mov cl, ah                      ; AH移动到CL
mov dh, al                      ; 商AL移动到DH
shr al, 1                       ; 柱面号 = 磁道号>>1
mov ch, al                      ; 求得的柱面号移动至CH
and dh, 1                       ; DH(商)与1与,得到磁头号
pop bx                          ; 弹出BX
; 调用BIOS中断服务程序
mov dl, [BS_DrvNum]             ; DL参数驱动器号

Label_Go_On_Reading: mov ah, 2 ; 设置中断服务程序,ah=02h,功能是读取磁盘扇区 mov al, byte [bp - 2] ; 读入的扇区数,放入al, [bp-2]是之前输入的参数,指读入扇区数 int 13h ; BIOS中断服务程序 ; 中断服务程序从软盘扇区读取数据到内存中,数据读取成功时CF复位,CF=0 ; 此时轮询CF位,直到读取完成,完成时CF=0 jc Label_Go_On_Reading ; CF = 1 回到这一块代码开始

add esp,    2                   ; 收回2个字节的栈空间
pop bp                          ; bp出栈
ret                             ; 返回
; RET
; 弹出栈中的保存的返回地址(之前被CALL存进去的),回到了call Func_ReadOneSector</code></pre>

封装之后,只需传入扇区号即可完成磁盘读取

模块Func_ReadOneSector功能:读取磁盘扇区。 AX=待读取的磁盘起始扇区号; CL=读入的扇区数量; ES:BX=>目标缓冲区起始地址。

文件搜索模块

因为FAT表里只描述文件由哪些簇组成,文件属性都放在目录项里。目录项整个占32Bytes,前11Bytes是文件名。

名称 偏移 长度 描述
DIR_Name 0x00 11 文件名8B,扩展名3B
DIR_Attr 0x0B 1 文件属性
保留 0x0c 10 保留位
DIR_WrtTime 0x16 2 最后一次写入时间
DIR_WrtDate 0x18 2 最后一次写入日期
DIR_FstClus 0x1A 2 起始簇号
DIR_FileSize 0x1C 4 文件大小

下面分析一下比较文件名的过程

首先有两个字符串要比较,一个是在代码中定义的LoaderFileName,这是我们要找的文件名。另一个是读取到的目录项中的文件名,在书中的代码里,目录项被读取到了8000h这个地址

因为要比较的是字符串,每次都只能比较一个字符,所以需要两个指针,一个指向[LoaderFileName],一个指向目录项的文件名部分。每次比较一个字符,如果相同就移动指针前往下一个字符。在这里,LoaderFileName的指针是SI,目录项文件名的指针是DI

    mov si, LoaderFileName                       ; loader文件名地址装进SI
    mov di, 8000h                                ; DI = 8000H

指针的操作。对SI指针的操作,书中使用了LODSB/LODSW/LODSD/LODSQ(区别是值的位数)指令,

从DS:(R|E)SI寄存器指定的内存地址中读取数据到AL/AX/EAX/RAX寄存器 当数据载入到AL/AX/EAX/RAX寄存器后,(R|E)SI寄存器将会依据R|EFLAGS标志寄存器的DF位(之前用CLD清零的(DF=0)) 自动减少或增加载入的数据长度 若DF=0, (R|E)SI++,在本程序中即指向文件名下一个字符的地址

LODSB/LODSW/LODSD/LODSQ指令可以完成:从内存([DS:(R|E)SI])读入数据到寄存器(AL/AX/EAX/RAX,取决于数据长度)-> 移动SI指针(方向取决于DF标志位)

DI指针的操作,则在比较一次后使用inc递增。

Label_Go_On:
inc di                                       ; DI++, 移动指向目录项文件名的指针,指向下一个字符
jmp Label_Cmp_FileName                       ; 比较这个字符</code></pre>

比较两个字符:

    cmp al, byte    [es:di]                      ; AL已经存入了SI指向的字符(AL是我们给出的文件名的一个字符),现在与[es:di]比较,[es:di]是从磁盘中读取到的目录的文件名部分的一个字符的值

FAT表项解析模块

因为书中使用的是FAT12文件系统,这个文件系统比较棘手的一点是一个目录项占12bit,也就是一个半Byte,首先要计算表项的存放情况,确定是先一个Byte还是先半个Byte,然后在读取表项时需要先把表项从两个字节里分离出来。

=============================
|AAAAAAAA|AAAABBBB|BBBBBBBB|
=============================
;=======    get FAT Entry

Func_GetFATEntry: ; 获取FAT表项 push es ; ES 存栈 push bx ; BX 存栈 push ax ; AX 存栈 mov ax, 00 ; AX = 0 mov es, ax ; ES = AX = 0 pop ax ; AX 出栈 mov byte [Odd], 0 ; 标志位Odd = 0 mov bx, 3 ; BX = 3 mul bx ; AX = BX * AX mov bx, 2 ; BX = 2 div bx ; AX / BX = AX….DX

cmp dx, 0                       ; 比较DX,若为奇数,DX=1,若为偶数则DX=0
jz  Label_Even                  ; 若为偶数,跳转到Label_Even,此时Odd=0
mov byte    [Odd],  1           ; 否则标志位Odd设为1

Label_Even:

xor dx, dx                      ; DX = DX ^ DX = 0, 这样写比mov使用更少的字节
mov bx, [BPB_BytesPerSec]       ; BX = 每扇区字节数
div bx                          ; AX = Fat表项偏移扇区号,DX = FAT表项在扇区中的偏移位置
push    dx                      ; DX 存栈
mov bx, 8000h                   ; BX = 8000H,目标缓冲区起始地址
add ax, SectorNumOfFAT1Start    ; AX = AX + FAT1表起始扇区号(1),得到需要的FAT项所在的的扇区号,作为ReadOneSector的参数之一
mov cl, 2                       ; CL = 2 指示ReadOneSector读入两个扇区
call    Func_ReadOneSector      ; 调用ReadOneSector,把FAT表项所在的扇区读入到8000H

pop dx                          ; DX 出栈
add bx, dx                      ; BX = BX + DX,BX = 目标缓冲区起始地址 + FAT表项在扇区中的偏移位置
mov ax, [es:bx]                 ; 读出FAT表项到AX
cmp byte    [Odd],  1           ; Odd == 1?
jnz Label_Even_2                ; Odd != 1 , 跳转到Label_Even_2
shr ax, 4                       ; 奇数项右移4位(半字节),把属于前一个FAT表项的部分去掉

Label_Even_2: and ax, 0fffh ; AX & 0fffh, 只保留前12个bits = 1.5Bytes,即截取该FAT表项 pop bx ; 弹出BX pop es ; 弹出ES ret ; 返回,回到call Func_GetFATEntry的地方。 返回值AX = 要找的FAT表项

参数AH=FAT表项号,返回值AX是下一个FAT表项号。 通过不断读出FAT表项号,然后根据FAT表项号计算出数据所在的扇区,读出数据到内存,最终将整个文件加载到内存中。

完整内容

;/***************************************************
;       版权声明
;
;   本操作系统名为:MINE
;   该操作系统未经授权不得以盈利或非盈利为目的进行开发,
;   只允许个人学习以及公开交流使用
;
;   代码最终所有权及解释权归田宇所有;
;
;   本模块作者:  田宇
;   EMail:      345538255@qq.com
;
;
;***************************************************/
org 0x7c00  

BaseOfStack equ 0x7c00

; Loader 程序起始物理地址 BaseOfLoader equ 0x1000 OffsetOfLoader equ 0x00

RootDirSectors equ 14 ; 根目录所占扇区数 SectorNumOfRootDirStart equ 19 SectorNumOfFAT1Start equ 1 SectorBalance equ 17

jmp short Label_Start        
nop
BS_OEMName  db  &#039;MINEboot&#039;      ; 生产厂商名
BPB_BytesPerSec dw  512         ; 每扇区字节数 512bytes
BPB_SecPerClus  db  1           ; 每簇扇区数 1
BPB_RsvdSecCnt  dw  1           ; 保留扇区数 1
BPB_NumFATs db  2               ; FAT表的份数 2
BPB_RootEntCnt  dw  224         ; 根目录可容纳扇区数 223
BPB_TotSec16    dw  2880        ; 总扇区数
BPB_Media   db  0xf0            ; 介质描述符 0xF0 软盘
BPB_FATSz16 dw  9               ; 每Fat扇区数 9
BPB_SecPerTrk   dw  18          ; 每磁道扇区数 18
BPB_NumHeads    dw  2           ; 磁头数 2
BPB_HiddSec dd  0               ; 隐藏扇区数 0
BPB_TotSec32    dd  0           ; 如果BPB_TotSec16为0, 则由这个值记录扇区数
BS_DrvNum   db  0               ; int 13h 的驱动器号 0
BS_Reserved1    db  0           ; 未使用 0
BS_BootSig  db  0x29            ; 扩展引导标记 0x29h
BS_VolID    dd  0               ; 卷序列号
BS_VolLab   db  &#039;boot loader&#039; ; 卷标 &#039;boot loader&#039;
BS_FileSysType  db  &#039;FAT12   &#039;  ; 文件系统类型 &#039;FAT12&#039;

Label_Start: ; 跳转位置,跳过元数据, boot引导开始

mov ax, cs              
mov ds, ax
mov es, ax
mov ss, ax
mov sp, BaseOfStack

;======= clear screen

mov ax, 0600h
mov bx, 0700h
mov cx, 0
mov dx, 0184fh
int 10h

;======= set focus

mov ax, 0200h
mov bx, 0000h
mov dx, 0000h
int 10h

;======= display on screen : Start Booting……

mov ax, 1301h
mov bx, 000fh
mov dx, 0000h
mov cx, 10
push    ax
mov ax, ds
mov es, ax
pop ax
mov bp, StartBootMessage
int 10h

; 上面是第一部分

;======= reset floppy 重置软盘

xor ah, ah
xor dl, dl
int 13h

;======= search loader.bin ; SectorNo 扇区号, ; SectorNumOfRootDirStart 起始扇区号 就是根目录的起点 mov word [SectorNo], SectorNumOfRootDirStart ; 搜索loader.bin

Lable_Search_In_Root_Dir_Begin:

; 有一个程序状态寄存器,执行cmp后会给其中几个位(作用不同)设flag
; jz 是选择ZF标志位做跳转条件 执行条件是ZF=0
cmp word    [RootDirSizeForLoop],   0        ; 一个扇区一个扇区地查找loader, 比较Root
jz  Label_No_LoaderBin                       ; 等于转移 jz的意思是jump if zero 、
; 没有跳转的话就往下执行
dec word    [RootDirSizeForLoop]             ; RootDirSizeForLoop - 1  前往下一个扇区
mov ax, 00h                                  ; ax=00h
; 准备调用读取软盘用的Func_ReadOneSector函数,现在要输入参数
; 参数: AX=待读取的磁盘起始扇区号;CL=读入的扇区数量;ES:BX 目标缓冲区起始地址
mov es, ax                                   ; es=ax=0
mov bx, 8000h                                ; bx=8000h, 
mov ax, [SectorNo]                           ; 要读取扇区号
mov cl, 1                                    ; 读入一个扇区
call    Func_ReadOneSector                   ; 读入一个扇区,数据装到缓冲区[ES:BX] ([00008000h])
mov si, LoaderFileName                       ; loader文件名地址装进SI
mov di, 8000h                                ; DI = 8000H
cld                                          ; CLD flag方向标志位DF清零,在字串操作中使变址寄存器SI或DI的地址指针自动增加,字串处理由前往后。
                                             ; SI--, DI--
mov dx, 10h                                  ; DX = 10h 每个扇区可容纳的目录项的个数 512/32=10h,一个根目录项是32Bytes,前11Bytes是文件名

Label_Search_For_LoaderBin:

cmp dx, 0                                    ; DX==0?
jz  Label_Goto_Next_Sector_In_Root_Dir       ; DX==0 =&gt; ZF = 1, 说明这个扇区的目录项已经读取完了,要到下一个扇区:跳转至jz   Label_Goto_Next_Sector_In_Root_Dir,
dec dx                                       ; DX-- 这个扇区还没读完,去找下一个目录项
mov cx, 11                                   ; cx==11,指目录项的文件名长度

Label_Cmp_FileName:

cmp cx, 0                                    ; cx==0?
jz  Label_FileName_Found                     ; 若CX==0,说明每一个字符都匹配,即找到文件,跳转到Label_FileName_Found
dec cx                                       ; 否则继续比较下一个字符,CX--,
lodsb                                        ; LODSB/LODSW/LODSD/LODSQ(区别是值的位数)指令:
                                             ;          从DS:(R|E)SI寄存器指定的内存地址中读取数据到AL/AX/EAX/RAX寄存器
                                             ;          当数据载入到AL/AX/EAX/RAX寄存器后,(R|E)SI寄存器将会依据R|EFLAGS标志寄存器的DF位(之前用CLD清零的(DF=0))
                                             ;          自动减少或增加载入的数据长度
                                             ;          若DF=0, (R|E)SI++,在本程序中即指向文件名下一个字符的地址
cmp al, byte    [es:di]                      ; AL已经存入了SI指向的字符(AL是我们给出的文件名的一个字符),现在与[es:di]比较,[es:di]是从磁盘中读取到的目录的文件名部分的一个字符的值
jz  Label_Go_On                              ; 相同,跳转到Label_Go_On,准备比较下一个字符
jmp Label_Different                          ; 不相同,确认与要搜索的文件名不符,跳转到Label_Different,准备读取下一个目录项

Label_Go_On:

inc di                                       ; DI++, 移动指向目录项文件名的指针,指向下一个字符
jmp Label_Cmp_FileName                       ; 比较这个字符

Label_Different:
; 目录项的文件名不相同 and di, 0ffe0h ; 这个and的目的是,将di指向这个目录项(刚刚搜索的那个)的开头 add di, 20h ; 20H = 32D,然后跳过这个目录项,到达下一个目录项开头 mov si, LoaderFileName ; 重置我们要搜索的文件名的指针,(因为搜索前面的目录项时,这个指针被动过) jmp Label_Search_For_LoaderBin ; 回到Label_Search_For_LoaderBin

Label_Goto_Next_Sector_In_Root_Dir:

add word    [SectorNo], 1                    ; SectorNo + 1,扇区号加1,即下一个扇区
jmp Lable_Search_In_Root_Dir_Begin           ; 去在下一个扇区里搜索

;======= display on screen : ERROR:No LOADER Found

Label_No_LoaderBin: ; 显示无loader mov ax, 1301h mov bx, 008ch mov dx, 0100h mov cx, 21 push ax mov ax, ds mov es, ax pop ax mov bp, NoLoaderMessage int 10h jmp $

;======= found loader.bin name in root director struct

Label_FileName_Found: ; 文件名匹配 mov ax, RootDirSectors ; AX = 根目录所占扇区数 and di, 0ffe0h ; di 重新指向找到的这个目录项的开始 add di, 01ah ; di 跳到起始簇号所在的地方 mov cx, word [es:di] ; 取出簇号(簇号占2字节=1字) push cx ; cx存栈 add cx, ax ; CX = CX + AX,得到数据区开头 add cx, SectorBalance ; CX = CX + 根目录起始区号 - 2。 实际上,扇区号 = 数据区簇号(从2开始)*每簇扇区号 + 根目录起始扇区号 + 根目录所占扇区数 - 2 mov ax, BaseOfLoader ; AX = loader程序起始物理地址,最终要把loader加载到内存的这个地方 mov es, ax ; ES = AX = loader程序起始物理地址 mov bx, OffsetOfLoader ; BX = OffsetOfLoader mov ax, cx ; AX = CX = (数据所在的)扇区号

Label_Go_On_Loading_File: push ax ; AX 压栈 push bx ; BX 压栈

; 显示 “...”
mov ah, 0eh                                 ; AH = 0eH
mov al, &#039;.&#039;
mov bl, 0fh
int 10h                                     ; BIOS中断服务程序

pop bx                                      ; BX 出栈
pop ax                                      ; AX 出栈
mov cl, 1                                   ; CL = 1;
call    Func_ReadOneSector                  ; 读取一个扇区
pop ax                                      ; 弹出AX
call    Func_GetFATEntry                    ; 获取FAT表项,FAT表项在AX中返回
cmp ax, 0fffh                               ; AX == 0fffh? 检查是否是最后一个簇
jz  Label_File_Loaded                       ; 如果是最后一个簇,跳转到Label_File_Loaded
push    ax                                  ; AX压栈
mov dx, RootDirSectors                      ; DX = 根目录所占扇区数
add ax, dx                                  ; AX = AX + DX 
add ax, SectorBalance                       ; AX + SectorBalance, AX =  文件所在簇号 + 根目录所占扇区数 + 根目录起始扇区 - 2 = FAT表项所指的簇
add bx, [BPB_BytesPerSec]                   ; BX + 每扇区字节数,目的是将读出的内容追加到上一次读出的内容后面
jmp Label_Go_On_Loading_File                ; 跳转至 Label_Go_On_Loading_File,读出下一个扇区

Label_File_Loaded:

jmp $

;======= read one sector from floppy ; 软盘读取功能 Func_ReadOneSector:

push    bp                      ; 栈帧寄存器压栈
mov bp, sp                      ; 
sub esp,    2                   ; esp - 2 在在栈中开辟两个字节的新空间
mov byte    [bp - 2],   cl      ; 参数cl=读入山区的数量 存栈
push    bx                      ; BX 存栈
; 用LBA扇区号计算CHS (Cylinder/Head/Sector,柱面/磁头/扇区)
mov bl, [BPB_SecPerTrk]         ; 每磁道扇区数装入bl
; div 指令根据除数位数选择寄存器,8位为AX, 16位DX:AX,32位EDX:EAX,
div bl                          ; AX/BL = AL ... AH , LBA扇区号/每磁道扇区数 = 磁道号……磁道内扇区号
inc ah                          ; 磁道内起始扇区号从1开始,LBA从0开始,所以要+1
mov cl, ah                      ; AH移动到CL
mov dh, al                      ; 商AL移动到DH
shr al, 1                       ; 柱面号 = 磁道号&gt;&gt;1
mov ch, al                      ; 求得的柱面号移动至CH
and dh, 1                       ; DH(商)与1与,得到磁头号
pop bx                          ; 弹出BX
; 调用BIOS中断服务程序
mov dl, [BS_DrvNum]             ; DL参数驱动器号

Label_Go_On_Reading: mov ah, 2 ; 设置中断服务程序,ah=02h,功能是读取磁盘扇区 mov al, byte [bp - 2] ; 读入的扇区数,放入al, [bp-2]是之前输入的参数,指读入扇区数 int 13h ; BIOS中断服务程序 ; 中断服务程序从软盘扇区读取数据到内存中,数据读取成功时CF复位,CF=0 ; 此时轮询CF位,直到读取完成,完成时CF=0 jc Label_Go_On_Reading ; CF = 1 回到这一块代码开始

add esp,    2                   ; 收回2个字节的栈空间
pop bp                          ; bp出栈
ret                             ; 返回
; RET
; 弹出栈中的保存的返回地址(之前被CALL存进去的),回到了116 call Func_ReadOneSector

;======= get FAT Entry

Func_GetFATEntry: ; 获取FAT表项 push es ; ES 存栈 push bx ; BX 存栈 push ax ; AX 存栈 mov ax, 00 ; AX = 0 mov es, ax ; ES = AX = 0 pop ax ; AX 出栈 mov byte [Odd], 0 ; 标志位Odd = 0 mov bx, 3 ; BX = 3 mul bx ; AX = BX * AX mov bx, 2 ; BX = 2 div bx ; AX / BX = AX….DX

cmp dx, 0                       ; 比较DX,若为奇数,DX=1,若为偶数则DX=0
jz  Label_Even                  ; 若为偶数,跳转到Label_Even,此时Odd=0
mov byte    [Odd],  1           ; 否则标志位Odd设为1

Label_Even:

xor dx, dx                      ; DX = DX ^ DX = 0, 这样写比mov使用更少的字节
mov bx, [BPB_BytesPerSec]       ; BX = 每扇区字节数
div bx                          ; AX = Fat表项偏移扇区号,DX = FAT表项在扇区中的偏移位置
push    dx                      ; DX 存栈
mov bx, 8000h                   ; BX = 8000H,目标缓冲区起始地址
add ax, SectorNumOfFAT1Start    ; AX = AX + FAT1表起始扇区号(1),得到需要的FAT项所在的的扇区号,作为ReadOneSector的参数之一
mov cl, 2                       ; CL = 2 指示ReadOneSector读入两个扇区
call    Func_ReadOneSector      ; 调用ReadOneSector,把FAT表项所在的扇区读入到8000H

pop dx                          ; DX 出栈
add bx, dx                      ; BX = BX + DX,BX = 目标缓冲区起始地址 + FAT表项在扇区中的偏移位置
mov ax, [es:bx]                 ; 读出FAT表项到AX
cmp byte    [Odd],  1           ; Odd == 1?
jnz Label_Even_2                ; Odd != 1 , 跳转到Label_Even_2
shr ax, 4                       ; 奇数项右移4位(半字节),把属于前一个FAT表项的部分去掉

Label_Even_2: and ax, 0fffh ; AX & 0fffh, 只保留前12个bits = 1.5Bytes,即截取该FAT表项 pop bx ; 弹出BX pop es ; 弹出ES ret ; 返回,回到call Func_GetFATEntry的地方。 返回值AX = 要找的FAT表项

;======= tmp variable

RootDirSizeForLoop dw RootDirSectors SectorNo dw 0 Odd db 0

;======= display messages

StartBootMessage: db "Start Boot" NoLoaderMessage: db "ERROR:No LOADER Found" LoaderFileName: db "LOADER BIN",0

;======= fill zero until whole sector

times   510 - ($ - $$)  db  0
dw  0xaa55

运行效果



comments powered by Disqus