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地址线,然后开中断,最后执行一个打印。
执行结果如下:
