1. 程式人生 > >Windows下寫作業系統---實踐(4)改造MBR,載入硬碟扇區

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。

最後顯示成功圖:

大功告成。