Windows下寫作業系統---實踐(4)改造MBR,載入硬碟扇區
前面已經用MBR(Main Boot Record)顯示了字串,證明了位於0柱面0磁頭1扇區的512位元組的程式碼被BIOS成功匯入了:0x0000:0x7C00,並執行成功。
但MBR始終空間有限,只有512位元組,別說執行核心了,就是執行一個稍微大一點的程式都做不到,那MBR得實現一個類似於BIOS的功能,從硬碟別的扇區將程式碼載入進記憶體,然後JMP過去執行。
我們初步設計:BIOS----->MBR----->LOAD----->核心(kernel)
這個MBR 的目的很簡單,我們讀硬碟的 第三個扇區,其實你想讀第幾個扇區都沒問題,主要是鄭鋼老師說他覺得這樣安全,那就按鄭老師說的辦,我們會將LOAD的程式碼放進第三個扇區裡,LOAD的主要是用來實現真實模式進入保護模式要做的很多工作,比如建立GDT(全域性描述符表)、建立記憶體對映(線性地址和實體地址的對映)等等,這裡涉及到了保護模式的知識,咱們還是遵照用到哪學到哪的想法,先來完善MBR。
既然要讀硬碟,那有必要來了解一下硬碟的相關知識。
CPU要和外部硬體交朋友打波波,需要一個I/O介面,這個I/O介面可以是一個小晶片也可以是一個電路板,這取決於多複雜,你可以理解I/O介面就是一個翻譯器,CPU講中文,外部硬體(音效卡,顯示卡,硬碟等)講英文,兩邊交朋友需要通過一個翻譯器才能交流起來。
想想我們會碰到幾個問題:
1。CPU不可能直接和硬體相連,硬體總是日新月異的,只會越來越多,不可能將所有的硬體裝置和CPU連線起來。
2。CPU只能同一時間和一個小朋友聊天,這麼多硬體同時和CPU說話,CPU不知道該聽誰的回答誰的。
所以引入了匯流排結構(比如一個公共道路,排著隊上去),然後ICH晶片(輸入輸出控制裝置集中器),主要是連線不同的匯流排,協調各個I/O介面對CPU的訪問,但有很多硬體還沒有開發出來,或者將要開發出來,為了方便,所以弄了個PCI介面,如果有什麼新的硬體開發出來,直接插入PCI擴充套件槽。
其實細說來說,CPU是通過埠(PORT)來和外圍的小夥伴交朋友的,埠就是一堆的暫存器,位於I/O介面的電路中。
每一個I/O介面都有可能有幾個埠,分別用於不同的目的.
例如:連線硬碟的就有好幾個埠,分別是命令埠0x20,表明從硬碟讀資料,寫入埠0x30,表明向硬碟寫資料,狀態埠(硬碟工作是否正常,操作是否成功,發生了哪些錯誤),引數埠(讀寫的扇區數量、LBA的邏輯地址)、資料埠
每個埠都有自己的資料寬度,在早期的系統中,有8位的,有16位的,現在有些埠還有32位的,那到底是8位還是16位,這是那些裝置和I/O製造者的自由。
ICH集成了兩個PATA/SATA介面,分別是主硬碟介面和從硬碟介面。
主硬碟介面的埠:0x1F0----0x1F7
從硬碟介面的埠:0x170----0x177
在Intel系統中,只允許存在65536個埠,埠號是:0x0000-0xFFFF,因為是獨立定址,所以不能使用MOV指令,取而代之的是in和out命令。
in ax,dx
in al,dx
目的運算元只能使用暫存器al[8位],ax【16位】,源運算元dx。從外部硬體讀取資料。
out dx,al
out dx,ax
目的運算元dx,源運算元al或者ax,CPU通過埠向外部裝置傳送訊息。
終於要進入正題了,開始講講硬碟了。
1。我們要了解讀寫硬碟的基本單位是扇區,也就是說每次讀一個扇區或者寫一扇區。
2。從硬碟讀寫資料最原始的辦法,是採用CHS模式,向硬碟控制器傳送柱面、磁頭、扇區號,太麻煩了,我不關心實體地址的換算,我只想0代表0號扇區,10000代表10000扇區,不要讓我去換算,你們內部搞定,那我們就採用LBA邏輯扇區,切記他是從0號開始的,CHS是從1號開始的,LBA的0號扇區就是CHS的1號扇區,到時候別搞錯了。
3。最早的LBA編址方法是LBA28,也就是2的28次方個扇區,每個扇區是512位元組,換算後可以管理128GB的硬碟,現在用的比較多的是LBA48,換算後,可以管理131072TB硬碟。
目前我們總共分配也才分100M的硬碟映像,所以用LBA28完全足夠了。
我們先設定讀入的扇區號,還有寫進記憶體的地址[下面的兩個常量會放進boot.inc裡,然後在mbr.s最開始的地方引用
[%include "boot.inc"]。
mov eax,LOAD_READ_SECTION
mov bx,LOAD_BASE_ADDRESS
mov cx,1 ;要讀取的扇區數
CALL Read_Disk_Section
jmp 0x900
這個要放進的記憶體地址LOAD_BASE_ADDRESS只要是在實踐二【真實模式佈局圖】裡的註明的可用區域都行,我就選了個0x900,沒有為什麼,看個人喜好。
1。設定要讀取的扇區數,通過上圖,我們知道暫存器是0x1F2,8位使用AL
Read_Disk_Section:
mov esi,eax ;儲存,後面有用
mov di,cx ;儲存,後面有用
mov dx,0x1F2
mov al,cl
out dx,al ;將要讀取的扇區數寫入硬碟控制器,如果al為0,那是代表要讀入256個扇區進去
2。設定LBA的扇區號,因為我們使用的是LBA28位,所以需要設定28位。
看上圖:0x1F3 LBA Low 這是低8位0----7
0x1F4 LBA Mid 這是中8位8---15
0x1F5 LBA High 這是高8位 16---23
那還差四位放哪裡?這時候就需要看一下0x1F6的圖了:
0x1F6 高4位放控制資訊,低4位放LBA的扇區號的最高4位
所以程式如下:
mov eax,esi ;恢復eax
mov dx,0x1F3
out dx,al ;低8位放入硬碟控制器的0x1F3埠
mov cl,8
shr eax,cl
mov dx,0x1f4
out dx,al ;中8位
shr eax,cl
mov dx,0x1f5
out dx,al ;高8位
shr eax,cl
mov dx,0x1f6
and al,0x0f
or al,0xe0
out dx,al ;最後
3。向埠0x1F7寫入0x20讀狀態,請求讀硬碟,也是8位,不過他會先反饋你一個狀態,到底是硬碟是忙還是不忙,如果不忙了有空閒了,你才能接著操作,如圖:
所以程式碼如下:
mov dx,0x1F7
mov al,0x20
out dx,al
這條要讀硬碟的程式碼執行後,硬碟就會忙開了,你需要迴圈去讀他的返回的狀態[0x1F7不僅是讀寫埠,也是狀態埠],是否硬碟已經忙完,準備就緒了,所以程式碼如下:
loop_Read_Disk_Stats:
nop ;可寫可不寫,給CPU一點緩衝的時間
in al,dx
and al,0x88
cmp al,0x08
jnz loop_Read_Disk_Stats
4。最後就是連續取出資料了,需要使用0x1F0的埠。
mov dx,256 ;我們打算以字的形式來取,一個扇區是512位元組,換算成字就是256
mov ax,di ;ax是幾個扇區
mul dx ;ax得到總共有多少個字
mov cx,ax
mov dx,0x1F0
;------------------------
loop_Readin_Mem:
in ax,dx
mov [bx],ax
add bx,2
loop loop_Readin_Mem
ret
完整程式碼如下:
%include "boot.inc" ;%include nasm的預處理指令,意思是讓編譯器在編譯之前,把"boot.inc"檔案包含進來
section MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov fs,ax
mov ss,ax
mov sp,0x7c00
;中斷清屏
mov ax,0x0600
mov cx,0x0
mov bx,0x300
mov dx,0x184f
int 0x10
;顯示文字
mov ax,message
mov bp,ax
mov cx,11
mov dx,0
mov bx,0x2
mov ax,0x1301
int 0x10
;操作視訊記憶體來顯示文字
mov ax,0xb800
mov gs,ax
mov byte[gs:0xa0],'F'
mov byte[gs:0xa1],0x2
mov byte[gs:0xa2],'l'
mov byte[gs:0xa3],0x2
mov byte[gs:0xa4],'y'
mov byte[gs:0xa5],0x2
mov byte[gs:0xa6],'.'
mov byte[gs:0xa7],0x2
mov byte[gs:0xa8],'.'
mov byte[gs:0xa9],0x2
mov byte[gs:0xaa],'.'
mov byte[gs:0xab],0x2
mov eax,LOAD_READ_SECTION
mov bx,LOAD_BASE_ADDRESS
mov cx,1
call Read_Disk_Section
jmp 0x900
;-------載入硬碟
;設定要讀取的扇區數
Read_Disk_Section:
mov esi,eax
mov di,cx
mov dx,0x1f2
mov al,cl
out dx,al
;--------------------------
mov eax,esi
mov dx,0x1f3
out dx,al
mov cl,8
shr eax,cl
mov dx,0x1f4
out dx,al
shr eax,cl
mov dx,0x1f5
out dx,al
shr eax,cl
and al,0x0f
or al,0xe0
mov dx,0x1f6
out dx,al
;----------------------------------
mov dx,0x1f7
mov al,0x20
out dx,al
loop_Read_Disk_Stats:
nop
in al,dx
and al,0x88
cmp al,0x08
jnz loop_Read_Disk_Stats
;--------------------------------
mov dx,256
mov ax,di
mul dx
mov cx,ax
mov dx,0x1F0
loop_Readin_Mem:
in ax,dx
mov [bx],ax
add bx,2
loop loop_Readin_Mem
ret
message db "QQ:22093636"
times 510-($-$$) db 0
db 0x55,0xaa
然後nasm -I include/ mbr.bin mbr.s 我把boot.inc建到了include資料夾下面了,-I就是告訴編譯器有一個庫檔案,可以在那裡面找找。
再 dd if=\目錄\mbr.bin of=\目錄\c.img bs=512 count=1 conv=notrunc
boot.inc的內容為:
LOAD_READ_SECTION equ 0x2
LOAD_BASE_ADDRESS equ 0x900
那好了MBR,我們還要建一個測試的LOAD,要不然怎麼知道載入成功了沒有,我們在CODE下再vim load.s,程式碼如下:
SECTION LOAD vstart=0x900
mov ax,loadmessage
mov bp,ax
mov cx,11
mov dx,0x0300
mov bx,0x2
mov ax,0x1301
int 0x10
jmp $
loadmessage db "loadmessage"
nasm -o load.bin load.s
dd if=\目錄\load.bin of=\目錄\c.img bs=512 count=1 seek=2 conv=notrunc
這個seek是跳過的意思,跳過兩個扇區,我們的物理扇區是從1開始數的,跳過兩個就是寫入第三個扇區,而LBA是邏輯扇區是從0開始數的,所以裡面的扇區號是2。
最後顯示成功圖:
大功告成。