13 從保護模式返回真實模式
參考
https://www.cnblogs.com/wanmeishenghuo/tag/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/
https://blog.51cto.com/13475106/category6.html
前幾節課我們演示了從真實模式進入到保護模式,那麼從保護模式返回到真實模式具體怎麼操作呢?
先將上一節的程式列出:
%include "inc.asm" org 0x9000 jmp CODE16_SEGMENT [section .gdt] ; GDT definition ; 段基址, 段界限, 段屬性 GDT_ENTRY : Descriptor 0, 0, 0 CODE32_DESC : Descriptor 0, Code32SegLen - 1, DA_C + DA_32 VIDEO_DESC : Descriptor 0xB8000, 0x07FFF, DA_DRWA + DA_32 DATA32_DESC : Descriptor 0, Data32SegLen - 1, DA_DR + DA_32 STACK_DESC : Descriptor 0, TopOfStackInit, DA_DRW + DA_32 ; GDT end GdtLen equ $ - GDT_ENTRY GdtPtr: dw GdtLen - 1 dd 0 ; GDT Selector Code32Selector equ (0x0001 << 3) + SA_TIG + SA_RPL0 VideoSelector equ (0x0002 << 3) + SA_TIG + SA_RPL0 Data32Selector equ (0x0003 << 3) + SA_TIG + SA_RPL0 StackSelector equ (0x0004 << 3) + SA_TIG + SA_RPL0 ; end of [section .gdt] TopOfStackInit equ 0x7c00 [section .dat] [bits 32] DATA32_SEGMENT: DTOS db "D.T.OS!", 0 DTOS_OFFSET equ DTOS - $$ HELLO_WORLD db "Hello World!", 0 HELLO_WORLD_OFFSET equ HELLO_WORLD - $$ Data32SegLen equ $ - DATA32_SEGMENT [section .s16] [bits 16] CODE16_SEGMENT: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, TopOfStackInit ; initialize GDT for 32 bits code segment mov esi, CODE32_SEGMENT mov edi, CODE32_DESC call InitDescItem mov esi, DATA32_SEGMENT mov edi, DATA32_DESC call InitDescItem ; initialize GDT pointer struct mov eax, 0 mov ax, ds shl eax, 4 add eax, GDT_ENTRY mov dword [GdtPtr + 2], eax ; 1. load GDT lgdt [GdtPtr] ; 2. close interrupt cli ; 3. open A20 in al, 0x92 or al, 00000010b out 0x92, al ; 4. enter protect mode mov eax, cr0 or eax, 0x01 mov cr0, eax ; 5. jump to 32 bits code jmp dword Code32Selector : 0 ; esi --> code segment label ; edi --> descriptor label InitDescItem: push eax mov eax, 0 mov ax, cs shl eax, 4 add eax, esi mov word [edi + 2], ax shr eax, 16 mov byte [edi + 4], al mov byte [edi + 7], ah pop eax ret [section .s32] [bits 32] CODE32_SEGMENT: mov ax, VideoSelector mov gs, ax mov ax, StackSelector mov ss, ax mov ax, Data32Selector mov ds, ax mov ebp, DTOS_OFFSET mov bx, 0x0C mov dh, 12 mov dl, 33 call PrintString mov ebp, HELLO_WORLD_OFFSET mov bx, 0x0C mov dh, 13 mov dl, 30 call PrintString jmp $ ; ds:ebp --> string address ; bx --> attribute ; dx --> dh : row, dl : col PrintString: push ebp push eax push edi push cx push dx print: mov cl, [ds:ebp] cmp cl, 0 je end mov eax, 80 mul dh add al, dl shl eax, 1 mov edi, eax mov ah, bl mov al, cl mov [gs:edi], ax inc ebp inc dl jmp print end: pop dx pop cx pop edi pop eax pop ebp ret Code32SegLen equ $ - CODE32_SEGMENT
上一節中,我們跳到32位保護模式後,並沒有設定棧頂指標esp,但是程式依然可以正常執行,這時怎麼回事呢?原因是我們在第52行設定了棧頂指標,而我們的程式中,16位的真實模式和32位的保護模式使用的棧是一樣的,因此,無需重新設定程式也可以正常執行。第14行的段描述符描述了32位保護模式下的棧的資訊,在保護模式下即使我們將這個段的選擇子,賦值給ss,那麼由於段基址是0,得到最終的棧頂指標依然是 段基址+esp=0+esp,所以不給ss賦值和給ss賦值的結果是一樣的。如果在32位保護時使用的棧和16位真實模式使用的棧不一樣的話,就不能這樣操作了,而必須在進入32位保護模式後設置ss段寄存和esp棧頂指標。
保護模式下的棧段,我們一般要進行以下步驟的設定:
1、指定一段空間,併為其定義段描述符
2、根據段描述表中的位置定義段選擇子
3、初始化棧段暫存器(ss <- StackSelector)
4、初始化棧頂指標(esp <- TopOfStack )
下面定義32位保護模式下的專用棧:
%include "inc.asm" org 0x9000 jmp CODE16_SEGMENT [section .gdt] ; GDT definition ; 段基址, 段界限, 段屬性 GDT_ENTRY : Descriptor 0, 0, 0 CODE32_DESC : Descriptor 0, Code32SegLen - 1, DA_C + DA_32 VIDEO_DESC : Descriptor 0xB8000, 0x07FFF, DA_DRWA + DA_32 DATA32_DESC : Descriptor 0, Data32SegLen - 1, DA_DR + DA_32 STACK32_DESC : Descriptor 0, TopOfStack32, DA_DRW + DA_32 ; GDT end GdtLen equ $ - GDT_ENTRY GdtPtr: dw GdtLen - 1 dd 0 ; GDT Selector Code32Selector equ (0x0001 << 3) + SA_TIG + SA_RPL0 VideoSelector equ (0x0002 << 3) + SA_TIG + SA_RPL0 Data32Selector equ (0x0003 << 3) + SA_TIG + SA_RPL0 Stack32Selector equ (0x0004 << 3) + SA_TIG + SA_RPL0 ; end of [section .gdt] TopOfStack16 equ 0x7c00 [section .dat] [bits 32] DATA32_SEGMENT: DTOS db "D.T.OS!", 0 DTOS_OFFSET equ DTOS - $$ HELLO_WORLD db "Hello World!", 0 HELLO_WORLD_OFFSET equ HELLO_WORLD - $$ Data32SegLen equ $ - DATA32_SEGMENT [section .s16] [bits 16] CODE16_SEGMENT: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, TopOfStack16 ; initialize GDT for 32 bits code segment mov esi, CODE32_SEGMENT mov edi, CODE32_DESC call InitDescItem mov esi, DATA32_SEGMENT mov edi, DATA32_DESC call InitDescItem mov esi, DATA32_SEGMENT mov edi, STACK32_DESC call InitDescItem ; initialize GDT pointer struct mov eax, 0 mov ax, ds shl eax, 4 add eax, GDT_ENTRY mov dword [GdtPtr + 2], eax ; 1. load GDT lgdt [GdtPtr] ; 2. close interrupt cli ; 3. open A20 in al, 0x92 or al, 00000010b out 0x92, al ; 4. enter protect mode mov eax, cr0 or eax, 0x01 mov cr0, eax ; 5. jump to 32 bits code jmp dword Code32Selector : 0 ; esi --> code segment label ; edi --> descriptor label InitDescItem: push eax mov eax, 0 mov ax, cs shl eax, 4 add eax, esi mov word [edi + 2], ax shr eax, 16 mov byte [edi + 4], al mov byte [edi + 7], ah pop eax ret [section .s32] [bits 32] CODE32_SEGMENT: mov ax, VideoSelector mov gs, ax mov ax, Stack32Selector mov ss, ax mov eax, TopOfStack32 mov esp, eax mov ax, Data32Selector mov ds, ax mov ebp, DTOS_OFFSET mov bx, 0x0C mov dh, 12 mov dl, 33 call PrintString mov ebp, HELLO_WORLD_OFFSET mov bx, 0x0C mov dh, 13 mov dl, 30 call PrintString jmp $ ; ds:ebp --> string address ; bx --> attribute ; dx --> dh : row, dl : col PrintString: push ebp push eax push edi push cx push dx print: mov cl, [ds:ebp] cmp cl, 0 je end mov eax, 80 mul dh add al, dl shl eax, 1 mov edi, eax mov ah, bl mov al, cl mov [gs:edi], ax inc ebp inc dl jmp print end: pop dx pop cx pop edi pop eax pop ebp ret Code32SegLen equ $ - CODE32_SEGMENT [section .gs] [bits 32] STACK32_SEGMENT: times 1024 * 4 db 0 Stack32SegLen equ $ - STACK32_SEGMENT TopOfStack32 equ Stack32SegLen - 1
184-190行我們重新定義了32位保護模式下的棧段。並在14行和19行為其填充了段描述符表項和段選擇子。我們在94行打上斷點,看看程式執行到這裡時棧頂指標暫存器的值是多少。啟動bochs開始執行,結果如下:
可以看到這時的esp是0x7c00。
122-126行,我們在32位保護模式中設定了棧的段基址和棧頂指標。繼續單步執行程式,如下:
圖中可以看出,我們將段選擇子賦值給了ss,將棧的段界限賦值給了esp,因為棧是向下生長的,所以就應該將段界限賦值給esp。
繼續執行程式,最終結果如下:
從保護模式返回時模式:
8086中的一個神祕限制:
無法直接從32位程式碼段回到真實模式
只能從16位程式碼段間接返回真實模式
在返回前必須用合適的選擇子對段暫存器賦值
可以從16位真實模式程式碼段跳到32位保護模式程式碼段,但是返回的話不能直接進行。
返回流程:先從32位保護模式的程式碼段返回16位保護模式的程式碼段(保護模式下也可以定義16位的程式碼段),然後從16位保護模式程式碼段跳到16位真實模式程式碼段。
16位保護模式的程式碼段在這裡作為一箇中間過渡過程,我們在這個段只幹一件事,就是用合適的段選擇子對段暫存器進行賦值。除此之外不做其他的邏輯上的操作。
在操作之前,我們先介紹一下處理器中的設計:
80286之後的處理器都提供相容8086的真實模式
然而,絕大多數時候處理器都運行於保護模式
因此,保護模式的執行效率至關重要
那麼,處理器如何高效的訪問記憶體中的段描述符呢?
運行於保護模式時,效能瓶頸在於:段描述符定義在記憶體中,如果每次都要訪問記憶體,效率會比較低。如何快速高效的訪問記憶體中的段描述符呢?解決方案如下:
使用高速緩衝儲存器
當使用選擇子設定段暫存器時,會觸發處理器的內部操作:
根據選擇子訪問記憶體中的段描述符
將段描述符載入到段暫存器的高速緩衝儲存器
需要段描述符資訊時,直接從高速緩衝器中獲得
處於真實模式時也會用到這個段暫存器高速緩衝儲存器。會用到其中的段基地址和段界限。
注意事項:
在真實模式 下,高速緩衝儲存器仍然發揮著作用
段基址是32位,其值是相應段暫存器的值乘以16
真實模式下段基址有效位為20位(快取記憶體中的32段基址足以容納),段界限固定為0xFFFF(64K)
段屬性的值不可設定,只能繼續沿用保護方式下所設定的值
高速緩衝儲存器不可以直接訪問設定值。只能通過特殊的方法:
通過載入一個合適的描述符選擇子到有關段暫存器,以使得對應的段描述符高速緩衝暫存器中含有合適的段界限和段屬性。
跳到16位真實模式的具體流程:
32位保護模式程式碼段 -> 16位保護模式程式碼段(重新整理段暫存器,退出保護模式) -> 16位真實模式程式碼段(設定段暫存器的值,關閉A20地址線,啟用硬體中斷)
彙編小知識:深入理解jmp指令
段內跳轉: 指令是三個位元組,操作碼(E9)為1個位元組(低地址),運算元是兩個位元組(高地址)(也就是段內偏移地址)。
段間跳轉:指令時5個位元組,操作碼(EA)為1個位元組(低地址),運算元是四個位元組(偏移地址、段基址)(高地址)
段間跳轉時,我們可以修改指令中的偏移地址和段基址就可以跳轉到另一個期望的段中去了。修改指令是執行時修改記憶體中的指令,而不是在源程式中修改。
從保護模式返回到真實模式的程式如下:
%include "inc.asm" org 0x9000 jmp ENTRY_SEGMENT [section .gdt] ; GDT definition ; 段基址, 段界限, 段屬性 GDT_ENTRY : Descriptor 0, 0, 0 CODE32_DESC : Descriptor 0, Code32SegLen - 1, DA_C + DA_32 VIDEO_DESC : Descriptor 0xB8000, 0x07FFF, DA_DRWA + DA_32 DATA32_DESC : Descriptor 0, Data32SegLen - 1, DA_DR + DA_32 STACK32_DESC : Descriptor 0, TopOfStack32, DA_DRW + DA_32 CODE16_DESC : Descriptor 0, 0xFFFF, DA_C UPDATE_DESC : Descriptor 0, 0xFFFF, DA_DRW ; GDT end GdtLen equ $ - GDT_ENTRY GdtPtr: dw GdtLen - 1 dd 0 ; GDT Selector Code32Selector equ (0x0001 << 3) + SA_TIG + SA_RPL0 VideoSelector equ (0x0002 << 3) + SA_TIG + SA_RPL0 Data32Selector equ (0x0003 << 3) + SA_TIG + SA_RPL0 Stack32Selector equ (0x0004 << 3) + SA_TIG + SA_RPL0 Code16Selector equ (0x0005 << 3) + SA_TIG + SA_RPL0 UpdateSelector equ (0x0006 << 3) + SA_TIG + SA_RPL0 ; end of [section .gdt] TopOfStack16 equ 0x7c00 [section .dat] [bits 32] DATA32_SEGMENT: DTOS db "D.T.OS!", 0 DTOS_OFFSET equ DTOS - $$ HELLO_WORLD db "Hello World!", 0 HELLO_WORLD_OFFSET equ HELLO_WORLD - $$ Data32SegLen equ $ - DATA32_SEGMENT [section .s16] [bits 16] ENTRY_SEGMENT: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, TopOfStack16 mov [BACK_TO_REAL_MODE + 3], ax ; initialize GDT for 32 bits code segment mov esi, CODE32_SEGMENT mov edi, CODE32_DESC call InitDescItem mov esi, DATA32_SEGMENT mov edi, DATA32_DESC call InitDescItem mov esi, DATA32_SEGMENT mov edi, STACK32_DESC call InitDescItem mov esi, CODE16_SEGMENT mov edi, CODE16_DESC call InitDescItem ; initialize GDT pointer struct mov eax, 0 mov ax, ds shl eax, 4 add eax, GDT_ENTRY mov dword [GdtPtr + 2], eax ; 1. load GDT lgdt [GdtPtr] ; 2. close interrupt cli ; 3. open A20 in al, 0x92 or al, 00000010b out 0x92, al ; 4. enter protect mode mov eax, cr0 or eax, 0x01 mov cr0, eax ; 5. jump to 32 bits code jmp dword Code32Selector : 0 BACK_ENTRY_SEGMENT: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, TopOfStack16 in al, 0x92 and al, 11111101b out 0x92, al sti mov bp, HELLO_WORLD mov cx, 12 mov dx, 0 mov ax, 0x1301 mov bx, 0x0007 int 0x10 jmp $ ; esi --> code segment label ; edi --> descriptor label InitDescItem: push eax mov eax, 0 mov ax, cs shl eax, 4 add eax, esi mov word [edi + 2], ax shr eax, 16 mov byte [edi + 4], al mov byte [edi + 7], ah pop eax ret [section .16] [bits 16] CODE16_SEGMENT: mov ax, UpdateSelector mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax mov eax, cr0 and al, 11111110b mov cr0, eax BACK_TO_REAL_MODE: jmp 0 : BACK_ENTRY_SEGMENT Code16SegLen equ $ - CODE16_SEGMENT [section .s32] [bits 32] CODE32_SEGMENT: mov ax, VideoSelector mov gs, ax mov ax, Stack32Selector mov ss, ax mov eax, TopOfStack32 mov esp, eax mov ax, Data32Selector mov ds, ax mov ebp, DTOS_OFFSET mov bx, 0x0C mov dh, 12 mov dl, 33 call PrintString mov ebp, HELLO_WORLD_OFFSET mov bx, 0x0C mov dh, 13 mov dl, 30 call PrintString jmp Code16Selector : 0 ; ds:ebp --> string address ; bx --> attribute ; dx --> dh : row, dl : col PrintString: push ebp push eax push edi push cx push dx print: mov cl, [ds:ebp] cmp cl, 0 je end mov eax, 80 mul dh add al, dl shl eax, 1 mov edi, eax mov ah, bl mov al, cl mov [gs:edi], ax inc ebp inc dl jmp print end: pop dx pop cx pop edi pop eax pop ebp ret Code32SegLen equ $ - CODE32_SEGMENT [section .gs] [bits 32] STACK32_SEGMENT: times 1014 * 4 db 0 Stack32SegLen equ $ - STACK32_SEGMENT TopOfStack32 equ Stack32SegLen - 1
147-164行定義了16位的保護模式程式碼,106-126行定義了另一個16位真實模式程式碼段。15、16行定義了新的段描述符,15行的段描述符是描述16位保護模式的程式碼段的。16行的段描述符是描述16位真實模式的程式碼段的。75-78行我們初始化了16位保護模式下的段描述符。程式從32位保護模式的196行跳轉到16位保護模式的程式碼段,然後將16位真實模式程式碼段的段選擇子分別賦給ds、es、fs、gs、ss段暫存器(賦值的意義就是重新整理對應的段描述符對應的高速緩衝儲存器),賦值的同時,處理器的內部機制會讀取記憶體,並初始化段暫存器快取記憶體。這樣這些暫存器快取記憶體中儲存的就是16位真實模式程式碼段的資訊了。然後,157-159行使處理器進入真實模式。當執行162行跳轉時,處理器已經處於16位真實模式。注意,在16位保護模式的程式碼中,我們沒有給cs賦值,因為這時程式還處於16位保護模式,如果這時候我們給cs賦值16位真實模式,那麼程式會出錯,因為這時程式碼還在16位保護模式執行中。
162行的跳轉我們要跳到16位的真實模式程式碼段處,這是一個段間跳轉,因此使用jmp 0 : BACK_ENTRY_SEGMENT(按照16位真實模式進行跳轉,偏移地址是16位的,定址範圍是64kb),這裡的0我們應該填入cs的值,這個值是程式執行到第50行處cs的值,這個cs的值是代表16位真實模式程式碼的基地址,因此我們在57行加了mov [BACK_TO_REAL_MODE + 3], ax,標籤BACK_TO_REAL_MODE是在161行定義的,這句程式碼的意思是,我們直接修改記憶體中的指令,使得跳轉指令中的基地址變為cs的值。這樣就實現了執行時動態的修改指令。因此,當第162行我們執行跳轉時,jmp 0 : BACK_ENTRY_SEGMENT指令完成跳轉的同時,也會改變cs的值和ip的值,程式可以正確的跳轉到16位真實模式BACK_ENTRY_SEGMENT程式碼處,在BACK_ENTRY_SEGMENT處,cs已經處於16位真實模式下正確的值。程式執行到第162行時,cs段暫存器對應的快取記憶體中儲存的還是16位保護模式的資訊,也就是在第15行的16位保護模式程式碼段描述符中如果我們填入的段界限是Code16SegLen - 1,那麼執行到162行時,cs快取記憶體中的段界限就是這個值,而這個值比較小(因為16位保護模式的程式碼我們寫的比較小),因此,執行第162行時,會發生越界訪問異常(雖然這時候是處於真實模式,但是跳轉時段界限依舊起作用),因為BACK_ENTRY_SEGMENT一般是大於Code16SegLen - 1那個值的,因此,15行中的段界限我們要按16位真實模式的段界限64k來填,也就是0xFFFF。
最終進入到16位真實模式程式碼段BACK_ENTRY_SEGMENT處,在這個段中,cs的值已經是16位真實模式程式碼段的地址了,將cs的值一次賦值給其他的段暫存器,在這裡已經進入了16位真實模式程式碼段,對段暫存器的賦值只會改變相應高速緩衝儲存器中的段基址,段界限和段屬性沿用UPDATE_DESC中定義的值。然後設定棧,然後開啟A20地址線,然後開中斷,最後執行一個列印。
執行結果如下: