自己動手從零寫桌面作業系統GrapeOS系列教學——23.從硬碟讀取檔案

2023-03-25 06:01:52

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


本講程式碼檔案為boot.asm,要讀取的檔案為data.txt。

一、在FAT16檔案系統中讀取檔案的流程

在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

關於程式碼的講解基本都寫在註釋中了,結合之前講的內容,大家應該能看懂。

三、通過Linux將檔案複製到虛擬硬碟中

本講要讀取的檔案是data.txt,如何將該檔案複製到虛擬硬碟的FAT16檔案系統中呢?我們這裡採用的方法是將該虛擬硬碟掛載到Linux系統上,然後就可以將data.txt複製到虛擬硬碟中了。前提是需要先將虛擬硬碟格式化,格式化的方法就是將boot程式寫入到虛擬硬碟的第一個磁區。因為boot程式中含有FAT16的結構化資料,Linux系統就知道如何讀寫該檔案系統了。

1.將boot程式寫入到虛擬硬碟的第一個磁區

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

截圖如下:

2.將虛擬硬碟掛載到Linux系統上並將data.txt複製到虛擬硬碟中

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」,但實際上在虛擬硬碟裡儲存的檔名已經是全部大寫的了,在下面的分析中可以看到。

3.虛擬硬碟資料分析

通過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。
從上面的截圖和資料可以看到:

  1. 在根目錄區的第一個目錄項就是我們要讀的檔案,檔名已是全大寫。
  2. 從目錄項中可以看到檔案的起始簇號是0x0003。
  3. 在FAT表中第3個FAT表項的值是0x0004,表示該檔案的第二個簇號是0x0004。
  4. 在FAT表中第4個FAT表項的值是0x0005,表示該檔案的第三個簇號是0x0005。
  5. 在FAT表中第5個FAT表項的值是0xffff,表示該檔案沒有下一個簇了,到此結束。
  6. 這個檔案的內容共佔用3個簇的空間,依次是簇3、簇4、簇5,讀取該檔案就是依次將這3個簇中的資料讀取出來。

四、程式演示

在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