學習作業系統原理最好的方法是自己寫一個簡單的作業系統,
之前我們在電腦的啟動程序中介紹過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
博客主頁:http://www.cnblogs.com/chengyujia/
歡迎轉載,但請保留作者和本文鏈接,謝謝!
歡迎在下面的評論區與我交流,
如果覺得寫的不錯,請點擊下面的“推薦”按鈕,讓我更有動力寫出更好的文章,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/548177.html
標籤:其他
上一篇:Python實作簡易版TCP代理
