之前已經做了一些理論上的鋪墊,這次我們就可以看代碼了。
# 一、代碼清單
~~~
;代碼清單11-1
;文件名:c11_mbr.asm
;文件說明:硬盤主引導扇區代碼
;創建日期:2011-5-16 19:54
;設置堆棧段和棧指針
mov ax,cs
mov ss,ax
mov sp,0x7c00
;計算GDT所在的邏輯段地址
mov ax,[cs:gdt_base+0x7c00] ;低16位
mov dx,[cs:gdt_base+0x7c00+0x02] ;高16位
mov bx,16
div bx
mov ds,ax ;令DS指向該段以進行操作
mov bx,dx ;段內起始偏移地址
;創建0#描述符,它是空描述符,這是處理器的要求
mov dword [bx+0x00],0x00
mov dword [bx+0x04],0x00
;創建#1描述符,保護模式下的代碼段描述符
mov dword [bx+0x08],0x7c0001ff
mov dword [bx+0x0c],0x00409800
;創建#2描述符,保護模式下的數據段描述符(文本模式下的顯示緩沖區)
mov dword [bx+0x10],0x8000ffff
mov dword [bx+0x14],0x0040920b
;創建#3描述符,保護模式下的堆棧段描述符
mov dword [bx+0x18],0x00007a00
mov dword [bx+0x1c],0x00409600
;初始化描述符表寄存器GDTR
mov word [cs: gdt_size+0x7c00],31 ;描述符表的界限(總字節數減一)
lgdt [cs: gdt_size+0x7c00]
in al,0x92 ;南橋芯片內的端口
or al,0000_0010B
out 0x92,al ;打開A20
cli ;保護模式下中斷機制尚未建立,應
;禁止中斷
mov eax,cr0
or eax,1
mov cr0,eax ;設置PE位
;以下進入保護模式... ...
jmp dword 0x0008:flush ;16位的描述符選擇子:32位偏移
;清流水線并串行化處理器
[bits 32]
flush:
mov cx,00000000000_10_000B ;加載數據段選擇子(0x10)
mov ds,cx
;以下在屏幕上顯示"Protect mode OK."
mov byte [0x00],'P'
mov byte [0x02],'r'
mov byte [0x04],'o'
mov byte [0x06],'t'
mov byte [0x08],'e'
mov byte [0x0a],'c'
mov byte [0x0c],'t'
mov byte [0x0e],' '
mov byte [0x10],'m'
mov byte [0x12],'o'
mov byte [0x14],'d'
mov byte [0x16],'e'
mov byte [0x18],' '
mov byte [0x1a],'O'
mov byte [0x1c],'K'
;以下用簡單的示例來幫助闡述32位保護模式下的堆棧操作
mov cx,00000000000_11_000B ;加載堆棧段選擇子
mov ss,cx
mov esp,0x7c00
mov ebp,esp ;保存堆棧指針
push byte '.' ;壓入立即數(字節)
sub ebp,4
cmp ebp,esp ;判斷壓入立即數時,ESP是否減4
jnz ghalt
pop eax
mov [0x1e],al ;顯示句點
ghalt:
hlt ;已經禁止中斷,將不會被喚醒
;-------------------------------------------------------------------------------
gdt_size dw 0
gdt_base dd 0x00007e00 ;GDT的物理地址
times 510-($-$$) db 0
db 0x55,0xaa
~~~
上面就是配書源碼。我們一點一點看。
# 二、源碼分析
### (一)設置堆棧和棧指針
~~~
;設置堆棧段和棧指針
mov ax,cs
mov ss,ax
mov sp,0x7c00
~~~
這個沒有什么好說的,就是初始化棧。這三行執行后,SS=0; SP=0x7c00;
需要注意的是,這樣設置后,棧的區域從0x0000_7c00向下擴展(不含0x0000_7c00這個字節),該區域包含了很多BIOS數據,包括實模式下的中斷向量表,所以一定要小心。
### (二)安裝段描述符
~~~
;計算GDT所在的邏輯段地址
mov ax,[cs:gdt_base+0x7c00] ;低16位
mov dx,[cs:gdt_base+0x7c00+0x02] ;高16位
mov bx,16
div bx
mov ds,ax ;令DS指向該段以進行操作
mov bx,dx ;段內起始偏移地址
~~~
怎么理解這段代碼呢?
首先,在代碼清單的95、96行,有
~~~
gdt_size dw 0
gdt_base dd 0x00007e00 ;GDT的物理地址
~~~
作者在這里聲明了標號gdt_base,還初始化了一個雙字——0x0000_7e00; 作者的意圖是從這個地方開始建立全局描述符表GDT。我們的程序就是一個引導扇區,占用了512(=0x200)字節。程序加載的物理地址是0x7c00, 0x7c00+0x200 = 0x7e00. 可見,在物理地址的安排上,引導程序后面緊跟著就是GDT。
目前我們還是處在實模式下,所以要建立GDT,必須將GDT的線性地址(物理地址)轉換成實模式下使用的“段地址:偏移地址”的形式。
mov ax,[cs:gdt_base+0x7c00] ;
這句使了段超越前綴“cs”,表明訪問代碼段中的數據;因為CS=0,所以就把物理地址(0x7c00+gdt_base)處的0x7e00傳送給了ax; 同樣地,將0x0000傳送給dx; 為了把線性地址轉換成邏輯地址,我們用DX:AX除以16,得到的商(AX)就是段地址,余數(DX)就是偏移地址。
mov bx,16???????
??????? div bx???????????
??????? mov ds,ax????????????????????????? ;令DS指向該段以進行操作
??????? mov bx,dx????????????????????????? ;段內起始偏移地址
這幾行執行之后,GDT的邏輯地址就是 DS:BX.
~~~
;創建0#描述符,它是空描述符,這是處理器的要求
mov dword [bx+0x00],0x00
mov dword [bx+0x04],0x00
~~~
**處理器規定,GDT中的第一個描述符必須是空描述符。**這是什么原因呢?因為很多時候,寄存器和內存單元的初始值都會為0,再加上程序設計有問題,就會在無意中用全0的索引來選擇描述符,這當然是不好的。因此,處理器要求將第一個描述符定義成空描述符。所以,上面兩行代碼定義了一個空描述符。
~~~
;創建#1描述符,保護模式下的代碼段描述符
mov dword [bx+0x08],0x7c0001ff
mov dword [bx+0x0c],0x00409800
~~~
這兩行用來創建第二個描述符。之前的博文我們已經掌握了數據段和代碼段描述符的格式,所以對這個描述符就不難理解了。
還記得我上一篇博文中寫了一個小程序嗎?[http://blog.csdn.net/longintchar/article/details/50507218](http://blog.csdn.net/longintchar/article/details/50507218)
趕緊用它來分析一下吧:
[](http://img.blog.csdn.net/20160113223951465)
線性基地址:0x0000_7c00
段界限為0x001FF,因為G=0,所以該段的長度是512(2的9次方)字節;
特權級:0
其他字段就不逐個說明了,相信你一定能懂。很明顯,這個描述符定義的段,就是主引導程序所在的區域。
接著看代碼。
~~~
;創建#2描述符,保護模式下的數據段描述符(文本模式下的顯示緩沖區)
mov dword [bx+0x10],0x8000ffff
mov dword [bx+0x14],0x0040920b
~~~
程序分析的結果是:
seg_base = 0XB8000
seg_limit = 0XFFFF
S = 1
DPL = 0
G = 0
D/B = 1
TYPE = 2
數據段: 可讀可寫
看來這個段是指向顯存的。
~~~
;創建#3描述符,保護模式下的堆棧段描述符
mov dword [bx+0x18],0x00007a00
mov dword [bx+0x1c],0x00409600
~~~
這是創建棧段的描述符。程序分析的結果是:
-----------------------
seg_base = 0
seg_limit = 0X7A00
S = 1
DPL = 0
G = 0
D/B = 1
TYPE = 6
數據段: 向下擴展,可讀可寫
------------------------
正如作者所說:段界限的值0x7a00加上1(0x7a01),就是ESP寄存器所允許的最小值。當執行隱式的棧操作(如PUSH、CALL)時,處理器會檢查ESP的值,一旦發現它小于0x7a01,就會引發異常中斷。如果你還不理解,那么可以把書翻到215頁。作者說在棧操作時,必須符合以下規則:
**實際使用的段界限+1 <= (ESP的內容減操作數的長度) <= 0xFFFF_FFFF**
就拿這個例子來說,因為G=0,所以段界限就是0x7a00. 假設現在ESP的內容是0x7a04,此時執行下面的指令:
push edx
因為壓入的是雙字,所以處理器會先將ESP的值減去4,于是ESP=0x7a00. 因為0x7a00小于0x7a01,因此會引發異常中斷。
### (三)LGDT指令
好了,現在描述符已經安裝完畢,接下來的工作是加載描述符表的線性基地址和界限到GDTR寄存器。相關的指令是lgdt. 該指令的格式為:
lgdt m48
也就是說,該指令的操作數內存操作數。注意,該指令在實模式和保護模式下都可以執行,也不影響任何標志位。
這個內存操作數指向一個6字節的內存區域,要求低16位是GDT的界限值(表的總字節數減去1),高32位是GDT的線性基地址。
~~~
gdt_size dw 0
gdt_base dd 0x00007e00 ;GDT的物理地址
~~~
還記得嗎,這是代碼中事先定義了6字節的空間。前兩個字節就是為了保存GDT的界限值。
~~~
;初始化描述符表寄存器GDTR
mov word [cs: gdt_size+0x7c00],31 ;描述符表的界限(總字節數減一)
lgdt [cs: gdt_size+0x7c00]
~~~
第一句寫入界限值,第二句把6字節加載到GDTR寄存器。
注意,到目前為止,我們依然在實模式下。
### (四)關于A20
### 1.A20 GATE 起源[1]
在8086/8088中,只有20根地址線,所以可以訪問的地址是2^20=1M。但由于8086/8088是16位地址模式,能夠表示的地址范圍是0-64K,所以為了訪問1M內存,Intel采取了分段的模式。
即:物理地址=16位段地址*16 + 16位偏移
但這種方式引起了新的問題,通過上述分段模式,能夠表示的最大內存為:FFFFh:FFFFh=FFFF0h+FFFFh=10FFEFh
但8086/8088只有20位地址線,所以當訪問100000h~10FFEFh之間的內存時,系統并不認為訪問越界而產生異常,而是自動從重新0開始計算,也就是說系統計算實際地址的時候是按照對1M求模的方式進行的,這種技術被稱為wrap-around(回繞)。
到了80286,系統的地址總線發展為24根,這樣能夠訪問的內存可以達到2^24=16M。為了兼容,Intel在設計80286時提出的目標是:在實模式下,系統所表現的行為應該和8086/8088所表現的完全一樣。但最終,80286芯片卻存在一個BUG:如果程序員訪問100000H~10FFEFH之間的內存,系統將實際訪問這塊內存,而不是象過去一樣重新從0開始。
為了解決上述問題,IBM使用鍵盤控制器上剩余的一些輸出線來管理第21根地址線(從0開始數是第20根),被稱為A20Gate;如果A20 Gate打開,則當程序員給出100000H~10FFEFH之間的地址的時候,系統將真正訪問這塊內存區域;如果A20Gate被禁止,則當程序員給出100000H~10FFEFH之間的地址的時候,系統仍然使用8086/8088的方式。絕大多數IBM PC兼容機默認的A20Gate是被禁止的。由于在當時沒有更好的方法來解決這個問題,所以IBM使用了鍵盤控制器來操作A20 Gate,但是這種操作太麻煩了,要使用一大堆指令。
### 2.Alt_A20_GATE
Alt_A20_GATE ,又稱Fast A20. 通過端口0x92的bit1來打開A20,具體方法是:先從端口讀出原數據,接著將bit1置1,然后再寫入該端口,這樣就打開了A20.
正如代碼所示
~~~
in al,0x92 ;南橋芯片內的端口
or al,0000_0010B
out 0x92,al ;打開A20
~~~
?
一次學太多會不會覺得累呢?我們就說到這里,下次繼續…
?
【參考資料】
[1] 如煙海的專欄. [http://blog.csdn.net/ruyanhai/article/details/7181842](http://blog.csdn.net/ruyanhai/article/details/7181842 "http://blog.csdn.net/ruyanhai/article/details/7181842")