自己動手從零寫桌面操作系統GrapeOS系列教程——24.加載並運行loader

學習操作系統原理最好的方法是自己寫一個簡單的操作系統。


之前我們在電腦的啓動過程中介紹過boot程序的主要任務就是加載並運行loader程序,本講我們就來實現。
本講代碼文件共2個:

  • boot.asm
  • loader.asm

一、代碼及講解

本講所用到的知識點都是之前已經用過的,只是在本講中綜合應用了一下。
關於如何讀取文件在上一講中已經介紹過了,我們只要在上講代碼中把要讀取的文件名改成loader的文件名"LOADER  BIN"即可讀取loader程序文件。
本講的boot.asm就是在上講的基礎上稍微改了下,加了3處提示語句。程序一開始先清屏並在屏幕上輸出字符串“GrapeOS boot start.”。然後從硬盤根目錄查找LOADER.BIN程序文件,如果沒有找到文件則在屏幕上輸出字符串“Loader not found.”,如果找到了文件則在屏幕上輸出字符串“Loader found.”。如果找到了文件則讀取文件內容,讀取完後通過jmp指令跳轉到loader在內存中的起始地址,這樣就完成了加載並運行loader。
boot.asm中的代碼如下:

;--------------------定義常量--------------------
;FAT16目錄項中各成員的偏移量:
;名稱                 偏移   長度    描述
DIR_Name        equ    0     ;11    文件名8B,擴展名3B
DIR_Attr        equ    11    ;1     目錄項屬性
;Reserved       equ    12    ;10    保留位
DIR_WrtTime     equ    22    ;2     最後一次寫入時間
DIR_WrtDate     equ    24    ;2     最後一次寫入日期
DIR_FstClus     equ    26    ;2     起始簇號
DIR_FileSize    equ    28    ;4     文件大小

BOOT_ADDRESS equ 0x7c00 ;boot程序加載到內存的地址。
DISK_BUFFER  equ 0x7e00 ;讀磁盤臨時存放數據用的緩存區,放到boot程序之後。
DISK_SIZE_M equ 4 ;磁盤容量,單位M。
FAT1_SECTORS     equ 32 ;FAT1佔用扇區數
ROOT_DIR_SECTORS equ 32 ;根目錄佔用扇區數
SECTOR_NUM_OF_FAT1_START     equ 1 ;FAT1表起始扇區號
SECTOR_NUM_OF_ROOT_DIR_START equ 33 ;根目錄區起始扇區號
SECTOR_NUM_OF_DATA_START     equ 65 ;數據區起始扇區號,對應簇號爲2。
SECTOR_CLUSTER_BALANCE       equ 63 ;簇號加上該值正好對應扇區號。
FILE_NAME_LENGTH     equ 11 ;文件名8字節加擴展名3字節共11字節。
DIR_ENTRY_SIZE       equ 32 ;目錄項爲32字節。
DIR_ENTRY_PER_SECTOR equ 16 ;每個扇區能存放目錄項的數目。

LOADER_ADDRESS          equ 0x1000          ;loader程序加載到內存的地址。
STACK_BOTTOM            equ LOADER_ADDRESS  ;棧底地址(把棧放到loader程序前面)
VIDEO_SEGMENT_ADDRESS   equ 0xb800          ;顯存的段地址(默認顯示模式爲25行80列字符模式)
VIDEO_CHAR_MAX_COUNT    equ 2000            ;默認屏幕最多顯示字符數。

;--------------------MBR開始--------------------
org BOOT_ADDRESS
jmp boot_start
nop

;FAT16參數區:
BS_OEMName 	db 'GrapeOS '   ;廠商名稱(8字節,含空格)
BPB_BytesPerSec dw 0x0200	;每扇區字節數
BPB_SecPerClus	db 0x01		;每簇扇區數
BPB_RsvdSecCnt	dw 0x0001	;保留扇區數(引導扇區的扇區數)
BPB_NumFATs	db 0x01		;FAT表的份數
BPB_RootEntCnt	dw 0x0200	;根目錄可容納的目錄項數
BPB_TotSec16	dw 0x2000 	;扇區總數(4MB)
BPB_Media	db 0xf8 	;介質描述符
BPB_FATSz16	dw 0x0020	;每個FAT表扇區數
BPB_SecPerTrk	dw 0x0020	;每磁道扇區數
BPB_NumHeads	dw 0x0040	;磁頭數
BPB_hiddSec	dd 0x00000000	;隱藏扇區數
BPB_TotSec32	dd 0x00000000	;如果BPB_TotSec16是0,由這個值記錄扇區數。
BS_DrvNum	db 0x80		;int 13h的驅動器號
BS_Reserved1	db 0x00		;未使用
BS_BootSig	db 0x29		;擴展引導標記
BS_VolID	dd 0x00000000   ;卷序列號
BS_VolLab	db 'Grape OS   ';卷標(11字節,含空格)
BS_FileSysType	db 'FAT16   '	;文件系統類型(8字節,含空格)

