存储器的保护(一)——《x86汇编语言:从实模式到保护模式》读书笔记18
本文是原書第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,0xaa1.設置堆棧段和棧指針
6 ;設置堆棧段和棧指針 7 mov eax,cs 8 mov ss,eax 9 mov sp,0x7c00第7、8兩行,你可能覺得有點怪異,但是這么寫是可以的。關于原因,作者已經在書中說明了。
[bits 16] mov ds,ax ;8E D8 [bits 32] mov ds,ax ;66 8E D8mov ds,eax ;8E D8以上代碼每一行的注釋是指令編譯后產生的機器碼。
對于某些老式的編譯器,在編譯“mov ds,ax”這條指令時,16位和32位的編譯結果是不同的:在32位模式下,會添加前綴0x66(因為編譯器認為源操作數AX是16位的,所以要添加0x66以反轉默認操作數的大小)。
但是,如果添加了0x66,處理器在執行時就會多花去一個時鐘周期,這樣的指令又用得很頻繁,所以不管是16位還是32位模式,它們被設計為相同的機器指令,都是8ED8,不需要指令前綴。可是某些編譯器太固執了,它們依然會加上指令前綴0x66. 好吧,為了照顧它們,程序員想出了一個辦法,就是用這樣的形式:
mov ds,eax你別說,還真的有效,果然生成了不加前綴的8ED8!
說到這里,NASM編譯器還是非常優秀的,至少他不會那么固執。不管處理器模式怎么變化,也不管指令形式如何,以下代碼編譯后都是一個結果:
[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,這個問題就到這里,我們繼續。
2.創建GDT
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行,創建了一個代碼段的別名描述符。這樣做用意何在呢?
在保護模式下,代碼段是不可寫入的,所謂不可寫入不是說改變了內存的物理性質,使內存寫不進去,而是說通過代碼段描述符訪問對應的內存區域時,處理器不允許向里面寫數據或者更改數據。
但是,如果非要修改代碼段,有沒有辦法呢?有,那就是為該代碼段建立一個新描述符,比如說可讀可寫的數據段描述符,這樣,通過這個數據段描述符,我們就可以堂而皇之地修改代碼段了。像這樣,當兩個或以上的描述符都指向同一個段時,把另外的那些描述符就成為別名描述符。
3.棧操作時的保護
36 mov dword [ebx+0x20],0x7c00fffe 37 mov dword [ebx+0x24],0x00cf9600第36、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 (進位被丟棄)
也就是說,當前程序定義的??臻g介于物理地址0x6c00~0x7bff 之間。大小為(0x7BFF- 0x6C00 + 0x01 =0x1000 )4KB;
4.修改段寄存器時的保護
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只允許加載可讀寫的數據段。
另外,還需要注意:
- 代碼段在任何時候都是不可寫的
- 對于DS,ES,FS,GS,可以向其加載數值為0的選擇子(但是訪問時會導致異常)
- 對于CS和SS,不允許向其傳送數值為0的選擇子
(3)檢查P位
如果P=0,表示描述符指向的段并不存在于物理內存中。此時,處理器中止處理,引發異常。
如果P=1,則處理器將段描述符加載到描述符高速緩存寄存器,同時置A位(僅限于當前討論的存儲器段描述符)
?
本博文的內容就到這里。第12章余下的內容,請參考存儲器的保護(二)——《x86匯編語言:從實模式到保護模式》讀書筆記19
總結
以上是生活随笔為你收集整理的存储器的保护(一)——《x86汇编语言:从实模式到保护模式》读书笔记18的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 互联网日报 | 美团市值突破万亿港元;北
- 下一篇: VMware上的ubuntu14.04与