在上几节中,我们在进入到保护模式后打印完一个字符后就死循环了,而现在我们想从保护模式切回实模式,先上代码:
- %include "pm.inc"
- org 0100h
- jmp LABEL_BEGIN
- [section .gdt]
- LABEL_DESC_DUMMY:
- Descriptor 0,0,0
- LABEL_DESC_CODE32:
- Descriptor 0,0ffffh,DA_C | DA_32
- LABEL_DESC_CODE16:
- Descriptor 0,0ffffh,DA_C
- LABEL_DESC_VIDEO:
- Descriptor 0b8000h,0ffffh,DA_DRW
- LABEL_DESC_NORMAL:
- Descriptor 0,0ffffh,DA_DRW
- GDT_Len equ $ - LABEL_DESC_DUMMY
- GDT_Ptr:
- dw GDT_Len - 1
- dd 0
- Selector_Code32 equ LABEL_DESC_CODE32 - LABEL_DESC_DUMMY
- Selector_Code16 equ LABEL_DESC_CODE16 - LABEL_DESC_DUMMY
- Selector_Video equ LABEL_DESC_VIDEO - LABEL_DESC_DUMMY
- Selector_Normal equ LABEL_DESC_NORMAL - LABEL_DESC_DUMMY
- [section .s16]
- [bits 16]
- LABEL_BEGIN:
- mov ax,cs
- mov ds,ax
- mov es,ax
- mov [LABEL_GO_BACK_TO_REAL + 3],ax
- Fill_Descriptor LABEL_DESC_CODE32,LABEL_CODE32
- Fill_Descriptor LABEL_DESC_CODE16,LABEL_BEGIN
- xor eax,eax
- mov ax,ds
- shl eax,4
- add eax,LABEL_DESC_DUMMY
- mov dword [GDT_Ptr + 2],eax
- lgdt [GDT_Ptr]
- cli
- in al,92h
- or al,00000010b
- out 92h,al
- mov eax,cr0
- or al,1
- mov cr0,eax
- jmp Selector_Code32:0
- _LABEL_PREPARE_GO_BACK_TO_REAL:
- LABEL_PREPARE_GO_BACK_TO_REAL equ _LABEL_PREPARE_GO_BACK_TO_REAL - $$
- mov ax,Selector_Normal
- mov ds,ax
- mov es,ax
- mov gs,ax
- mov fs,ax
- mov eax,cr0
- and al,11111110b
- mov cr0,eax
- LABEL_GO_BACK_TO_REAL:
- jmp 0:LABEL_ALREADY_REAL
- LABEL_ALREADY_REAL:
- in al,92h
- and al,11111101b
- out 92h,al
- sti
- mov ax,4c00h
- int 21h
- [section .s32]
- [bits 32]
- LABEL_CODE32:
- mov ax,Selector_Video
- mov gs,ax
- mov ah,0ch
- mov al,'x'
- mov [gs:80 * 10],ax
- jmp Selector_Code16:LABEL_PREPARE_GO_BACK_TO_REAL
我们看到,94行进行了一个跳转,取代了原来的死循环。跳转到了16位代码段的_LABEL_PREPARE_GO_BACK_TO_REAL 处,这里要注意一点,_LABEL_PREPARE_GO_BACK_TO_REAL在保护模式下是不能用的,是在实模式下用的,我们又额外定义了一个LABEL_PREPARE_GO_BACK_TO_REAL 宏, 表示标号_LABEL_PREPARE_GO_BACK_TO_REAL在此16位代码段的偏移。为什么不就直接在这个32位代码段完成切换回实模式的工作而要先跳到16位代码段呢?这个等会儿要说到。
让我们继续前进,控制权来到了跳转的标号处,我们把一个选择子赋给了各个段寄存器,我们看到了GDT多了此选择子对应的描述符,为什么要这样呢?先从段描述符高速缓冲寄存器说起:
在实模式下,段寄存器含有段值,为访问存储器形成物理地址时,处理器引用相应的某个段寄存器并将其值乘以16,形成20位的段基地址。在保护模式下,段寄存器含有段选择子,如上所述,为了访问存储器形成线性地址时,处理器要使用选择子所指定的描述符中的基地址等信息。为了避免在每次存储器访问时,都要访问描述符表而获得对应的段描述符,从80286开始每个段寄存器都配有一个高速缓冲寄存器,称之为段描述符高速缓冲寄存器或描述符投影寄存器,对程序员而言它是不可见的。每当把一个选择子装入到某个段寄存器时,处理器自动从描述符表中取出相应的描述符,把描述符中的信息保存到对应的高速缓冲寄存器中。此后对该段访问时,处理器都使用对应高速缓冲寄存器中的描述符信息,而不用再从描述符表中取描述符。
段描述符高速缓冲寄存器在处理器内,所以可对其进行快速访问。绝大多数情况下,对存储器的访问是在对应选择子装入到段寄存器之后进行的,所以,使用段描述符高速缓冲寄存器可以得到很好的执行性能。段描述符高速缓冲寄存器之内保存的描述符信息将一直保存到重新把选择子装载到段寄存器时再更新。程序员尽管不可见段描述符高速缓冲寄存器,但必须注意到它的存在和它的上述更新时机。例如,在改变了描述符表中的某个当前段的描述符后,也要更新对应的段描述符高速缓冲寄存器的内容,即使段选择子未作改变,这可通过重新装载段寄存器实现。
这些高速缓冲寄存器在实方式下仍发挥作用,只是内容上与保护模式下有所不同。段基地址仍是 32位,其值是相应段寄存器值(段值)乘以16,在把段值装载到段寄存器时刷新。由于其值是16位段值乘上16,所以在实模式下基地址实际上有效位只有20位。每个段的32位段界限都固定为0FFFFH,段属性的许多位也是固定的。所谓固定是指在实方式下不可设置这些属性值,只能继续沿用保护方式下所设置的值。因此,在准备结束保护模式回到实模式之前,要通过加载一个合适的描述符选择子到有关段寄存器,以使得对应段描述符高速缓冲寄存器中含有合适的段界限和属性。本实例GDT中的描述符Normal就是这样一个描述符,在返回实模式之前把对应选择子Normal_Selector加载到DS和ES就是此目的。16位代码段描述符中的内容也符合实模式的需要,所以在通过16位代码段返回实模式时,CS段描述符中的内容也符合实模式的要求。需要注意的是,不能从32位代码段返回实模式,这是因为无法实现从32位代码段返回时CS高速缓冲寄存器中的属性符合实模式的要求(实模式不能改变段属性)。顺便说以下,实例中的描述符都是符合实模式要求的。段描述符高速缓冲寄存器中含有合适的段界限。
这就是为什么要给各个选择子赋Selector_Normal和必须从16位代码段返回实模式的原因了。接着看代码,第73行的代码很奇怪,跳转的地址的段值居然是0,但看看35行就明白了,在程序一开始就把实模式下的段值赋给了跳转指令的段值,这样就可以在实模式下正常的跳转到对应的代码段了。这种方法有一定的技巧。
剩下的代码就很熟悉了,运行结果在显示了字符后成功回到了DOS,如图所示:
