前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >保护模式进阶 -- 再回实模式

保护模式进阶 -- 再回实模式

作者头像
用户3147702
发布2022-06-27 14:20:38
6180
发布2022-06-27 14:20:38
举报
文章被收录于专栏:小脑斧科技博客

1. 引言

上一篇文章中,我们看到了如何从实地址模式进入到保护模式: 进军保护模式 但是那一段简短的程序中,存在着很多不足,例如,数据直接在内存中读写,数据实际上没有被保护模式保护起来,同时,由于没有堆栈段,无法实现函数调用,到最后,我们的程序在死循环中结束,更优雅的方式实际上是能够返回到实地址模式并正常的退出程序,而不是一直死循环下去。 本文,我们就来修改上一篇文章中的程序,实现保护模式的进阶功能。

2. 数据段与堆栈段

2.1. 内存空间的创建

首先我们需要在内存上开辟数据段与堆栈段的空间:

代码语言:javascript
复制
[SECTION .data1]     ; 数据段
ALIGN    32
[BITS    32]
LABEL_DATA:
SPValueInRealMode    dw    0
BootMessage:        db  "Hello World my OS!", 0
OffsetBootMessage    equ    BootMessage - $$
DataLen                equ    $ - LABEL_DATA

; 全局堆栈段
[SECTION .gs]
ALIGN    32
[BITS    32]
LABEL_STACK:
    times 512 db 0
TopOfStack    equ    $ - LABEL_STACK - 1

数据段我们按需使用,存储了预留的用于存储实地址模式堆栈寄存器 sp 值的内存变量 SPValueInRealMode 和我们要显示的字符串 BootMessage 及他的偏移,以及数据段大小 DataLen。 而全局堆栈段由于需要在程序中进行压栈与出栈操作,所以必须提前预留,我们预期了 512 字节的栈空间。 我们知道,汇编中,堆栈寄存器值降低为压栈操作,增加则为出战操作,因此栈顶在高地址处,也就是这里我们定义的 TopOfStack 变量。

2.2. 创建 GDT 描述符

在 GDT 中,我们依次写入数据段与堆栈段的描述符:

代码语言:javascript
复制
LABEL_DESC_DATA:   Descriptor        0,          DataLen-1, 92h           ; 可读写数据段,界限 64KB
LABEL_DESC_STACK:  Descriptor       0,       TopOfStack, 4093h      ; 32 位全局堆栈段,可读写数据段,且栈指针默认使用 esp 寄存器

段基址我们填写了 0,因为我们要通过代码读取上述在内存空间中开辟的实际数据段与堆栈段的基地址。 段界限与段属性是已知的,直接填写即可。 段属性的具体取值可以参看: 详解 32 位保护模式与内存分段机制

对于数据段,92h 表示:

  • G 位为 0 — 以字节为粒度
  • B 位为 0 — 界限位 64KB
  • S 位为 1 — 表示是数据段
  • TYPE 位为 2 — 可读写

对于堆栈段,4093h 则表示:

  • G 位为 0 — 以字节为粒度
  • B 位为 1 — 默认使用 esp 作为栈指针寄存器
  • S 位为 1 — 表示是数据段
  • TYPE 位为 3 — 可访问、可读写

2.3. 初始化段基址

和代码段的段基址初始化一样,我们先计算出物理地址,再赋值到描述符的对应位置:

代码语言:javascript
复制
; 初始化数据段描述符
xor    eax, eax
mov    ax, ds
shl    eax, 4
add    eax, LABEL_DATA
mov    word [LABEL_DESC_DATA + 2], ax
shr    eax, 16
mov    byte [LABEL_DESC_DATA + 4], al
mov    byte [LABEL_DESC_DATA + 7], ah

; 初始化堆栈段描述符
xor    eax, eax
mov    ax, ds
shl    eax, 4
add    eax, LABEL_STACK
mov    word [LABEL_DESC_STACK + 2], ax
shr    eax, 16
mov    byte [LABEL_DESC_STACK + 4], al
mov    byte [LABEL_DESC_STACK + 7], ah

