2020301918-os/boot/boot.asm
2022-10-02 12:54:35 +08:00

337 lines
16 KiB
NASM
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

org 07c00h ; Boot 状态, Bios 将把 Boot Sector 加载到 0:7C00h 处并开始执行
; 下面这部分不仅是boot的内容也是Fat12文件系统的引导部分
; boot需要文件系统的加持(可以没有但是如果这么干的话boot可扩展性为0磁盘内容位置稍微变动就不能用了)才能正确的将loader的内容导入到内存里执行
;================================================================================================
; Fat12规定前3字节是跳转代码后面再是文件系统信息
; 如果是短跳转(2字节指令机器码以0xEB开头)就需要后面加一个nop填充到3字节
; 如果是长跳转(3字节指令机器码以0xE9开头)就不需要添加nop了
jmp Main ; Start to boot.
; nop ; 这个 nop 不可少
; 下面是 FAT12 磁盘的头
; 正常情况下boot是要对磁盘头的数据进行解析的
; 但是出于简单考虑,直接将磁盘头硬编码进来,如果要分析可就太麻烦了,汇编本来就看的头大,还搞那么多未知元
; 里面很多信息实际上在boot里用不上请各位别对里面的参数纠结太多用到了再查也不迟
BS_OEMName DB 'ForrestY' ; OEM String, 必须 8 个字节
BPB_BytsPerSec DW 512 ; 每扇区字节数
BPB_SecPerClus DB 1 ; 每簇多少扇区
BPB_RsvdSecCnt DW 1 ; Boot 记录占用多少扇区
BPB_NumFATs DB 2 ; 共有多少 FAT 表
BPB_RootEntCnt DW 224 ; 根目录文件数最大值
BPB_TotSec16 DW 2880 ; 逻辑扇区总数
BPB_Media DB 0xF0 ; 媒体描述符
BPB_FATSz16 DW 9 ; 每FAT扇区数
BPB_SecPerTrk DW 18 ; 每磁道扇区数
BPB_NumHeads DW 2 ; 磁头数(面数)
BPB_HiddSec DD 0 ; 隐藏扇区数
BPB_TotSec32 DD 0 ; 如果 wTotalSectorCount 是 0 由这个值记录扇区数
BS_DrvNum DB 80h ; 中断 13 的驱动器号
BS_Reserved1 DB 0 ; 未使用
BS_BootSig DB 29h ; 扩展引导标记 (29h)
BS_VolID DD 0 ; 卷序列号
BS_VolLab DB 'OrangeS0.02' ; 卷标, 必须 11 个字节
BS_FileSysType DB 'FAT12 ' ; 文件系统类型, 必须 8个字节
; 文件系统信息存放完毕后后面的内容就可以自由调整了,撒花!
; 原先Orange的代码是按照 主函数->常量->变量->子函数的顺序给出,逻辑很乱
; 这次尝试用C语言风格按照 常量->变量->子函数->主函数的顺序给出,更加符合正常的编程逻辑
;============================================================================
;常量
;================================================================================================
; boot的内存模型实际上比较简单区间左闭右开
; (0x500~0x7c00) 栈
; (0x7c00~0x7e00) 引导扇区
; (0x90000~0x90400) 缓冲区GetNextCluster函数会用到它
; (0x90400~?) 加载区loader代码会加载到这里
BaseOfStack equ 07c00h ; Boot状态下堆栈基地址(栈底, 从这个位置向低地址生长)
BaseOfLoader equ 09000h ; LOADER.BIN 被加载到的位置 ---- 段地址
OffsetOfLoader equ 0400h ; LOADER.BIN 被加载到的位置 ---- 偏移地址
; 这部分请看手册
RootDirSectors equ 14
SectorNoOfRootDirectory equ 19
SectorNoOfFAT1 equ 1
DeltaSectorNo equ 31
;================================================================================================
;============================================================================
;变量
;----------------------------------------------------------------------------
LeftRootDirSectors dw RootDirSectors ; 还未搜索的根目录扇区数
RootDirSectorNow dw SectorNoOfRootDirectory ; 目前正在搜索的根目录扇区
BufferPacket times 010h db 0 ; ReadSector函数会用到的用于向int 13h中断的一个缓冲区
;============================================================================
;字符串
;----------------------------------------------------------------------------
LoaderFileName db "LOADER BIN", 0 ; LOADER.BIN 的文件名(为什么中间有空格请RTFM)
; 为简化代码, 下面每个字符串的长度均为 MessageLength
MessageLength equ 9
BootMessage: db "Booting " ; 9字节, 不够则用空格补齐. 序号 0
Message1 db "Ready. " ; 9字节, 不够则用空格补齐. 序号 1
Message2 db "Read Fail" ; 9字节, 不够则用空格补齐. 序号 2
Message3 db "No Loader" ; 9字节, 不够则用空格补齐. 序号 3
;============================================================================
; 汇编并不像高级语言一样规范,寄存器忘保存,调用子函数后发现值变了可太痛苦了
; 所以为了减少这份痛苦,这里的所有函数都保证函数除了返回值寄存器其余的主要寄存器都有保护现场
; 保证调用之后不用担心寄存器值变了
;----------------------------------------------------------------------------
; 函数名: DispStr
;----------------------------------------------------------------------------
; 作用:
; 显示一个字符串, 函数开始时 dh 中应该是字符串序号(从0开始)
DispStr:
push bp
mov bp, sp
pusha
push es
mov ax, MessageLength
mul dh
add ax, BootMessage
mov bp, ax
mov ax, ds
mov es, ax ; ES:BP = 串地址
mov cx, MessageLength ; CX = 串长度
mov ax, 01301h ; AH = 13, AL = 01h
mov bx, 0007h ; 页号为0(BH = 0) 黑底白字(BL = 07h)
mov dl, 0
int 10h
pop es
popa
pop bp
ret
;----------------------------------------------------------------------------
; 函数名: DispDot
;----------------------------------------------------------------------------
; 作用:
; 打印一个点
DispDot:
push bp
mov bp, sp
pusha
mov ah, 0Eh ; `. 每读一个扇区就在 "Booting " 后面
mov al, '.' ; | 打一个点, 形成这样的效果:
mov bl, 0Fh ; | Booting ......
int 10h ; /
popa
pop bp
ret
;----------------------------------------------------------------------------
; 函数名: ReadSector
;----------------------------------------------------------------------------
; 作用:
; 将磁盘的数据读入到内存中
; ax: 从哪个扇区开始
; cx: 读入多少个扇区
; (es:bx): 读入的缓冲区的起始地址
;
; 这里使用的是bios的扩展读功能,写成C语言的结构体是这样的
; struct buffer_packet {
; short buffer_packet_size; /* struct's size可以为 0x10 或 0x18*/
; short sectors; /* 读多少个 sectors */
; char *buffer; /* buffer address */
; long long start_sectors; /* 从哪个 sector 开始读 */
; long long *l_buffer; /* 64 位的 buffer address 这个我们不管!!!*/
; };
; 需要注意的是buffer参数的高16位填充的是段地址(es)低16位填充的是偏移量(bx)
;
; int 13h扩展读功能要求
; ah = 0x42
; dl = 驱动号
; (ds:si)是一个指向buffer_packet的指针
; 中断结束后有一个状态返回具体请STFW & RTFM
ReadSector:
push bp
mov bp, sp
pusha
mov si, BufferPacket
mov word [si + 0], 010h ; buffer_packet_size
mov word [si + 2], cx ; sectors
mov word [si + 4], bx ; buffer-offset
mov word [si + 6], es ; buffer-segment
mov word [si + 8], ax ; start_sectors
mov dl, [BS_DrvNum] ; 驱动号
mov ah, 42h ; 扩展读
int 13h
jc .ReadFail ; 读取失败简单考虑就默认bios坏了
popa
pop bp
ret
.ReadFail:
mov dh, 2
call DispStr
jmp $ ; 如果cf位置1就意味着读入错误这个时候建议直接开摆
;----------------------------------------------------------------------------
; 函数名: GetNextCluster
;----------------------------------------------------------------------------
; 作用:
; ax存放的是当前的簇(cluster)号根据当前的簇号在fat表里查找找到下一个簇的簇号并将返回值存放在ax
GetNextCluster:
push bp
mov bp, sp
pusha
mov bx, 3 ; 一个FAT项长度为1.5字节
mul bx
mov bx, 2 ; ax = floor(clus_number * 1.5)
div bx ; 这个时候ax里面放着的是FAT项基地址相对于FAT表开头的字节偏移量
; 如果clus_number为奇数则dx为1否则为0
push dx ; 临时保存奇数标识信息
mov dx, 0 ; 下面除法要用到
mov bx, [BPB_BytsPerSec]
div bx ; dx:ax / BPB_BytsPerSec
; ax <- 商 (基地址在FAT表的第几个扇区)
; dx <- 余数 (基地址在扇区内的偏移)
mov bx, 0 ; bx <- 0 于是, es:bx = BaseOfLoader:0
add ax, SectorNoOfFAT1 ; 此句之后的 ax 就是FAT项所在的扇区号
mov cx, 2 ; 读取FAT项所在的扇区, 一次读两个, 避免在边界
call ReadSector ; 发生错误, 因为一个FAT项可能跨越两个扇区
mov bx, dx ; 将偏移量搬回bx
mov ax, [es:bx]
pop bx ; 取回奇数标识信息
cmp bx, 0 ; 如果是第奇数个FAT项还得右移四位
jz EvenCluster ; 可能是微软(FAT是微软创建的)第一个亲儿子的原因,有它的历史局限性
shr ax, 4 ; 当时的磁盘很脆弱经常容易写坏所以需要两张FAT表备份而且人们能够制作的存储设备的容量很小
EvenCluster:
and ax, 0FFFh ; 读完需要与一下因为高位是未定义的防止ax值有误
mov word [bp - 2], ax ; 这里用了一个技巧这样在popa的时候ax也顺便更新了
popa
pop bp
ret
;----------------------------------------------------------------------------
; 函数名: StringCmp
;----------------------------------------------------------------------------
; 作用:
; 比较 ds:si 和 es:di 处的字符串比较长度为11仅为loader.bin所用
; 如果两个字符串相等ax返回1否则ax返回0
StringCmp:
push bp
mov bp, sp
pusha
mov cx, 11 ; 比较长度为11
cld ; 清位保险一下
.STARTCMP:
lodsb ; ds:si -> al
cmp al, byte [es:di]
jnz .DIFFERENT
inc di
dec cx
cmp cx, 0
jz .SAME
jmp .STARTCMP
.DIFFERENT:
mov word [bp - 2], 0 ; 这里用了一个技巧这样在popa的时候ax也顺便更新了
jmp .ENDCMP
.SAME:
mov word [bp - 2], 1 ; 下一步就是ENDCMP了就懒得jump了
.ENDCMP:
popa
pop bp
ret
;----------------------------------------------------------------------------
; 这里就是真正的boot的处理函数了boot实际上只做了一件事将loader从磁盘里搬到内存指定位置
; 如何将loader搬到内存中就需要文件系统里面的信息的帮助
; 通过扫描根目录区中所有可能的目录项找到loader.bin对应的目录项
; 然后根据目录项信息读入loader.bin的文件内容
Main:
mov ax, cs ; cs <- 0
mov ds, ax ; ds <- 0
mov ss, ax ; ss <- 0
mov ax, BaseOfLoader
mov es, ax ; es <- BaseOfLoader
mov sp, BaseOfStack ; 这几个段寄存器在Main里都不会变了
; 清屏
mov ax, 0600h ; AH = 6, AL = 0h
mov bx, 0700h ; 黑底白字(BL = 07h)
mov cx, 0 ; 左上角: (0, 0)
mov dx, 0184fh ; 右下角: (80, 50)
int 10h ; int 10h
mov dh, 0 ; "Booting "
call DispStr ; 显示字符串
mov ah, 0 ; ┓
mov dl, [BS_DrvNum] ; ┣ 硬盘复位
int 13h ; ┛
; 下面在 A 盘的根目录寻找 LOADER.BIN
FindLoaderInRootDir:
mov ax, [RootDirSectorNow]; ax <- 现在正在搜索的扇区号
mov bx, OffsetOfLoader ; es:bx = BaseOfLoader:OffsetOfLoader
mov cx, 1
call ReadSector
mov si, LoaderFileName ; ds:si -> "LOADER BIN"
mov di, OffsetOfLoader ; es:di -> BaseOfLoader:400h = BaseOfLoader*10h+400h
mov dx, 10h ; 32(目录项大小) * 16(dx) = 512(BPB_BytsPerSec)
CompareFilename:
call StringCmp
cmp ax, 1
jz LoaderFound ; ax == 1 -> 比对成了
dec dx
cmp dx, 0
jz GotoNextRootDirSector ; 该扇区的所有目录项都探索完了,去探索下一个扇区
add di, 20h ; 32 -> 目录项大小
jmp CompareFilename
GotoNextRootDirSector:
inc word [RootDirSectorNow] ; 改变正在搜索的扇区号
dec word [LeftRootDirSectors] ; ┓
cmp word [LeftRootDirSectors], 0 ; ┣ 判断根目录区是不是已经读完
jz NoLoader ; ┛ 如果读完表示没有找到 LOADER.BIN就直接开摆
jmp FindLoaderInRootDir
NoLoader:
mov dh, 3
call DispStr
jmp $
LoaderFound: ; 找到 LOADER.BIN 后便来到这里继续
add di, 01Ah ; 0x1a = 28 这个 28 在目录项里偏移量对应的数据是起始簇号RTFM
mov dx, word [es:di] ; 起始簇号占2字节读入到dx里
mov bx, OffsetOfLoader ; es:bx = BaseOfLoader:OffsetOfLoader
LoadLoader:
call DispDot
mov ax, dx ; ax <- 数据区簇号
add ax, DeltaSectorNo ; 数据区的簇号需要加上一个偏移量才能得到真正的扇区号
mov cx, 1 ; 一个簇就仅有一个扇区
call ReadSector
mov ax, dx ; ax <- 数据区簇号在之前ax = 数据区簇号+偏移量)
call GetNextCluster ; 根据数据区簇号获取文件下一个簇的簇号
mov dx, ax ; dx <- 下一个簇的簇号
cmp dx, 0FFFh ; 判断是否读完了根据文档理论上dx只要在0xFF8~0xFFF都行但是这里直接偷懒只判断0xFFF
jz LoadFinished
add bx, [BPB_BytsPerSec] ; 别忘了更新bx否则你会发现文件发生复写的情况指来回更新BaseOfLoader:OffsetOfLoader ~ BaseOfLoader:OffsetOfLoader+0x200
jmp LoadLoader
LoadFinished:
mov dh, 1 ; "Ready."
call DispStr ; 显示字符串
; 这一句正式跳转到已加载到内
; 存中的 LOADER.BIN 的开始处,
; 开始执行 LOADER.BIN 的代码。
; Boot Sector 的使命到此结束
jmp BaseOfLoader:OffsetOfLoader
times 510-($-$$) db 0 ; 填充剩下的空间使生成的二进制代码恰好为512字节
dw 0xaa55 ; 结束标志