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

 

 执行结果如下:

 

  

posted on 2020-12-04 13:53  lh03061238  阅读(142)  评论(0编辑  收藏  举报

导航