这样,我们就完成了数据段与堆栈段的创建和初始化。

2.4. 创建段选择子

代码语言:javascript
复制
SelectorData        equ    LABEL_DESC_DATA        - LABEL_GDT
SelectorStack        equ    LABEL_DESC_STACK    - LABEL_GDT

3. 使用数据段与堆栈段 — 实现函数调用

3.1. 为段寄存器赋值段选择子

代码语言:javascript
复制
    mov    ax, SelectorData
    mov    ds, ax                    ; 数据段选择子

    mov    ax, SelectorStack
    mov    ss, ax                    ; 堆栈段选择子
    mov    esp, TopOfStack

3.2. 函数编写与调用

代码语言:javascript
复制
    xor edi, edi
    mov    edi, 80 * 2 * 2         ; 屏幕第 2 行, 第 0 列
    xor esi, esi
    mov esi, OffsetBootMessage
    call DisplayString

DisplayString:
    push eax
    mov    ah, 8Ch                    ; 0000: 黑底    1100: 红字
    cld
.loop_label:
    lodsb
    test al, al
    jz .over_print
    mov    [gs:edi], ax
    add    edi, 2
    jmp .loop_label

.over_print:
    pop eax
    ret

4. 跳回实地址模式

我们已经完成了对上一篇文章中代码的改造,引入了数据段,对数据实现了保护,并且通过堆栈段的引入,实现了函数调用。 接下来,完成所有工作的程序我们就要让他回到实地址模式中。

4.1. 原理描述

还记得我们是怎么从实地址模式进入保护模式的吗:

  1. 准备 GDT
  2. 通过 lgdt 指令加载 gdtr
  3. 通过 cli 指令关闭硬件中断
  4. 打开 A20 地址线
  5. 置 cr0 的 PE 位,打开保护模式
  6. 跳转到保护模式代码段起始地址处

所以我们从后往前进行反向操作是不是就可以了呢? 从上一篇文章的讲述中,我们就知道,只要 cr0 的 PE 位被复位为 0,那么 CPU 就会通过 16 位实地址模式的段基址 * 16 + 段偏移来计算物理地址。 最容易想到的,只要在进入保护模式之前,将各个段基址寄存器的值保存下来,在复位 PE 位之前将他们更新为原值,然后就可以实现跳转回实地址模式了。 但实际上,80X86 CPU 内部还有一系列 64 位段描述符缓冲寄存器。

4.1.1. 段描述符高速缓冲寄存器

保护模式寻址需要通过 GDTR 寄存器 + 段选择子定位到 GDT 中的描述符,再通过描述符中的段基址定位。 而程序大部分时间实在同一个段内运行的,如果每次地址切换都进行上述一系列操作,对于 CPU 的性能来说显然是非常浪费的,于是,CPU 内部针对六个段基址寄存器:CS、DS、ES、FS、GS、SS 分别有一个对应的段描述符高速缓冲寄存器。 作为缓冲寄存器,用户是不能直接操作的,但每一次对任何一个段基址寄存器的赋值操作都会更新对应的缓冲寄存器。 在实地址模式下,除 CS 外所有的缓冲寄存器都必须拥有相同的段界限:0xffff,以及段属性:0x92,CS 对应的高速缓冲寄存器的段属性则必须为 0x98。 因此,我们需要分别创建符合上述要求的代码段描述符与非代码段描述符。

4.1.2. 跳回实地址模式切换步骤

剩下的工作就比较简单了,只要按照跳转到保护模式时操作的反操作即可:

  1. 段描述符高速缓冲寄存器赋值
  2. 复位 PE 位,切换到实地址模式
  3. 跳转到实地址模式代码起始地址
  4. 关闭 A20 地址线
  5. 打开硬件中断
  6. 通过触发 BIOS 中断退出程序

4.1.3. BIOS 21H 中断

BIOS 21H 中断用于,进行各种 IO 操作,具体可以参见: http://spike.scu.edu.au/~barry/interrupts.html#ah4c。

如果在触发 INT 21H 时,AH 值为 4CH,则退出当前程序。

4.2. 创建用于编写返回代码的代码段和

4.2.1. 在 GDT 中插入描述符

代码语言:javascript
复制
LABEL_DESC_NORMAL: Descriptor       0,           0ffffh, 92h        ; Normal 描述符
LABEL_DESC_CODE16: Descriptor        0,           0ffffh, 98h          ; 非一致代码段, 用于跳回 16 BITS 模式

如上所述,这个段描述符用于在切换到实地址模式后按要求填充段描述符高速缓冲寄存器,因此段界限与段属性是固定的。

4.2.2. 初始化写入段基址

代码语言:javascript
复制
; 初始化 16 位代码段描述符
mov    ax, cs
movzx    eax, ax
shl    eax, 4
add    eax, LABEL_SEG_CODE16
mov    word [LABEL_DESC_CODE16 + 2], ax
shr    eax, 16
mov    byte [LABEL_DESC_CODE16 + 4], al
mov    byte [LABEL_DESC_CODE16 + 7], ah

对于 LABEL_DESC_NORMAL 描述符,我们只是希望用它填充高速缓冲寄存器,并不需要通过他定位到内存中具体的段,所以无需初始化他的段基址。

4.2.3. 创建段选择子

代码语言:javascript
复制
SelectorNormal        equ    LABEL_DESC_NORMAL    - LABEL_GDT
SelectorCode16        equ    LABEL_DESC_CODE16    - LABEL_GDT

4.3. 编写代码

代码语言:javascript
复制
; 16 位代码段. 由 32 位代码段跳入, 跳出后到实模式
[SECTION .s16code]
ALIGN    32
[BITS    16]
LABEL_SEG_CODE16:
    ; 初始化段描述符高速缓冲寄存器
    mov    ax, SelectorNormal
    mov    ds, ax
    mov    es, ax
    mov    fs, ax
    mov    gs, ax
    mov    ss, ax

    ; 复位 PE 位,切换到实地址模式
    mov    eax, cr0
    and    al, 0feh
    mov    cr0, eax

LABEL_GO_BACK_TO_REAL:
    jmp word 0:LABEL_REAL_ENTRY    ; 段地址会在程序开始处被设置成正确的值

Code16Len equ $ - LABEL_SEG_CODE16

LABEL_REAL_ENTRY:        ; 从保护模式跳回到实模式就到了这里
    mov    ax, cs
    mov    ds, ax
    mov    es, ax
    mov    ss, ax

    mov    sp, [SPValueInRealMode]

    ; 关闭 A20 地址线
    in    al, 92h
    and    al, 0fdh
    out    92h, al

    ; 打开硬件中断
    sti

    ; 触发 BIOS int 21h 中断,回到实地址模式
    mov    ax, 4c00h
    int    21h

5. 执行结果

启动虚拟机,我们可以看到:

6. 附录 — 完整代码

代码语言:javascript
复制
; ---------------- 内存段描述符宏 -------------
; usage: Descriptor Base, Limit, Attr
;        Base:  dd
;        Limit: dd (low 20 bits available)
;        Attr:  dw (lower 4 bits of higher byte are always 0)
%macro Descriptor 3
    dw    %2 & 0FFFFh                            ; 段界限1
    dw    %1 & 0FFFFh                            ; 段基址1
    db    (%1 >> 16) & 0FFh                    ; 段基址2
    dw    ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh)    ; 属性1 + 段界限2 + 属性2
    db    (%1 >> 24) & 0FFh                    ; 段基址3
%endmacro

; ------------ DOS 加载初始内存地址 -----------
org    0100h
    jmp    LABEL_BEGIN

