學習作業系統原理最好的方法是自己寫一個簡單的作業系統。
本講程式碼檔案為boot.asm,要讀取的檔案為data.txt。
在GrapeOS中用到的檔案少且小,所有檔案都放在了根目錄下,數量不會超過16個,佔用的簇不會超過254個。所以讀取目錄項只需要讀取根目錄的第1個磁區即可,讀取FAT表項也只需讀取FAT1表的第1個磁區即可。
以下是讀取檔案的流程圖:
boot.asm中的程式碼如下:
;--------------------定義常數--------------------
;FAT16目錄項中各成員的偏移量:
;名稱 偏移 長度 描述
DIR_Name equ 0 ;11 檔名8B,擴充套件名3B
DIR_Attr equ 11 ;1 目錄項屬性
;保留位 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程式載入到記憶體的地址。
FILE_ADDRESS equ 0x1000 ;檔案讀到記憶體中的地址。
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 ;每個磁區能存放目錄項的數目。
;--------------------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
;讀取檔案開始
;讀取根目錄的第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,read_file_name_string ;目標地址指向檔案在硬碟中的正確檔名。
mov cx,FILE_NAME_LENGTH ;字元比較次數為FAT16檔名長度,每比較一個字元,cx會自動減一。
repe cmpsb ;逐位元組比較ds:si和es:di指向的兩個字串。
jcxz file_found ;當cx為0時跳轉,cx為0表示上面比較的兩個字串相同。找到了檔案。
inc bx
cmp bx,DIR_ENTRY_PER_SECTOR
jl next_dir_entry ;檢查下一個目錄項。
jmp file_not_found ;沒有找到檔案。
file_found: ;找到了檔案
;從目錄項中獲取檔案的起始簇號
shl bx,5 ;乘以32
add bx,DISK_BUFFER
mov bx,[bx+DIR_FstClus] ;檔案的起始簇號
;讀取FAT1表的第1個磁區(我們用到的檔案少且小,只用到了該磁區中的簇號。)
mov esi,SECTOR_NUM_OF_FAT1_START
mov di,DISK_BUFFER ;放到boot程式之後
call func_read_one_sector
mov bp,FILE_ADDRESS ;檔案內容讀取到記憶體中的起始地址
;按簇號讀檔案內容
read_file:
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_file ;jb無符號小於則跳轉,jl有符號小於則跳轉。
read_file_finish: ;讀取檔案結束
jmp stop
file_not_found: ;沒有找到檔案
stop:
hlt
jmp stop
;讀取硬碟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
read_file_name_string:db "DATA TXT",0 ;要讀取的檔案在硬碟中儲存的檔名,共11個位元組,含空格。
times 510-($-$$) db 0
db 0x55,0xaa
關於程式碼的講解基本都寫在註釋中了,結合之前講的內容,大家應該能看懂。
本講要讀取的檔案是data.txt,如何將該檔案複製到虛擬硬碟的FAT16檔案系統中呢?我們這裡採用的方法是將該虛擬硬碟掛載到Linux系統上,然後就可以將data.txt複製到虛擬硬碟中了。前提是需要先將虛擬硬碟格式化,格式化的方法就是將boot程式寫入到虛擬硬碟的第一個磁區。因為boot程式中含有FAT16的結構化資料,Linux系統就知道如何讀寫該檔案系統了。
dd if=/dev/zero of=/media/VMShare/GrapeOS.img bs=1M count=4
nasm boot.asm -o boot.bin
dd if=boot.bin of=/media/VMShare/GrapeOS.img conv=notrunc
截圖如下:
mount /media/VMShare/GrapeOS.img /mnt/ -t msdos -o loop
ll /mnt/
cp data.txt /mnt/
sync #資料同步,立馬把資料寫入硬碟。
ll /mnt/
umount /mnt/
截圖如下:
上圖中在複製完data.txt後,通過ll /mnt/
檢視虛擬硬碟根目錄,此時雖然看到的檔名是小寫「data.txt」,但實際上在虛擬硬碟裡儲存的檔名已經是全部大寫的了,在下面的分析中可以看到。
通過hexdump檢視虛擬硬碟資料:
hexdump /media/VMShare/GrapeOS.img -C
截圖如下:
截圖中的部分資料如下:
000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.|
00000200 00 00 00 00 00 00 04 00 05 00 ff ff 00 00 00 00 |................|
00000210 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00004200 44 41 54 41 20 20 20 20 54 58 54 20 00 00 00 00 |DATA TXT ....|
00004210 00 00 00 00 00 00 fa 4e 78 56 03 00 58 04 00 00 |.......NxV..X...|
00004220 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
前面我們計算過,FAT1表的起始磁區是磁區1,位元組偏移是0x200,根目錄區的起始磁區是磁區33,位元組偏移是0x4200。
從上面的截圖和資料可以看到:
在cmd命令列中啟動QEMU的偵錯模式:
C:\Users\CYJ>qemu-system-i386 d:\GrapeOS\VMShare\GrapeOS.img -S -s
在Linux命令列中啟動GDB:
[root@CentOS7 Lesson23]# gdb
(gdb) target remote 你的Windows的IP地址:1234
(gdb) b *0x7c00
(gdb) c
(gdb) x /32xb 0x1000 #在讀檔案前檢視此時0x1000處的記憶體資料,可以看到都是0。
0x1000: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x1008: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x1010: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x1018: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
(gdb) c
執行幾秒,然後Ctrl鍵+C鍵暫停執行,此時讀取檔案的程式已執行完畢。
(gdb) x /32xb 0x1000 #在讀完檔案後檢視此時0x1000處的記憶體資料,可以看到已經不是0了。
0x1000: 0x78 0x38 0x36 0x20 0x28 0x61 0x6c 0x73
0x1008: 0x6f 0x20 0x6b 0x6e 0x6f 0x77 0x6e 0x20
0x1010: 0x61 0x73 0x20 0x38 0x30 0x78 0x38 0x36
0x1018: 0x20 0x6f 0x72 0x20 0x74 0x68 0x65 0x20
(gdb) x /32c 0x1000 #為了方便觀察可以以字元形式展示資料。通過對比,下面的32個字元的確和data.txt中前32個字元相同。
0x1000: 120 'x' 56 '8' 54 '6' 32 ' ' 40 '(' 97 'a' 108 'l' 115 's'
0x1008: 111 'o' 32 ' ' 107 'k' 110 'n' 111 'o' 119 'w' 110 'n' 32 ' '
0x1010: 97 'a' 115 's' 32 ' ' 56 '8' 48 '0' 120 'x' 56 '8' 54 '6'
0x1018: 32 ' ' 111 'o' 114 'r' 32 ' ' 116 't' 104 'h' 101 'e' 32 ' '
(gdb) x /32c 0x1440 #檢視檔案的最後二十多個字元。通過對比可以看到和data.txt中的相同。
0x1440: 105 'i' 116 't' 115 's' 32 ' ' 90 'Z' 105 'i' 108 'l' 111 'o'
0x1448: 103 'g' 32 ' ' 90 'Z' 45 '-' 56 '8' 48 '0' 32 ' ' 118 'v'
0x1450: 97 'a' 114 'r' 105 'i' 97 'a' 110 'n' 116 't' 41 ')' 46 '.'
0x1458: 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000'
通過上述演示說明讀取檔案成功。
視訊版地址:https://www.bilibili.com/video/BV1xN411K7Lc/
配套的程式碼與資料在:https://gitee.com/jackchengyujia/grapeos-course
GrapeOS作業系統交流QQ群:643474045