突破 512 字节的限制(五)

        我们今天来接着学习操作系统。在之前我们在一个新的 OS 上编写了一个打印 hello 的语句,那么在实际的 OS 中,主程序的 512 字节肯定是放不下的。那么我们就要学习如何突破这 512 个字节,进而接着在 OS 上运行随后的代码。在上节博客中我们学习了主引导程序的扩展,那么我们在后面的学习中就是要将 512 字节后的代码交由软盘来存储。也就是将控制权由主引导程序交由软盘上的程序,进而执行后面的工作。我们先来做一个准备工作,编写一个辅助函数。它的功能是:1、字符串打印;2、软盘读取

        我们先来看看在 BIOS 中的字符串打印有哪些特点,如下

            1、指定打印参数(AX = 0x1301, BX = 0x0007);

            2、指定字符串的内存地址(ES:BP = 串地址);

            3、指定字符串的长度(CX = 串长度);

            4、中断调用(int 0x10)。

        下来我们来看一个字符串打印示例,如下所示

图片.png

        那么既然在代码中涉及到了汇编代码,我们就稍微来介绍下相关的汇编知识。1、在汇编中可以定义函数(函数名使用标签定义):call function,注意函数体的最后一条指令为 ret;2、如果代码中定义函数,那么需要定义栈空间:用于保存关键寄存器的值,栈顶地址通过 sp 寄存器保存;3、汇编中的“常量定义”(equ):a> 用法:const equ 0x7c00; ==> #define const 0x7c00;b> 与 dx(db, dw, dd) 的区别:dx 定义占用相应的内存空间,equ 定义不会占用任何内存空间。下来我们就来看看打印函数是怎么编写的,在编写打印函数之前,我们先写一个 makefile,用来代替那些繁琐的镜像制作步骤


makefile 源码

.PHONY : all clean rebuild

SRC := boot.asm
OUT := boot.bin
IMG := data.img

RM := rm -rf

all : $(SRC) $(OUT)
    dd if=$(OUT) of=$(IMG) bs=512 count=1 conv=notrunc
    @echo "Success!"
    
$(IMG) :
    bximage $@ -q -fd -size=1.44
    
$(OUT) : $(SRC)
    nasm $^ -o $@

clean :
    $(RM) $(IMG) $(OUT)
    
rebuild :
    @$(MAKE) clean
    @$(MAKE) all


boot.asm 源码

org 0x7c00

jmp short start
nop

header:
    BS_OEMName     db "D.T.Soft"
    BPB_BytsPerSec dw 512
    BPB_SecPerClus db 1
    BPB_RsvdSecCnt dw 1
    BPB_NumFATs    db 2
    BPB_RootEntCnt dw 224
    BPB_TotSec16   dw 2880
    BPB_Media      db 0xF0
    BPB_FATSz16    dw 9
    BPB_SecPerTrk  dw 18
    BPB_NumHeads   dw 2
    BPB_HiddSec    dd 0
    BPB_TotSec32   dd 0
    BS_DrvNum      db 0
    BS_Reserved1   db 0
    BS_BootSig     db 0x29
    BS_VolID       dd 0
    BS_VolLab      db "D.T.OS-0.01"
    BS_FileSysType db "FAT12   "

start:
    mov ax, cs
    mov ss, ax
    mov ds, ax
    mov es, ax
    mov sp, ax

    mov ax, MsgStr  ; 指定打印的字符串
    mov cx, 6       ; 指定打印的个数

    mov bp, ax  ; 指定目标字符串的段内偏移地址
    mov ax, ds
    mov es, ax  ; 指定目标字符串所在段的起始地址
    mov ax, 0x1301
    mov bx, 0x0007

    int 0x10    ; 指定 BIOS 的 0x10 号中断

last:
    hlt
    jmp last

MsgStr db "Hello, YHOS!"
Buf:
    times 510-($-$$) db 0x00
    db 0x55, 0xaa

        我们来编译看看结果

图片.png

        我们看到确实打印出了指定的前 6 个字符,说明我们的 Print 函数已经实现完成。接下来我们思考一个问题:主引导程序中如何读取指定扇区处的数据?

        我们先来看看软盘的构造:一个软盘有 2 个盘面,每个盘面对应 1 个磁头;每一个盘面被划分为若干个圆圈,成为柱面(磁道);每一个柱面被划分为若干个扇区,每个扇区 512 个字节。具体表示如下

