學習操作系統原理最好的方法是自己寫一個簡單的操作系統。
本講代碼文件爲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。
從上面的截圖和數據可以看到:
- 在根目錄區的第一個目錄項就是我們要讀的文件,文件名已是全大寫。
- 從目錄項中可以看到文件的起始簇號是0x0003。
- 在FAT表中第3個FAT表項的值是0x0004,表示該文件的第二個簇號是0x0004。
- 在FAT表中第4個FAT表項的值是0x0005,表示該文件的第三個簇號是0x0005。
- 在FAT表中第5個FAT表項的值是0xffff,表示該文件沒有下一個簇了,到此結束。
- 這個文件的內容共佔用3個簇的空間,依次是簇3、簇4、簇5,讀取該文件就是依次將這3個簇中的數據讀取出來。
四、程序演示
在cmd命令行中啓動QEMU的調試模式:
C:\Users\CYJ>qemu-system-i386 d:\GrapeOS\VMShare\GrapeOS.img -S -s
在Linux命令行中啓動GDB:
[[email protected] 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