; ------------------- GDT ---------------------
[SECTION .gdt]
; GDT
;                              段基址,           段界限,        属性
LABEL_GDT:           Descriptor       0,                0, 0            ; 空描述符
LABEL_DESC_NORMAL: Descriptor       0,           0ffffh, 92h        ; Normal 描述符
LABEL_DESC_CODE32: Descriptor        0, SegCode32Len - 1, 4098h      ; 非一致代码段
LABEL_DESC_CODE16: Descriptor        0,           0ffffh, 98h          ; 非一致代码段, 用于跳回 16 BITS 模式
LABEL_DESC_DATA:   Descriptor        0,          DataLen-1, 92h           ; 可读写数据段,界限 64KB
LABEL_DESC_STACK:  Descriptor       0,       TopOfStack, 4093h      ; 32 位全局堆栈段,可读写数据段,且栈指针默认使用 esp 寄存器
LABEL_DESC_VIDEO:  Descriptor 0B8000h,           0ffffh, 92h        ; 显存首地址
; ------------------ END OF GDT ----------------

GdtLen        equ    $ - LABEL_GDT    ; GDT长度
GdtPtr        dw    GdtLen - 1        ; GDT界限
            dd    0                ; GDT基地址

; ------------------ GDT 选择子 -----------------
SelectorNormal        equ    LABEL_DESC_NORMAL    - LABEL_GDT
SelectorCode32        equ    LABEL_DESC_CODE32    - LABEL_GDT
SelectorCode16        equ    LABEL_DESC_CODE16    - LABEL_GDT
SelectorData        equ    LABEL_DESC_DATA        - LABEL_GDT
SelectorStack        equ    LABEL_DESC_STACK    - LABEL_GDT
SelectorVideo        equ    LABEL_DESC_VIDEO    - LABEL_GDT
; --------------- END OF 段选择子 ----------------

[SECTION .data1]     ; 数据段
ALIGN    32
[BITS    32]
LABEL_DATA:
SPValueInRealMode    dw    0
BootMessage:        db  "Hello World my OS!", 0
OffsetBootMessage    equ    BootMessage - $$
DataLen                equ    $ - LABEL_DATA

; 全局堆栈段
[SECTION .gs]
ALIGN    32
[BITS    32]
LABEL_STACK:
    times 512 db 0
TopOfStack    equ    $ - LABEL_STACK - 1

[SECTION .s16]
[BITS    16]
LABEL_BEGIN:
    ; 初始化段基址寄存器
    mov    ax, cs
    mov    ds, ax
    mov    es, ax
    mov    ss, ax
    mov    sp, 0100h

    mov    [LABEL_GO_BACK_TO_REAL+3], ax
    mov    [SPValueInRealMode], sp

    ; 初始化 16 位代码段描述符
    mov    ax, cs
    movzx    eax, ax
    shl    eax, 4
    add    eax, LABEL_SEG_CODE16
    mov    word [LABEL_DESC_CODE16 + 2], ax
    shr    eax, 16
    mov    byte [LABEL_DESC_CODE16 + 4], al
    mov    byte [LABEL_DESC_CODE16 + 7], ah

    ; 初始化非一致代码段描述符
    xor    eax, eax
    mov    ax, cs
    shl    eax, 4
    add    eax, LABEL_SEG_CODE32    ; 计算非一致代码段基地址物理地址
    mov    word [LABEL_DESC_CODE32 + 2], ax
    shr    eax, 16
    mov    byte [LABEL_DESC_CODE32 + 4], al
    mov    byte [LABEL_DESC_CODE32 + 7], ah

    ; 初始化数据段描述符
    xor    eax, eax
    mov    ax, ds
    shl    eax, 4
    add    eax, LABEL_DATA
    mov    word [LABEL_DESC_DATA + 2], ax
    shr    eax, 16
    mov    byte [LABEL_DESC_DATA + 4], al
    mov    byte [LABEL_DESC_DATA + 7], ah

    ; 初始化堆栈段描述符
    xor    eax, eax
    mov    ax, ds
    shl    eax, 4
    add    eax, LABEL_STACK
    mov    word [LABEL_DESC_STACK + 2], ax
    shr    eax, 16
    mov    byte [LABEL_DESC_STACK + 4], al
    mov    byte [LABEL_DESC_STACK + 7], ah

    ; 准备加载 GDTR
    xor    eax, eax                ; 清空 eax 寄存器
    mov    ax, ds
    shl    eax, 4
    add    eax, LABEL_GDT            ; 计算出 GDT 基地址的物理地址
    mov    dword [GdtPtr + 2], eax    ; [GdtPtr + 2] <- gdt 基地址

    ; 加载 GDTR
    lgdt    [GdtPtr]

    ; 关闭硬件中断
    cli

    ; 打开 A20 地址总线
    in    al, 92h
    or    al, 00000010b
    out    92h, al

    ; 置位 PE 标志位,打开保护模式
    mov    eax, cr0
    or    eax, 1
    mov    cr0, eax

    ; 跳转进入保护模式
    jmp    dword SelectorCode32:0    ; 执行这一句会把 SelectorCode32 装入 cs,
                                ; 并跳转到 Code32Selector:0  处