图片.png

        那么之前的 3.5 寸的软盘的数据特性如下:

            1、每个盘面一共有 80 个柱面(编号为 0~79);

            2、每个柱面有 18 个扇区(编号为 1~18);

            3、存储大小:2 * 80 * 18 * 512 = 1474560 Bytes = 1440 KB = 1.44MB

        下来我们就来看看软盘数据的读取。软盘数据以扇区(512字节)为单位进行读取,指定数据所在位置的磁头号、柱面号、扇区号。计算公式如下

图片.png

        我们接下来就来看看 BIOS 中的软盘数据读取,通过 int 0x13 来实现,具体功能如下所示

图片.png

        下来看看软盘数据读取的流程,如下

图片.png

        我们在上面的公式中用到了除法操作,那么我们就来介绍下汇编中的 16 位除法操作(div),被除数放到 AX 寄存器,除数放到通用寄存器或内存单元(8 位),结果:商位于 AL,余数位于 AH。下来我们就来实现磁盘数据的读取操作代码

org 0x7c00

jmp short start
nop

define:
    BaseOfStack equ 0x7c00

header:
    BS_OEMName     db "D.T.Soft"
    BPB_BytsPerSec dw 512
    BPB_SecPerClus db 1
    BPB_RsvdSecCnt dw 1
    BPB_NumFATs    db 2
    BPB_RootEntCnt dw 224
    BPB_TotSec16   dw 2880
    BPB_Media      db 0xF0
    BPB_FATSz16    dw 9
    BPB_SecPerTrk  dw 18
    BPB_NumHeads   dw 2
    BPB_HiddSec    dd 0
    BPB_TotSec32   dd 0
    BS_DrvNum      db 0
    BS_Reserved1   db 0
    BS_BootSig     db 0x29
    BS_VolID       dd 0
    BS_VolLab      db "D.T.OS-0.01"
    BS_FileSysType db "FAT12   "

start:
    mov ax, cs
    mov ss, ax
    mov ds, ax
    mov es, ax
    mov sp, BaseOfStack
    
    mov ax, 34        
    mov cx, 1         
    mov bx, Buf   
    
    call ReadSector    
    
    mov bp, Buf
    mov cx, 34
    
    call Print
    
last:
    hlt
    jmp last
    
; es:bp --> string address
; cx    --> string length
Print:
    mov ax, 0x1301       
    mov bx, 0x0007
    int 0x10
    ret

; no parameters
ResetFloppy:
    push ax
    push dx
    
    mov ah, 0x00
    mov dl, [BS_DrvNum]
    int 0x13        
    
    pop dx
    pop ax
    
    ret
    
; ax   --> 逻辑扇区号
; cx   --> 连续读取的扇区
; es:bx --> 内存地址
ReadSector:
    push bx
    push cx
    push dx
    push ax
    
    call ResetFloppy 
    
    push bx
    push cx
   
    mov bl, [BPB_SecPerTrk]
    div bl
    mov cl, ah
    add cl, 1   
    mov ch, al
    shr ch, 1  
    mov dh, al
    and dh, 1  
    mov dl, [BS_DrvNum]  
    
    pop ax  
    pop bx
    
    mov ah, 0x02 

read:    
    int 0x13
    jc read 
    
    pop ax
    pop dx
    pop cx
    pop bx
    
    ret
    
MsgStr db "Hello, YHOS!"
MsgLen equ ($-MsgStr)
    
Buf:
    times 510-($-$$) db 0x00
    db 0x55, 0xaa

        我们先来看看生成的 data.img 中,我们所需的数据在什么地方,如下

图片.png

        我们看到是在 0x4400 处存放的,那么我们用 4400 的十进制 17424/512 = 34,因此我们在上面的 start 中, mov ax 34。字节长度为 34。下来我们来看看运行结果,是不是我们指定的这个地址处的这个字符串。结果如下

图片.png

        那么我们看到已经在正确打印出我们指定的字符串了。下来我们接着继续做准备工作,实现下面两个函数:内存比较和根目录区查找,整体思路如下

图片.png

        那么我们如何在根目录区查找目标文件呢?那便是通过根目录项的前 11 个字节进行判断,我们之前有用 C++ 实现过,代码如下