;通過以上參數可知硬盤容量爲4MB,共8K個扇區。扇區具體分佈如下:
;區域名     扇區數      扇區號          字節偏移            說明
;引導扇區   1個扇區     扇區0           0x0000~0x01ff
;FAT1表     32個扇區    扇區1~32        0x0200~0x41ff   可記錄8K-2個簇
;FAT2表     無          無              無              無
;根目錄區   32個扇區    扇區33~64       0x4200~0x81ff   可容納512個目錄項
;數據區     8127個扇區  扇區65~0x1fff   0x8200~0x3fffff

;--------------------程序開始--------------------
boot_start:
;初始化寄存器
mov ax,cs
mov ds,ax
mov es,ax ;cmpsb會用到ds:si和es:di
mov ss,ax
mov sp,STACK_BOTTOM
mov ax,VIDEO_SEGMENT_ADDRESS
mov gs,ax ;本程序中gs專用於指向顯存段

;清屏
call func_clear_screen

;打印字符串:"GrapeOS boot start."
mov si,boot_start_string
mov di,0 ;在屏幕第1行顯示
call func_print_string

;讀取loader文件開始
;讀取根目錄的第1個扇區(1個扇區可以存放16個目錄項,我們用到的文件少,不會超過16個。)
mov esi,SECTOR_NUM_OF_ROOT_DIR_START 
mov di,DISK_BUFFER
call func_read_one_sector

;在16個目錄項中通過文件名查找文件
cld ;cld將標誌位DF置0,在串處理指令中控制每次操作後讓si和di自動遞增。std相反。下面repe cmpsb會用到。
mov bx,0 ;用bx記錄遍歷第幾個目錄項。
next_dir_entry:
mov si,bx
shl si,5 ;乘以32(目錄項的大小)
add si,DISK_BUFFER              ;源地址指向目錄項中的文件名。
mov di,loader_file_name_string  ;目標地址指向loader程序在硬盤中的正確文件名。
mov cx,FILE_NAME_LENGTH ;字符比較次數爲FAT16文件名長度,每比較一個字符,cx會自動減一。
repe cmpsb ;逐字節比較ds:si和es:di指向的兩個字符串。
jcxz loader_found ;當cx爲0時跳轉。cx爲0表示上面比較的兩個字符串相同。找到了loader文件。
inc bx
cmp bx,DIR_ENTRY_PER_SECTOR
jl next_dir_entry ;檢查下一個目錄項。
jmp loader_not_found ;沒有找到loader文件。

loader_found:
;打印字符串:"Loader found."
mov si,loader_found_string
mov di,80 ;在屏幕第2行顯示
call func_print_string

;從目錄項中獲取loader文件的起始簇號
shl bx,5 ;乘以32
add bx,DISK_BUFFER
mov bx,[bx+DIR_FstClus] ;loader的起始簇號

;讀取FAT1表的第1個扇區(我們用到的文件少且小,只用到了該扇區中的簇號)
mov esi,SECTOR_NUM_OF_FAT1_START 
mov di,DISK_BUFFER ;放到boot程序之後
call func_read_one_sector

;按簇讀loader
mov bp,LOADER_ADDRESS ;loader文件內容讀取到內存中的起始地址
read_loader:
xor esi,esi ;esi清零
mov si,bx ;簇號
add esi,SECTOR_CLUSTER_BALANCE
mov di,bp
call func_read_one_sector
add bp,512 ;下一個目標地址

;獲取下一個簇號(每個FAT表項爲2字節)
shl bx,1 ;乘2,每個FAT表項佔2個字節
mov bx,[bx+DISK_BUFFER]

;判斷下一個簇號
cmp bx,0xfff8 ;大於等於0xfff8表示文件的最後一個簇
jb read_loader ;jb無符號小於則跳轉,jl有符號小於則跳轉。

read_loader_finish: ;讀取loader文件結束
jmp LOADER_ADDRESS ;跳轉到loader在內存中的地址

loader_not_found: ;沒有找到loader文件。
;打印字符串:"Loader not found."
mov si,loader_not_found_string
mov di,80 ;在屏幕第2行顯示
call func_print_string

stop:
hlt
jmp stop

;清屏函數(將屏幕寫滿空格就實現了清屏)
;輸入參數:無。
;輸出參數:無。
func_clear_screen:
mov ah,0x00 ;黑底黑字
mov al,' '  ;空格
mov cx,VIDEO_CHAR_MAX_COUNT ;循環控制
.start_blank:
mov bx,cx ;以下3行表示bx=(cx-1)*2 
dec bx
shl bx,1
mov [gs:bx],ax ;[gs:bx]表示字符對應的顯存地址(從屏幕右下角往前清屏)
loop .start_blank
ret

;打印字符串函數。
;輸入參數:ds:si,di。
;輸出參數:無。
;ds:si 表示字符串起始地址,以0爲結束符。
;di 表示字符串在屏幕上顯示的起始位置(0~1999)。
func_print_string:
mov ah,0x07 ;ah表示字符屬性,0x07表示黑底白字。
shl di,1 ;乘2(屏幕上每個字符對應2個顯存字節)。
.start_char:
mov al,[si]
cmp al,0
jz .end_print
mov [gs:di],ax ;將字符和屬性放到對應的顯存中。
inc si
add di,2
jmp .start_char
.end_print:
ret