LABEL_REAL_ENTRY:        ; 从保护模式跳回到实模式就到了这里
    mov    ax, cs
    mov    ds, ax
    mov    es, ax
    mov    ss, ax

    mov    sp, [SPValueInRealMode]

    ; 关闭 A20 地址线
    in    al, 92h
    and    al, 0fdh
    out    92h, al

    ; 打开硬件中断
    sti

    ; 触发 BIOS int 21h 中断,回到实地址模式
    mov    ax, 4c00h
    int    21h

[SECTION .s32]    ; 32 位代码段. 由实模式跳入.
[BITS    32]

LABEL_SEG_CODE32:
    mov    ax, SelectorData
    mov    ds, ax                    ; 数据段选择子

    mov    ax, SelectorVideo
    mov    gs, ax                    ; 赋值视频段选择子

    mov    ax, SelectorStack
    mov    ss, ax                    ; 堆栈段选择子

    mov    esp, TopOfStack

    xor edi, edi
    mov    edi, 80 * 2 * 2         ; 屏幕第 2 行, 第 0 列
    xor esi, esi
    mov esi, OffsetBootMessage
    call DisplayString
    jmp SelectorCode16:0

DisplayString:
    push eax
    mov    ah, 8Ch                    ; 0000: 黑底    1100: 红字
    cld
.loop_label:
    lodsb
    test al, al
    jz .over_print
    mov    [gs:edi], ax
    add    edi, 2
    jmp .loop_label

.over_print:
    pop eax
    ret

SegCode32Len        equ    $ - LABEL_SEG_CODE32

; 16 位代码段. 由 32 位代码段跳入, 跳出后到实模式
[SECTION .s16code]
ALIGN    32
[BITS    16]
LABEL_SEG_CODE16:
    ; 跳回实模式:
    mov    ax, SelectorNormal
    mov    ds, ax
    mov    es, ax
    mov    fs, ax
    mov    gs, ax
    mov    ss, ax

    mov    eax, cr0
    and    al, 0feh
    mov    cr0, eax

LABEL_GO_BACK_TO_REAL:
    jmp word 0:LABEL_REAL_ENTRY    ; 段地址会在程序开始处被设置成正确的值

Code16Len    equ    $ - LABEL_SEG_CODE16
; END of [SECTION .s16code]

7. 参考资料

http://spike.scu.edu.au/~barry/interrupts.html#ah4c。 https://nasm.us/doc/nasmdoc6.html。 《Orange’s》。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-01-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小脑斧科技博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 引言
  • 2. 数据段与堆栈段
    • 2.1. 内存空间的创建
      • 2.2. 创建 GDT 描述符
        • 2.3. 初始化段基址
          • 2.4. 创建段选择子
          • 3. 使用数据段与堆栈段 — 实现函数调用
            • 3.1. 为段寄存器赋值段选择子
              • 3.2. 函数编写与调用
              • 4. 跳回实地址模式
                • 4.1. 原理描述
                  • 4.1.1. 段描述符高速缓冲寄存器
                  • 4.1.2. 跳回实地址模式切换步骤
                  • 4.1.3. BIOS 21H 中断
                • 4.2. 创建用于编写返回代码的代码段和
                  • 4.2.1. 在 GDT 中插入描述符
                  • 4.2.2. 初始化写入段基址
                  • 4.2.3. 创建段选择子
                • 4.3. 编写代码
                • 5. 执行结果
                • 6. 附录 — 完整代码
                • 7. 参考资料
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档