图片.png

        接下来我们便要用汇编语言来实现这部分的代码逻辑了。我们在实现之前先来看看内存比较是怎么回事,首先指定源起始地址(DS : SI),接着指定目标起始地址(ES : DI),最后判断在期望长度(CX)内每一个字节是否都相等。如下

图片.png

        在汇编中的比较与跳转是用 cmp 和 jz 实现的;比较指令示例:cmp cx, 0  ==> 比较 cx 的值是否为 0;跳转指令示例:jz equal ==> 如果比较的结果为真,则跳转至 equal 标签处。那么我们的比较操作示例代码如下

图片.png

        我们来看看具体源码是怎么编写的

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

    mov si, MsgStr
    mov di, DEST
    mov cx, MsgLen

    call MemCmp

    cmp cx, 0
    jz label
    jmp last

label:
    mov bp, MsgStr
    mov cx, MsgLen
    call Print

last:
    hlt
    jmp last


; ds:si --> souurce
; es:di --> destination
; cx    --> length
;
; return:
;        ( cx == 0) ? equal : noequal
MemCmp:
    push si
    push di
    push ax

compare:
    cmp cx, 0
    jz equal
    mov al, [si]
    cmp al, byte [di]
    jz goon
    jmp noequal
goon:
    inc si     ; si++
    inc di     ; di++
    dec cx     ; cx--
    jmp compare

equal:
noequal:
    pop ax
    pop di
    pop si

    ret
    
MsgStr db "Hello, YHOS!"
MsgLen equ ($-MsgStr)
DEST db "Hello, YHOS!"
Buf:
    times 510-($-$$) db 0x00
    db 0x55, 0xaa

        我们基于之前的代码添加上面的部分代码。我们来编译运行看看 DEST 和 MsgStr 是相同的,因此会打印出这个字符串

图片.png

        我们看到确实已经是打印出来了,那么我们如何确认是程序正常运行还是异常的呢?我们通过反汇编来查找 cmp cx, 0 这句指令的地址,进而打上断点,通过查看相关的寄存器的值。如果 cx 的值此时为 0,那么便证明我们的代码是正确的了。我们通过查看这句指令的地址如下

图片.png

        那么我们在这块打上断点,来看看此时相关寄存器的值是多少

图片.png

        我们看到 ecx 寄存器的值确实是 0,因此它是正确的。如果我们将 DEST 字符串的最后一个! 改为 ?,我们来看看这个寄存器的值此时是不是还是 0

图片.png

        我们看到此时 ecx 寄存器的值为 1,证明就是最后一个字符不匹配导致的。因而我们的内存比较操作函数是正确的,下来我们继续来看看如何查找根目录区是否存在目标文件,思路如下

图片.png

        那么如何来加载根目录区呢?示例代码如下

图片.png

        我们在访问栈空间中的栈顶数据时,不能使用 sp 直接访问栈顶数据,而是要通过其他通用寄存器间接访问栈顶数据,示例代码如下

图片.png

        我们来看看最终的代码是怎么写的

define:
    BaseOfStack equ 0x7c00
    RootEntryOffset equ 19
    RootEntryLength equ 14
    
start:
    mov ax, cs
    mov ss, ax
    mov ds, ax
    mov es, ax
    mov sp, BaseOfStack
    
    mov ax, RootEntryOffset
    mov cx, RootEntryLength
    mov bx, Buf
    
    call ReadSector
    
    mov si, Target
    mov cx, TarLen
    mov dx, 0
    
    call FindEntry
    
    cmp dx, 0
    jz output
    jmp last

output:
    mov bp, MsgStr
    mov cx, MsgLen
    call Print
    
last:
    hlt
    jmp last

; es:bx --> root entry offset address
; ds:si --> target string
; cx    --> target length
;
; return:
;       (dx != 0) ? exist : noexist
;         exist --> bx is the target entry
FindEntry:
    push di
    push bp
    push cx
    
    mov dx, [BPB_RootEntCnt]
    mov bp, sp
    
find:
    cmp dx, 0
    jz noexist
    mov di, bx    ; bx 寄存器的值指向了根目录区的第一项的入口地址
    mov cx, [bp]
    call MemCmp
    cmp cx, 0
    jz exist
    add bx, 32    ; 每一项代表 32 个字节
    dec dx        ; dx--
    jmp find
    
