本文是原書第12章的學習筆記。程序員
說句題外話,這篇博文是補寫的,由於讓我誤刪了,可惡的是CSDN的回收站裏找不到! 好吧,那就再寫一遍,我有堅強的意志。司馬遷曰:「文王拘而演《周易》;仲尼厄而做《春秋》;屈原放逐,乃賦《離騷》;左丘失明,厥有《國語》;孫子臏腳,《兵法》修列;不韋遷蜀,世傳《呂覽》……」好了,不煽情了,進入正題。小程序
第12章的代碼以下。緩存
1 ;代碼清單12-1 2 ;文件名:c12_mbr.asm 3 ;文件說明:硬盤主引導扇區代碼 4 ;建立日期:2011-10-27 22:52 5 6 ;設置堆棧段和棧指針 7 mov eax,cs 8 mov ss,eax 9 mov sp,0x7c00 10 11 ;計算GDT所在的邏輯段地址 12 mov eax,[cs:pgdt+0x7c00+0x02] ;GDT的32位線性基地址 13 xor edx,edx 14 mov ebx,16 15 div ebx ;分解成16位邏輯地址 16 17 mov ds,eax ;令DS指向該段以進行操做 18 mov ebx,edx ;段內起始偏移地址 19 20 ;建立0#描述符,它是空描述符,這是處理器的要求 21 mov dword [ebx+0x00],0x00000000 22 mov dword [ebx+0x04],0x00000000 23 24 ;建立1#描述符,這是一個數據段,對應0~4GB的線性地址空間 25 mov dword [ebx+0x08],0x0000ffff ;基地址爲0,段界限爲0xfffff 26 mov dword [ebx+0x0c],0x00cf9200 ;粒度爲4KB,存儲器段描述符 27 28 ;建立保護模式下初始代碼段描述符 29 mov dword [ebx+0x10],0x7c0001ff ;基地址爲0x00007c00,512字節 30 mov dword [ebx+0x14],0x00409800 ;粒度爲1個字節,代碼段描述符 31 32 ;建立以上代碼段的別名描述符 33 mov dword [ebx+0x18],0x7c0001ff ;基地址爲0x00007c00,512字節 34 mov dword [ebx+0x1c],0x00409200 ;粒度爲1個字節,數據段描述符 35 36 mov dword [ebx+0x20],0x7c00fffe 37 mov dword [ebx+0x24],0x00cf9600 38 39 ;初始化描述符表寄存器GDTR 40 mov word [cs: pgdt+0x7c00],39 ;描述符表的界限 41 42 lgdt [cs: pgdt+0x7c00] 43 44 in al,0x92 ;南橋芯片內的端口 45 or al,0000_0010B 46 out 0x92,al ;打開A20 47 48 cli ;中斷機制還沒有工做 49 50 mov eax,cr0 51 or eax,1 52 mov cr0,eax ;設置PE位 53 54 ;如下進入保護模式... ... 55 jmp dword 0x0010:flush ;16位的描述符選擇子:32位偏移 56 57 [bits 32] 58 flush: 59 mov eax,0x0018 60 mov ds,eax 61 62 mov eax,0x0008 ;加載數據段(0..4GB)選擇子 63 mov es,eax 64 mov fs,eax 65 mov gs,eax 66 67 mov eax,0x0020 ;0000 0000 0010 0000 68 mov ss,eax 69 xor esp,esp ;ESP <- 0 70 71 mov dword [es:0x0b8000],0x072e0750 ;字符'P'、'.'及其顯示屬性 72 mov dword [es:0x0b8004],0x072e074d ;字符'M'、'.'及其顯示屬性 73 mov dword [es:0x0b8008],0x07200720 ;兩個空白字符及其顯示屬性 74 mov dword [es:0x0b800c],0x076b076f ;字符'o'、'k'及其顯示屬性 75 76 ;開始冒泡排序 77 mov ecx,pgdt-string-1 ;遍歷次數=串長度-1 78 @@1: 79 push ecx ;32位模式下的loop使用ecx 80 xor bx,bx ;32位模式下,偏移量能夠是16位,也能夠 81 @@2: ;是後面的32位 82 mov ax,[string+bx] 83 cmp ah,al ;ah中存放的是源字的高字節 84 jge @@3 85 xchg al,ah 86 mov [string+bx],ax 87 @@3: 88 inc bx 89 loop @@2 90 pop ecx 91 loop @@1 92 93 mov ecx,pgdt-string 94 xor ebx,ebx ;偏移地址是32位的狀況 95 @@4: ;32位的偏移具備更大的靈活性 96 mov ah,0x07 97 mov al,[string+ebx] 98 mov [es:0xb80a0+ebx*2],ax ;演示0~4GB尋址。 99 inc ebx 100 loop @@4 101 102 hlt 103 104;------------------------------------------------------------------------------- 105 string db 's0ke4or92xap3fv8giuzjcy5l1m7hd6bnqtw.' 106;------------------------------------------------------------------------------- 107 pgdt dw 0 108 dd 0x00007e00 ;GDT的物理地址 109;------------------------------------------------------------------------------- 110 times 510-($-$$) db 0 111 db 0x55,0xaa
6 ;設置堆棧段和棧指針 7 mov eax,cs 8 mov ss,eax 9 mov sp,0x7c00
第七、8兩行,你可能以爲有點怪異,可是這麼寫是能夠的。關於緣由,做者已經在書中說明了。oop
[bits 16] mov ds,ax ;8E D8 [bits 32] mov ds,ax ;66 8E D8 mov ds,eax ;8E D8
以上代碼每一行的註釋是指令編譯後產生的機器碼。學習
對於某些老式的編譯器,在編譯「mov ds,ax」這條指令時,16位和32位的編譯結果是不一樣的:在32位模式下,會添加前綴0x66(由於編譯器認爲源操做數AX是16位的,因此要添加0x66以反轉默認操做數的大小)。spa
可是,若是添加了0x66,處理器在執行時就會多花去一個時鐘週期,這樣的指令又用得很頻繁,因此不論是16位仍是32位模式,它們被設計爲相同的機器指令,都是8ED8,不須要指令前綴。但是某些編譯器太執拗了,它們依然會加上指令前綴0x66. 好吧,爲了照顧它們,程序員想出了一個辦法,就是用這樣的形式:.net
mov ds,eax
你別說,還真的有效,果真生成了不加前綴的8ED8!設計
說到這裏,NASM編譯器仍是很是優秀的,至少他不會那麼執拗。無論處理器模式怎麼變化,也無論指令形式如何,如下代碼編譯後都是一個結果:3d
[bits 16] mov ds,ax ;8E D8 mov ds,eax ;8E D8 [bits 32] mov ds,ax ;8E D8 mov ds,eax ;8E D8
說了這麼多,其實我就是把做者講的內容又講了一遍。無論你理解了沒有,反正我是有點糊塗了。指針
由於剛開始的這段代碼,是在16位模式下執行的,編譯也是按照16位來編譯的,因此按照16位的寫法就能夠了。如下這樣寫,簡單明瞭。
7 mov ax,cs 8 mov ss,ax
反彙編後,生成的機器碼以下:
但是,若是按照配書程序,那麼反彙編後成了:
看到了嗎,第一行多了前綴0x66,執行時會多用掉一個指令週期。
我我的認爲,寫代碼用通俗的寫法就好,能讓人看懂的代碼纔是好代碼。OK,這個問題就到這裏,咱們繼續。
11 ;計算GDT所在的邏輯段地址 12 mov eax,[cs:pgdt+0x7c00+0x02] ;GDT的32位線性基地址 13 xor edx,edx 14 mov ebx,16 15 div ebx ;分解成16位邏輯地址 16 17 mov ds,eax ;令DS指向該段以進行操做 18 mov ebx,edx ;段內起始偏移地址
106;------------------------------------------------------------------------------- 107 pgdt dw 0 108 dd 0x00007e00 ;GDT的物理地址 109;-------------------------------------------------------------------------------
第12行,就是把GDT的物理地址0x7e00傳送到EAX,至於爲何給標號pgdt加上(0x7c00+0x02),相信你已經明白了,若是不明白,看看個人圖。
第13行到15行,實際上是作除法運算,把物理地址分解爲段地址和偏移地址: EDX:EAX / 16 = EAX(獲得段地址) …EDX(獲得偏移地址)
第17到18行,DS:EBX就指向了GDT的開頭。
20 ;建立0#描述符,它是空描述符,這是處理器的要求 21 mov dword [ebx+0x00],0x00000000 22 mov dword [ebx+0x04],0x00000000 23 24 ;建立1#描述符,這是一個數據段,對應0~4GB的線性地址空間 25 mov dword [ebx+0x08],0x0000ffff ;基地址爲0,段界限爲0xfffff 26 mov dword [ebx+0x0c],0x00cf9200 ;粒度爲4KB,存儲器段描述符 27 28 ;建立2#描述符,保護模式下初始代碼段描述符 29 mov dword [ebx+0x10],0x7c0001ff ;基地址爲0x00007c00,512字節 30 mov dword [ebx+0x14],0x00409800 ;粒度爲1個字節,代碼段描述符 31 32 ;建立3#描述符,上面代碼段的別名描述符 33 mov dword [ebx+0x18],0x7c0001ff ;基地址爲0x00007c00,512字節 34 mov dword [ebx+0x1c],0x00409200 ;粒度爲1個字節,數據段描述符
第20~30行分別建立了3個描述符,相信你們都很熟悉了。須要說明的是33~34行,建立了一個代碼段的別名描述符。這樣作用意何在呢?
在保護模式下,代碼段是不可寫入的,所謂不可寫入不是說改變了內存的物理性質,使內存寫不進去,而是說經過代碼段描述符訪問對應的內存區域時,處理器不容許向裏面寫數據或者更改數據。
可是,若是非要修改代碼段,有沒有辦法呢?有,那就是爲該代碼段創建一個新描述符,好比說可讀可寫的數據段描述符,這樣,經過這個數據段描述符,咱們就能夠冠冕堂皇地修改代碼段了。像這樣,當兩個或以上的描述符都指向同一個段時,把另外的那些描述符就成爲別名描述符。
36 mov dword [ebx+0x20],0x7c00fffe 37 mov dword [ebx+0x24],0x00cf9600
第3六、37行安裝了棧段描述符。用咱們的小程序分析一下(參見數據段描述符和代碼段描述符(二)——《x86彙編語言:從實模式到保護模式》讀書筆記11),結果是:
-----------------------
seg_base = 0X7C00
seg_limit = 0XFFFFE
S = 1
DPL = 0
G = 1
D/B = 1
TYPE = 6
數據段: 向下擴展,可讀可寫
------------------------
得知,基地址是0x7c00,描述符中的界限值是0xFFFFE,G=1,是向下擴展的可讀寫數據段(通常做爲棧段)。
有效界限(effective limit)
段的有效界限取決於G標誌。
G=0:有效界限就是描述符中的界限值
G=1:有效界限 = 描述符中的段界限值* 0x1000 + 0xFFF
請牢記這個概念,由於咱們會屢次用到。
對於下擴(E=1)數據段,有效界限指定了段中最後一個不容許訪問的偏移地址。
B=0:偏移地址的有效範圍是 [有效界限+1,0xFFFF] ,爲了敘述方便,這裏用閉區間表示。
B=1:偏移地址的有效範圍是 [有效界限+1,0xFFFF_FFFF]
若是要想訪問向下擴展的棧段,那麼SP或者ESP的值必需要在偏移地址的有效範圍內。
結合本文的代碼,seg_base = 0X7C00,seg_limit = 0XFFFFE,G = 1,因而有效界限是
0xFFFFE * 0x1000 + 0xFFF = 0xFFFF_EFFF;
那麼偏移地址的有效範圍是 [ 0xFFFF_F000, 0xFFFF_FFFF]
假設ESP的初始值爲0,這時候執行 push eax, 請問合法嗎?
分析:ESP先減去4,等於0xFFFF_FFFC,而後(假如合法)EAX的值會被寫入 偏移爲 0xFFFF_FFFC~0xFFFF_FFFF的四個存儲單元,由於這些偏移值在有效範圍內,因此沒有問題。
假設ESP的初始值爲1,這時候執行push eax, 請問合法嗎?
分析:ESP先減去4,等於0xFFFF_FFFD,而後(假如合法)EAX的值會被寫入 偏移爲 0xFFFF_FFFD~0xFFFF_FFFF,0x0000_0000的四個存儲單元,由於偏移0不在有效範圍內,因此會引起異常。
在Bochs中模擬這種狀況,咱們發現CPU重啓了。
對於POP指令,也是這個道理。
假設ESP的初始值爲0xFFFF_FFFC,這時候執行 pop eax, 請問合法嗎?
分析:若是合法,那麼偏移爲 0xFFFF_FFFC~0xFFFF_FFFF的四個存儲單元中的內容會傳送到eax,以後ESP+4=0;顯然0xFFFF_FFFC~0xFFFF_FFFF是有效的偏移,因此容許執行。以下圖:
假設ESP的初始值爲0xFFFF_FFFD,這時候執行 pop eax, 請問合法嗎?
分析:若是合法,那麼偏移爲 0xFFFF_FFFD~0xFFFF_FFFF,0x0000_0000的四個存儲單元中的內容會傳送到eax,以後ESP+4=1;顯然其中0不是有效的偏移,因此不容許執行。以下圖:
再回到咱們的代碼,由於ESP僅提供偏移地址,真正的物理地址 = 偏移地址 + 段基地址;因此,對於本代碼中的棧,結合段基地址= 0x7c00,有效偏移地址= [ 0xFFFF_F000, 0xFFFF_FFFF],因此
最低端有效物理地址 = 0x7c00 + 0xFFFF_F000 = 0x6c00(進位被丟棄)
最高端有效物理地址 = 0x7c00 + 0xFFFF_FFFF = 0x7BFF (進位被丟棄)
也就是說,當前程序定義的棧空間介於物理地址0x6c00~0x7bff 之間。大小爲(0x7BFF- 0x6C00 + 0x01 =0x1000 )4KB;
54 ;如下進入保護模式... ... 55 jmp dword 0x0010:flush ;16位的描述符選擇子:32位偏移
57 [bits 32] 58 flush: 59 mov eax,0x0018 60 mov ds,eax 61 62 mov eax,0x0008 ;加載數據段(0..4GB)選擇子 63 mov es,eax 64 mov fs,eax 65 mov gs,eax 66 67 mov eax,0x0020 ;0000 0000 0010 0000 68 mov ss,eax 69 xor esp,esp ;ESP <- 0
第55行,這條指令會隱式地修改CS;一樣,會修改寄存器的指令還出如今58~68行(粗體部分)。
以上的指令涉及全部的段寄存器,當這些指令執行時,處理器把指令中給出的選擇子傳送到段寄存器的選擇器部分(就是16位可見部分)。可是,處理器的固件在完成傳送以前,會進行以下檢查:
(1)檢查索引號
要求:段選擇子中的描述符索引 * 8 + 7 <= GDT(或LDT)的界限值
若是不符合要求,則產生異常13,同時段寄存器中的原值不變。
(2)檢查描述符的類別
原書表12-1,我在這裏繪製一份。
Y:表示容許
N:表示不容許
舉例:SS只容許加載可讀寫的數據段。
另外,還須要注意:
(3)檢查P位
若是P=0,表示描述符指向的段並不存在於物理內存中。此時,處理器停止處理,引起異常。
若是P=1,則處理器將段描述符加載到描述符高速緩存寄存器,同時置A位(僅限於當前討論的存儲器段描述符)
本博文的內容就到這裏。第12章餘下的內容,請參考存儲器的保護(二)——《x86彙編語言:從實模式到保護模式》讀書筆記19