;讀取硬盤1個扇區(主硬盤控制器主盤)
;輸入參數:esi,ds:di。
;esi LBA扇區號
;ds:di 將數據寫入到的內存起始地址
;輸出參數:無。
func_read_one_sector:
;第1步:檢查硬盤控制器狀態
mov dx,0x1f7
.not_ready1:
nop ;nop相當於稍息 hlt相當於睡覺
in al,dx ;讀0x1f7端口
and al,0xc0 ;第7位爲1表示硬盤忙,第6位爲1表示硬盤控制器已準備好,正在等待指令。
cmp al,0x40 ;當第7位爲0,且第6位爲1,則進入下一個步。
jne .not_ready1 ;若未準備好,則繼續判斷。
;第2步:設置要讀取的扇區數
mov dx,0x1f2
mov al,1
out dx,al ;讀取1個扇區
;第3步:將LBA地址存入0x1f3~0x1f6
mov eax,esi
;LBA地址7~0位寫入端口0x1f3
mov dx,0x1f3
out dx,al
;LBA地址15~8位寫入端口寫入0x1f4
shr eax,8
mov dx,0x1f4
out dx,al
;LBA地址23~16位寫入端口0x1f5
shr eax,8
mov dx,0x1f5
out dx,al
;第4步:設置device端口
shr eax,8
and al,0x0f ;LBA第24~27位
or al,0xe0 ;設置7~4位爲1110,表示LBA模式,主盤
mov dx,0x1f6
out dx,al
;第5步:向0x1f7端口寫入讀命令0x20
mov dx,0x1f7
mov al,0x20
out dx,al
;第6步:檢測硬盤狀態
.not_ready2:
nop ;nop相當於稍息 hlt相當於睡覺
in al,dx ;讀0x1f7端口
and al,0x88 ;第7位爲1表示硬盤忙,第3位爲1表示硬盤控制器已準備好數據傳輸。
cmp al,0x08 ;當第7位爲0,且第3位爲1,進入下一步。
jne .not_ready2 ;若未準備好,則繼續判斷。
;第7步:從0x1f0端口讀數據
mov cx,256 ;每次讀取2字節,一個扇區需要讀256次。
mov dx,0x1f0
.go_on_read:
in ax,dx
mov [di],ax
add di,2
loop .go_on_read
ret

loader_file_name_string:db "LOADER  BIN",0  ;loader程序在硬盤中存儲的文件名,共11個字節,含空格。
boot_start_string:db "GrapeOS boot start.",0
loader_not_found_string:db "Loader not found.",0
loader_found_string:db "Loader found.",0

times 510-($-$$) db 0
db 0x55,0xaa

目前我們的loader程序非常簡單,只是向屏幕輸出一行字符串“GrapeOS loader start.”。
loader.asm中的代碼如下:

org 0x1000

;打印字符串:"GrapeOS loader start."
mov si,loader_start_string
mov di,160 ;屏幕第3行顯示
call func_print_string

stop:
hlt
jmp stop

;打印字符串函數
;輸入參數:ds:si,di。
;輸出參數:無。
;si 表示字符串起始地址,以0爲結束符。
;di 表示字符串在屏幕上顯示的起始位置(0~1999)
func_print_string:
mov ah,0x07 ;ah 表示字符屬性 黑底白字
shl di,1 ;乘2(屏幕上每個字符對應2個顯存字節)
.start_char: 
mov al,[si]
cmp al,0
jz .end_print
mov [gs:di],ax
inc si
add di,2
jmp .start_char
.end_print:
ret

loader_start_string:db "GrapeOS loader start.",0

二、程序演示

編譯boot和loader:

[root@CentOS7 Lesson24]# nasm boot.asm -o boot.bin
[root@CentOS7 Lesson24]# nasm loader.asm -o loader.bin

將boot寫入到虛擬硬盤的第一個扇區:

[root@CentOS7 Lesson24]# dd conv=notrunc if=boot.bin of=/media/VMShare/GrapeOS.img

運行QEMU:

C:\Users\CYJ>qemu-system-i386 d:\GrapeOS\VMShare\GrapeOS.img

運行截圖如下:

上面截圖上顯示“Loader not found.”,因爲loader.bin文件還沒有放入虛擬硬盤裏,下面我們來放入。

[root@CentOS7 Lesson24]# mount /media/VMShare/GrapeOS.img /mnt/ -t msdos -o loop
[root@CentOS7 Lesson24]# cp loader.bin /mnt/
[root@CentOS7 Lesson24]# sync
[root@CentOS7 Lesson24]# umount /mnt/

重新運行QUME,截圖如下:

上面截圖中顯示“GrapeOS loader start.”,說明已成功加載並運行loader。


視頻版地址:https://www.bilibili.com/video/BV1KV4y197sa/
配套的代碼與資料在:https://gitee.com/jackchengyujia/grapeos-course
GrapeOS操作系統交流QQ羣:643474045

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章