1. 程式人生 > >突破 512 位元組的限制(五)

突破 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 [email protected] -q -fd -size=1.44
    
$(OUT) : $(SRC)
    nasm $^ -o [email protected]

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 --> (運算結果取整)。