exist:
noexist:
    pop cx
    pop bp
    pop di
    
    ret
    
MsgStr db "No LOADER ..."
MsgLen equ ($-MsgStr)
Target db "LOADER     "
TarLen equ ($-Target)

        我们来查找根目录区中有没有 LOADER 的字符串,如果有,就什么都不打印,如果没有,就打印 No LOADER ...。我们来看看结果,方法是一样的。我们还是通过查看相关寄存器的值来确定函数是否正确执行。 dx 不是 0 ,则证明目标字符串存在,如果为 0,则没有。

图片.png

        我们看到 dx 不是 0,那么它就是存在的。我们再通过之前在 bochsrc 中加载 freedos 的方式来看看 data.img 是否存在 LOADER 呢?

图片.png

        我们看到在 data.img 中确实是存在 LOADER 字符串的,接下来我们在目标字符串前面 加上 - ,来看看是否会打印出 No LOADER ... 呢?

图片.png

图片.png

        我们看到再次执行后,dx 的值已经为 0,No LOADER ... 字符串也被打印出来了。从而再次证明我们写的根目录区查找函数是正确的,我们接着向下看,我们再来看看下来的流程图

图片.png

        我们现在的目标就是备份目标文件的目录信息(MemCpy),加载 Fat 表,并完成 Fat 表项的查找与读取(FatVec)。我们来看看目标文件的目录信息都有什么,备份它其实质就是内存拷贝。如下

图片.png

        在实现 MemCpy 的时候,注意的一个事项就是拷贝方向。要区分是从尾部向头部进行拷贝还是从头部向尾部进行拷贝,如下

图片.png

        我们在实现前先来看看相关的汇编代码,大于小于的代码指令的编写如下所示

图片.png

        我们接下来看看具体的源码是怎么实现的,如下

; ds:si --> source
; es:di --> destinaton
; cx    --> length
MemCpy:
    push si
    push di
    push cx
    push ax

    cmp si, di
    
    ja btoe    ; si > di
    
    add si, cx 
    add di, cx 
    dec si
    dec di 
    jmp etob ; si < di
    
    
btoe:
    cmp cx, 0
    jz done
    mov al, [si]
    mov byte [di], al
    inc si
    inc di
    dec cx
    jmp btoe
    
etob:
    cmp cx, 0
    jz done
    mov al, [si]
    mov byte [di], al
    dec si
    dec di
    dec cx
    jmp etob

done:
    pop ax
    pop cx
    pop di
    pop si
    
    ret

        测试代码如下

start:
    mov ax, cs
    mov ss, ax
    mov ds, ax
    mov es, ax
    mov sp, BaseOfStack
    
    mov ax, RootEntryOffset
    mov cx, RootEntryLength
    mov bx, Buf 

    call ReadSector

    mov si, Target
    mov cx, TarLen
    mov dx, 0

    call FindEntry

    cmp dx, 0
    jz output

    mov si, Target
    mov di,  MsgStr
    mov cx, TarLen

    call MemCpy

output:
    mov bp, MsgStr
    add bp, MsgLen
    call Print

        我们想要在内存中查找 LOADER 这个字符串并实现拷贝,看到运行的结果如下

图片.png

        我们接下来来看看 Fat 表项的读取,Fat 表项中的每个表项占用 1.5 个字节,即:使用 3 个字节可以表示 2 个表项,如下

图片.png

        我们下来看看 Fat 表项的“动态组装”,如下图所示

图片.png

        当 FatVec[j] 中的下标 j = 0, 2, 4, 6, 8 等时,i = j / 2 * 3  ==>(i, j 均为整数); FatVec[j] = ((Fat[i+1] & 0x0F) << 8) | Fat[i]; FatVec[j+1] = (Fat[i+2] << 4) | ((Fat[i+4]) & 0x0F); 接下来讲讲汇编中的相关代码的操作,在汇编中的 16 为乘法操作(mul):a> 被乘数放到 AL 寄存器; b> 乘数放到通用寄存器或内存单元(8位);c> 相乘的结果放到 AX 寄存器中。具体实现源码如下

