突破 512 位元組的限制(五)
我們今天來接著學習作業系統。在之前我們在一個新的 OS 上編寫了一個列印 hello 的語句,那麼在實際的 OS 中,主程式的 512 位元組肯定是放不下的。那麼我們就要學習如何突破這 512 個位元組,進而接著在 OS 上執行隨後的程式碼。在上節部落格中我們學習了主載入程式的擴充套件,那麼我們在後面的學習中就是要將 512 位元組後的程式碼交由軟盤來儲存。也就是將控制權由主載入程式交由軟盤上的程式,進而執行後面的工作。我們先來做一個準備工作,編寫一個輔助函式。它的功能是:1、字串列印;2、軟盤讀取
我們先來看看在 BIOS 中的字串列印有哪些特點,如下
1、指定列印引數(AX = 0x1301, BX = 0x0007);
2、指定字串的記憶體地址(ES:BP = 串地址);
3、指定字串的長度(CX = 串長度);
4、中斷呼叫(int 0x10)。
下來我們來看一個字串列印示例,如下所示
那麼既然在程式碼中涉及到了彙編程式碼,我們就稍微來介紹下相關的彙編知識。1、在彙編中可以定義函式(函式名使用標籤定義):call function,注意函式體的最後一條指令為 ret;2、如果程式碼中定義函式,那麼需要定義棧空間:用於儲存關鍵暫存器的值,棧頂地址通過 sp 暫存器儲存;3、彙編中的“常量定義”(equ):a> 用法:const equ 0x7c00; ==> #define const 0x7c00;b> 與 dx(db, dw, dd) 的區別:dx 定義佔用相應的記憶體空間,equ 定義不會佔用任何記憶體空間。
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
我們來編譯看看結果
我們看到確實打印出了指定的前 6 個字元,說明我們的 Print 函式已經實現完成。接下來我們思考一個問題:主載入程式中如何讀取指定扇區處的資料?
我們先來看看軟盤的構造:一個軟盤有 2 個盤面,每個盤面對應 1 個磁頭;每一個盤面被劃分為若干個圓圈,成為柱面(磁軌);每一個柱面被劃分為若干個扇區,每個扇區 512 個位元組。具體表示如下
那麼之前的 3.5 寸的軟盤的資料特性如下:
1、每個盤面一共有 80 個柱面(編號為 0~79);
2、每個柱面有 18 個扇區(編號為 1~18);
3、儲存大小:2 * 80 * 18 * 512 = 1474560 Bytes = 1440 KB = 1.44MB
下來我們就來看看軟盤資料的讀取。軟盤資料以扇區(512位元組)為單位進行讀取,指定資料所在位置的磁頭號、柱面號、扇區號。計算公式如下
我們接下來就來看看 BIOS 中的軟盤資料讀取,通過 int 0x13 來實現,具體功能如下所示
下來看看軟盤資料讀取的流程,如下
我們在上面的公式中用到了除法操作,那麼我們就來介紹下彙編中的 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 中,我們所需的資料在什麼地方,如下
我們看到是在 0x4400 處存放的,那麼我們用 4400 的十進位制 17424/512 = 34,因此我們在上面的 start 中, mov ax 34。位元組長度為 34。下來我們來看看執行結果,是不是我們指定的這個地址處的這個字串。結果如下
那麼我們看到已經在正確打印出我們指定的字串了。下來我們接著繼續做準備工作,實現下面兩個函式:記憶體比較和根目錄區查詢,整體思路如下
那麼我們如何在根目錄區查詢目標檔案呢?那便是通過根目錄項的前 11 個位元組進行判斷,我們之前有用 C++ 實現過,程式碼如下
接下來我們便要用匯編語言來實現這部分的程式碼邏輯了。我們在實現之前先來看看記憶體比較是怎麼回事,首先指定源起始地址(DS : SI),接著指定目標起始地址(ES : DI),最後判斷在期望長度(CX)內每一個位元組是否都相等。如下
在彙編中的比較與跳轉是用 cmp 和 jz 實現的;比較指令示例:cmp cx, 0 ==> 比較 cx 的值是否為 0;跳轉指令示例:jz equal ==> 如果比較的結果為真,則跳轉至 equal 標籤處。那麼我們的比較操作示例程式碼如下
我們來看看具體原始碼是怎麼編寫的
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 是相同的,因此會打印出這個字串
我們看到確實已經是打印出來了,那麼我們如何確認是程式正常執行還是異常的呢?我們通過反彙編來查詢 cmp cx, 0 這句指令的地址,進而打上斷點,通過檢視相關的暫存器的值。如果 cx 的值此時為 0,那麼便證明我們的程式碼是正確的了。我們通過檢視這句指令的地址如下
那麼我們在這塊打上斷點,來看看此時相關暫存器的值是多少
我們看到 ecx 暫存器的值確實是 0,因此它是正確的。如果我們將 DEST 字串的最後一個! 改為 ?,我們來看看這個暫存器的值此時是不是還是 0
我們看到此時 ecx 暫存器的值為 1,證明就是最後一個字元不匹配導致的。因而我們的記憶體比較操作函式是正確的,下來我們繼續來看看如何查詢根目錄區是否存在目標檔案,思路如下
那麼如何來載入根目錄區呢?示例程式碼如下
我們在訪問棧空間中的棧頂資料時,不能使用 sp 直接訪問棧頂資料,而是要通過其他通用暫存器間接訪問棧頂資料,示例程式碼如下
我們來看看最終的程式碼是怎麼寫的
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,則沒有。
我們看到 dx 不是 0,那麼它就是存在的。我們再通過之前在 bochsrc 中載入 freedos 的方式來看看 data.img 是否存在 LOADER 呢?
我們看到在 data.img 中確實是存在 LOADER 字串的,接下來我們在目標字串前面 加上 - ,來看看是否會打印出 No LOADER ... 呢?
我們看到再次執行後,dx 的值已經為 0,No LOADER ... 字串也被打印出來了。從而再次證明我們寫的根目錄區查詢函式是正確的,我們接著向下看,我們再來看看下來的流程圖
我們現在的目標就是備份目標檔案的目錄資訊(MemCpy),載入 Fat 表,並完成 Fat 表項的查詢與讀取(FatVec)。我們來看看目標檔案的目錄資訊都有什麼,備份它其實質就是記憶體拷貝。如下
在實現 MemCpy 的時候,注意的一個事項就是拷貝方向。要區分是從尾部向頭部進行拷貝還是從頭部向尾部進行拷貝,如下
我們在實現前先來看看相關的彙編程式碼,大於小於的程式碼指令的編寫如下所示
我們接下來看看具體的原始碼是怎麼實現的,如下
; 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 這個字串並實現拷貝,看到執行的結果如下
我們接下來來看看 Fat 表項的讀取,Fat 表項中的每個表項佔用 1.5 個位元組,即:使用 3 個位元組可以表示 2 個表項,如下
我們下來看看 Fat 表項的“動態組裝”,如下圖所示
當 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 ,然後看看結果
我們看到打印出來的是 4,我們再在 Linux 下進行斷點除錯,看看 ecx 暫存器的值是不是也是 4。通過反彙編我們查到在獲取目標起始處的位置和調取 FatVec 的地方打上斷點,我們來看看結果
我們看到第一次 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 --> (運算結果取整)。