; cx --> index
; bx --> fat table address
;
; return:
;       dx --> fat[index]
FatVec:
    mov ax, cx
    mov cl, 2
    div cl    ; cx / 2
    
    push ax
    
    mov ah, 0
    mov cx, 3
    mul cx
    mov cx, ax
    
    pop ax
    
    cmp ah, 0    ; 余数是否为0
    jz even
    jmp odd
    
even: 
    mov dx, cx
    add dx, 1
    add dx, bx   
    mov bp, dx
    mov dl, byte [bp]
    and dl, 0x0F
    shl dx, 8 ;
    add cx, bx  
    mov bp, cx
    or  dl, byte [bp]
    jmp return
    
odd:
    mov dx, cx
    add dx, 2
    add dx, bx
    mov bp, dx
    mov dl, byte [bp]
    mov dh, 0    ; 将 dx 寄存器的高8位全部赋值为0
    shl dx, 4
    add cx, 1
    add cx, bx
    mov bp, cx
    mov cl, byte [bp]
    shr cl, 4
    and cl, 0x0F
    mov ch, 0
    or  dx, cx

return:
    ret

        测试代码如下

define:
    BaseOfStack     equ 0x7c00
    BaseOfLoader    equ 0x9000
    RootEntryOffset equ 19
    RootEntryLength equ 14
    EntryItemLength equ 32
    FatEntryOffset  equ 1
    FatEntryLength  equ 9
    
start:
    mov ax, cs
    mov ss, ax
    mov ds, ax
    mov es, ax
    mov sp, BaseOfStack
    
    mov ax, RootEntryOffset
    mov cx, RootEntryLength
    mov bx, Buf
    
    call ReadSector
    
    mov si, Target
    mov cx, TarLen
    mov dx, 0
    
    call FindEntry
    
    cmp dx, 0
    jz output
    
    mov si, bx ; 将起始地址放到 si 中
    mov di, EntryItem
    mov cx, EntryItemLength
    
    call MemCpy
    
    ; 计算 Fat 表所占用的内存
    mov ax, FatEntryLength
    mov cx, [BPB_BytsPerSec]
    mul cx ; 将所占用的内存大小结果保存到 ax 中
    mov bx, BaseOfLoader
    sub bx, ax ; bx 就是 Fat 表在内存中的起始位置了
    
    mov ax, FatEntryOffset
    mov cx, FatEntryLength
    
    call ReadSector
    
    mov cx, [EntryItem + 0x1A] ; 获取目标起始处的位置
    
    call FatVec
    
    jmp last

output:
    mov bp, MsgStr
    mov cx, MsgLen
    call Print
    
last:
    hlt
    jmp last

        我们先来看看之前生成的镜像中,FatVec[j] 的值为多少。用 Qt 之前写程序来进行验证,在 ReadFileContent 函数中进行 j 的输出。将 main 函数中的目标字符串换成 LOADER ,然后看看结果

图片.png

        我们看到打印出来的是 4,我们再在 Linux 下进行断点调试,看看 ecx 寄存器的值是不是也是 4。通过反汇编我们查到在获取目标起始处的位置和调取 FatVec 的地方打上断点,我们来看看结果

图片.png

        我们看到第一次 ecx 的值确实 4,也就和在 Qt 中的结果进行相互验证了,edx 的值之前为 0,在调取完之后变成了 7。那么我们的代码调试也到此结束。

        通过今天的学习,总结如下:1、如果在汇编代码中定义了函数,那么需要定义栈空间。读取数据前,逻辑扇区号需要转化为磁盘的物理地址;2、物理软盘上的数据位置由磁头号,柱面号和扇区号唯一确定,软盘数据以扇区(512字节)为单位进行读取;3、可通过查找目录区判断是否存在目标文件:加载根目录区至内存中(ReadSector),遍历根目录区中每一项(FindEntry),通过每一项的前11个字节进行判断(MemCmp),当目标不存在时打印错误信息(Print);4、内存拷贝时需要考虑进行拷贝的方向,当 si > di 时,从前向后拷贝。当 si <= di 时,从后向前拷贝;5、Fat 表加载到内存中只会,需要“动态组装”表项:Fat 表中使用 3 个字节表示 2 个表项,其实字节 = 表项下标 / 2 * 3 --> (运算结果取整)。

猜你喜欢

转载自blog.51cto.com/12810168/2328650