一个操作系统的实现(3)-保护模式进阶

上节内容是从实模式进入到保护模式,只是进入保护模式打印了一个字母P。但是没有体现出保护模式的优势,也没有从保护模式中返回。这节就是要体验保护模式下读写大地址内存的能力和从保护模式返回到实模式。

这节要做的内容如下:首先在屏幕的第11行输出In Protect Mode now. ^-^。然后在屏幕第12行输出内存中起始地址为5MB的连续的8个字节。然后向这个以5MB开始的内存中写入ABCDEFGH。再次在第13行输出这8个字节。结果演示如下:

源代码300多行,很长,分段讲述,主要讲新增的部分

源代码解释

首先是GDT段

GDT段

[SECTION .gdt]
; GDT
;                            段基址,        段界限 , 属性
LABEL_GDT:         Descriptor    0,              0, 0         ; 空描述符
LABEL_DESC_NORMAL: Descriptor    0,         0ffffh, DA_DRW    ; Normal 描述符
LABEL_DESC_CODE32: Descriptor    0, SegCode32Len-1, DA_C+DA_32; 非一致代码段, 32
LABEL_DESC_CODE16: Descriptor    0,         0ffffh, DA_C      ; 非一致代码段, 16
LABEL_DESC_DATA:   Descriptor    0,      DataLen-1, DA_DRW    ; Data
LABEL_DESC_STACK:  Descriptor    0,     TopOfStack, DA_DRWA+DA_32; Stack, 32 位
LABEL_DESC_TEST:   Descriptor 0500000h,     0ffffh, DA_DRW
LABEL_DESC_VIDEO:  Descriptor  0B8000h,     0ffffh, DA_DRW    ; 显存首地址
; 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
SelectorTest        equ    LABEL_DESC_TEST        - LABEL_GDT
SelectorVideo        equ    LABEL_DESC_VIDEO    - LABEL_GDT
; END of [SECTION .gdt]

相比于上一节,这里新增了下面几个描述符表项:

LABEL_DESC_NORMAL:这个描述符用在从保护模式返回实模式的过程,为了让对应的段描述符告诉缓冲寄存器中含有合适的界限和属性。这儿有点不明白

LABEL_DESC_CODE16:在保护模式返回到实模式的过程中用到。用来将SeletorNormal选择子赋给ds、es、fs、gs、ss,跟上面LABEL_DESC_NORMAL合作实现返回实模式的准备工作。在书上43页

LABEL_DESC_DATA:数据段描述符

LABEL_DESC_STACK:栈段描述符

LABEL_DESC_TEST:用来测试的大地址内存(5MB起始的内存)

自然地,需要增加了相应的选择子。

接下来新增了数据段。

数据段

[SECTION .data1]     ; 数据段
ALIGN    32
[BITS    32]
LABEL_DATA:
SPValueInRealMode    dw    0
; 字符串
PMMessage:        db    "In Protect Mode now. ^-^", 0    ; 在保护模式中显示
OffsetPMMessage        equ    PMMessage - $$
StrTest:        db    "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0
OffsetStrTest        equ    StrTest - $$
DataLen            equ    $ - LABEL_DATA
; END of [SECTION .data1]

对于上面的代码

SPValueInRealMode:实模式中栈顶指针的值会保存在这里。干什么用?

ALIGN 32:这是个伪指令,告诉编译器本伪指令下面的内存变量必须从下一个能被32整除的地址开始分配。

接下来你会发现定义的两个数据块PMMessageStrTest下面都定义了另外一个符号Offset_PMMessageOffsetStrTest,用来表示对应的上一个字符串相对于本节开始处(也就是LABEL_DATA处)的偏移。定义这两个符号是因为在保护模式下需要用到这个偏移来定位字符串的位置。而不再需要实模式下的地址。

你看到GDT中关于数据段的段基址并没有初始化,所以需要对数据段描述符进行初始化,在[SECTION .s16]中,初始化代码如下:

        ; 初始化数据段描述符
        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

回顾一下,保护模式下段值仍然是用16位的寄存器来存储。段值×16+偏移地址在此时它仅仅变成了一个索引,这个索引指向的就是GDT的一个表项,这个表项里面详细定义了段的起始地址、界限、属性等内容。在书的31页有详细介绍。

接下来定义了全局堆栈段,因为保护模式下用到了堆栈,这些都是需要我们自己定义的。因此需要在实模式下新建堆栈段。

堆栈段

定义堆栈段的源代码如下:

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

TopOfStack    equ    $ - LABEL_STACK - 1

; END of [SECTION .gs]

这里定义的栈的大小是512字节,栈顶是TopOfStack

但计算栈顶的时候减一是为什么呢?这儿我也没搞清楚。栈为空时,ss:esp指向的栈的最底部单元下面的单元,这个单元的偏移地址应该为栈最底部的双字(因为是32位的堆栈段)单元的偏移地址+4。但是这里我无论怎么计算都不是在所说的位置,好像栈空间小于512字节了,难道是为了避免访问越界,减1更有保障?

[SECTION .s16]有对应的堆栈段描述符如下:

        ; 初始化堆栈段描述符
        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

再看看GDT中的关于堆栈的表项,属性是DA_DRWA+DA_32。DA_DRWA表示存在的已访问可读写数据段类型值。DA_32表示他是一个32位的堆栈段。

[SECTION .s32]有对应的堆栈初始化代码如下:

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

        mov     esp, TopOfStack

关于数据段、栈段定义位置,描述符定义的位置,相应的寄存器初始化位置

从上面可以看到:

数据段,栈段,代码段的定义是独立的;

数据段,栈段,32位代码段描述符的初始化是在16位代码段中定义的;

数据段,栈段相应寄存器的初始化要看它门在哪个代码段中使用,比如说这里的栈段是在32位代码段中使用的,所以ss,esp寄存器的初始化过程实在32位代码段中进行的。

16位代码段(运行在实模式)

这段用来对GDT进行初始化并进入保护模式,详细的介绍参考上一节文章。

[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

    ; 初始化 32 位代码段描述符
    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
    mov    ax, ds
    shl    eax, 4
    add    eax, LABEL_GDT        ; eax <- gdt 基地址
    mov    dword [GdtPtr + 2], eax    ; [GdtPtr + 2] <- gdt 基地址

    ; 加载 GDTR
    lgdt    [GdtPtr]

    ; 关中断
    cli

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

    ; 准备切换到保护模式
    mov    eax, cr0
    or    eax, 1
    mov    cr0, eax

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

mov [LABEL_GO_BACK_TO_REAL+3], axmov
[SPValueInRealMode], sp
:这条指令是干什么的呢?先告诉你它是为了后面从保护模式返回到实模式用,具体为什么是这样在下面有详细的介绍

程序刚加载就会执行这个代码段,之后跳到32位代码段中运行,32位代码段如下。

32位代码段(运行在保护模式)

相应的代码如下。虽然很长,但很简单,

首先,

是初始化相应段的寄存器。包括数据段测试段视频段(屏幕)、堆栈段

然后,

显示字符串In Protect Mode now. ^-^

然后,

显示从内存地址5MB起始的8个字节,然后向内存5MB起始的地址中写入数据段中的字符串,然后再显示从内存5MB起始的8个字节。每次显示完成后都执行换行操作。

最后通过跳转指令jmp SelectorCode16:0返回到实模式,到这里32位代码段就结束了。又回到了实模式。下面的一小节介绍跳转到的目的地做了什么工作。

先附上32位代码段,里面有详细的注释供参考

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

LABEL_SEG_CODE32:
    mov    ax, SelectorData
    mov    ds, ax                    ; 数据段选择子
    mov    ax, SelectorTest
    mov    es, ax                    ; 测试段选择子
    mov    ax, SelectorVideo
    mov    gs, ax                    ; 视频段选择子

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

    mov    esp, TopOfStack

    ; 下面显示一个字符串
    mov    ah, 0Ch                    ; 0000: 黑底    1100: 红字
    xor    esi, esi
    xor    edi, edi
    mov    esi, OffsetPMMessage    ; 源数据偏移
    mov    edi, (80 * 10 + 0) * 2    ; 目的数据偏移。屏幕第 10 行, 第 0 列。
    cld                         ; 置标志位DF的置为零
.1:
    lodsb                       ; al=((ds)*16+(esi))、(esi)=(esi)+1
    test    al, al              ; al^al,结果不保存,只影响标志位
    jz    .2                      ; 如果ZF标志位为1,转移到.2
    mov    [gs:edi], ax            ; 否则,输出到屏幕
    add    edi, 2                  ; 屏幕偏移寄存器加2
    jmp    .1                      ; 跳转到.1
.2:                                ; 显示完毕

    call    DispReturn          ; 换行

    call    TestRead            ; 读出内存5MB起始的连续8字节内容
    call    TestWrite           ; 向5MB起始的内存中写入字符串
    call    TestRead            ; 再一次读出内存5MB起始的连续8字节内容
                                ; 如果正确,读出的内存与写入的内容相同

    ; 到此停止
    jmp    SelectorCode16:0        ; 跳转到准备工作(返回实模式的准备工作代码)

; ------------------------------------------------------------------------
TestRead:                       ;输出到屏幕函数
    xor    esi, esi                ;esi置0
    mov    ecx, 8                  ; 向屏幕读出8个字节
.loop:
    mov    al, [es:esi]            ; 将内存字节内容传送到al中
    call    DispAL              ; 以16进制的形式显示出来
    inc    esi                     ; 内存地址增加1
    loop    .loop               ; 继续循环读出到屏幕

    call    DispReturn          ; 调用换行函数

    ret                         ; 函数返回
; TestRead 结束-----------------------------------------------------------

; ------------------------------------------------------------------------
TestWrite:                      ; 写入内存函数
    push    esi                 ; 保存寄存器esi内容
    push    edi                 ; 保存寄存器edi内容
    xor    esi, esi                ; esi置0
    xor    edi, edi                ; edi置0
    mov    esi, OffsetStrTest        ; 源数据偏移
    cld                         ; DF标志位置0
.1:                             ; 因此内存-内存没有直接通路,所以下面需要al中转
    lodsb                       ; al=((ds)*16+(esi))、(esi)=(esi)+1
    test    al, al              ; al^al,结果不保存,只影响标志位
    jz    .2                      ; 如果ZF标志位为0,跳转到.2
    mov    [es:edi], al            ; 将字符写入内存
    inc    edi                     ; 内存偏移到下一位
    jmp    .1                      ; 循环到写入的字符串为0时结束
.2:

    pop    edi                     ; 还原子函数用到的寄存器edi、esi,注意顺序
    pop    esi

    ret
; TestWrite 结束----------------------------------------------------------

; ------------------------------------------------------------------------
; 以16进制显示 AL 中的数字
; 默认地:
;    数字已经存在 AL 中
;    edi 始终指向要显示的下一个字符的位置
; 被改变的寄存器:
;    ax, edi
; ------------------------------------------------------------------------
DispAL:
    push    ecx                 ; 保存子程序中用到的寄存器ecx、edx
    push    edx

    mov    ah, 0Ch                    ; 0000: 黑底    1100: 红字
    mov    dl, al                  ; al高4位,低4位分开处理
    shr    al, 4                   ; 先处理高四位
    mov    ecx, 2                  ; 循环两次,第一次高四位,第二次低四位
.begin:
    and    al, 01111b
    cmp    al, 9                   ; 如果大于9,要显示A~F的ASCii码
    ja    .1
    add    al, ‘0‘                 ; 如果小于或等于9,显示0~9的ASCii码
    jmp    .2
.1:
    sub    al, 0Ah
    add    al, ‘A‘
.2:
    mov    [gs:edi], ax            ; 输出到屏幕上
    add    edi, 2                  ; 屏幕偏移指针+2

    mov    al, dl                  ; 处理低4位
    loop    .begin
    add    edi, 2                  ; edi要时刻指向要显示下一个字符的位置

    pop    edx                     ; 还原子函数用到的寄存器edx、ecx,注意顺序
    pop    ecx

    ret                         ; 子程序返回
; DispAL 结束-------------------------------------------------------------

; ------------------------------------------------------------------------
DispReturn:                     ; 实现屏幕输出的换行
    push    eax                 ; 保存用到的eax、ebx
    push    ebx

    mov    eax, edi                ;
    mov    bl, 160                 ;
    div    bl                      ; 执行结束后,ax存放的是当前行的行号码
    and    eax, 0FFh               ; 执行结束后,eax存放的是当前行的行号码
    inc    eax                     ; 让eax存放下一行的行号码
    mov    bl, 160                 ;
    mul    bl                      ; 执行结束后,eax存放的是下一行的行首偏移值
    mov    edi, eax                ; 将下一行的行首偏移置传送到edi中

    pop    ebx                     ; 还原子函数用到的寄存器ebx、eax,注意顺序
    pop    eax

    ret                         ; 子程序返回
; DispReturn 结束---------------------------------------------------------

SegCode32Len    equ    $ - LABEL_SEG_CODE32
; END of [SECTION .s32]

从保护模式跳转到实模式的16位代码段

从保护模式返回到实模式有点复杂。因为在准备结束保护模式回到实模式之前,需要加载一个合适的描述符选择子到有关的寄存器,以使对应段描述符高速缓冲寄存器中含有合适的段界限和属性。而且,我们不能从32位的代码段返回实模式,只能从16位的代码段中返回。这是因为无法实现从32位代码段返回时cs告诉缓冲寄存器中的属性符合实模式的要求(实模式不能改变属性)。

所以,在这里,我们新增一个Normal描述符。在返回实模式之前把对应选择子SelectorNormal加载到ds、es和ss,就是上面所说的这个原因

下面就来看一下保护模式返回到实模式前用到的16位代码段:

; 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, 11111110b
    mov    cr0, eax

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

Code16Len    equ    $ - LABEL_SEG_CODE16

; END of [SECTION .s16code]

这段代码前面6条mov指令就是上面所说的目的:为了使对应段描述符告诉缓存寄存器中含有合适的段界限和属性,需要加载一个合适的描述符选择子到有关寄存器中。这边我现在也不太了解,先记住吧

接下来的3条指令实现将cr0的PE位(第0位)置零,代表运行(将要运行)在保护模式下。

还记得上面提到的mov [LABEL_GO_BACK_TO_REAL+3], ax指令吗?上面只是说它要用在从保护模式返回到实模式中,这里就详细说一下它为什么能够实现这样的功能:

你看这里的jmp指令的段地址是0,但是这是程序刚加载到内存中的时候,随着运行到mov [LABEL_GO_BACK_TO_REAL+3], ax会发生什么呢?

首先看一下jmp 0:LABEL_REAL_ENTRY的机器码:

   BYTE1      BYTE2     BYTE3      BYTE4     BYTE5
0EAh offset Segment

由上图可以看出,LABEL_GO_BACK_TO_REAL+3恰好就是Segment的地址,而执行mov
[LABEL_GO_BACK_TO_REAL+3], ax
之前ax的值已经是实模式下的cs(假设记为cs_real_mode)了,所以它这条mov指令将把cs保存到segment的位置,等到jmp指令执行时,它已经不再是:

jmp    0:LABEL_REAL_ENTRY

而是:

jmp    cs_real_mode:LABEL_REAL_ENTRY

这条指令将会跳转到标号LABEL_REAL_ENTRY处。现在已经跳回到实模式了,接下来就是要重新设置各个寄存器的值,并回复sp的值,然后关闭A20,打开中断,重新回到原来的样子。LABEL_REAL_ENTRY的代码如下:

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

    mov    sp, [SPValueInRealMode]

    in    al, 92h        ; `.
    and    al, 11111101b    ;  | 关闭 A20 地址线
    out    92h, al        ; /

    sti            ; 开中断

    mov    ax, 4c00h    ; `.
    int    21h        ; /  回到 DOS
; END of [SECTION .s16]

这里我们又看到了SPValueInRealMode,还记得上面没有详细说的指令mov
[SPValueInRealMode], sp
吗?从这而很容易可以看出,它保存实模式下sp的值,也是为了现在回到实模式回复sp的值。

关闭A20地址先,开中断之后,通过int 21h中断返回到DOS。

这样整个程序的运行过程就结束了哈哈。

编译运行

通过nasm编译成.com文件,这里面还有如何突破引导扇区512字节的限制。弄明白了再详细记录。

完整源代码

下面是主程序的完整源代码


; ==========================================
; pmtest2.asm
; 编译方法:nasm pmtest2.asm -o pmtest2.com
; ==========================================

%include    "pm.inc"    ; 常量, 宏, 以及一些说明

org    0100h
    jmp    LABEL_BEGIN

[SECTION .gdt]
; GDT
;                            段基址,        段界限 , 属性
LABEL_GDT:         Descriptor    0,              0, 0         ; 空描述符
LABEL_DESC_NORMAL: Descriptor    0,         0ffffh, DA_DRW    ; Normal 描述符
LABEL_DESC_CODE32: Descriptor    0, SegCode32Len-1, DA_C+DA_32; 非一致代码段, 32
LABEL_DESC_CODE16: Descriptor    0,         0ffffh, DA_C      ; 非一致代码段, 16
LABEL_DESC_DATA:   Descriptor    0,      DataLen-1, DA_DRW    ; Data
LABEL_DESC_STACK:  Descriptor    0,     TopOfStack, DA_DRWA+DA_32; Stack, 32 位
LABEL_DESC_TEST:   Descriptor 0500000h,     0ffffh, DA_DRW
LABEL_DESC_VIDEO:  Descriptor  0B8000h,     0ffffh, DA_DRW    ; 显存首地址
; 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
SelectorTest        equ    LABEL_DESC_TEST        - LABEL_GDT
SelectorVideo        equ    LABEL_DESC_VIDEO    - LABEL_GDT
; END of [SECTION .gdt]

[SECTION .data1]     ; 数据段
ALIGN    32
[BITS    32]
LABEL_DATA:
SPValueInRealMode    dw    0
; 字符串
PMMessage:        db    "In Protect Mode now. ^-^", 0    ; 在保护模式中显示
OffsetPMMessage        equ    PMMessage - $$
StrTest:        db    "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0
OffsetStrTest        equ    StrTest - $$
DataLen            equ    $ - LABEL_DATA
; END of [SECTION .data1]

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

TopOfStack    equ    $ - LABEL_STACK - 1

; END of [SECTION .gs]

[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

    ; 初始化 32 位代码段描述符
    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
    mov    ax, ds
    shl    eax, 4
    add    eax, LABEL_GDT        ; eax <- gdt 基地址
    mov    dword [GdtPtr + 2], eax    ; [GdtPtr + 2] <- gdt 基地址

    ; 加载 GDTR
    lgdt    [GdtPtr]

    ; 关中断
    cli

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

    ; 准备切换到保护模式
    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]

    in    al, 92h        ; `.
    and    al, 11111101b    ;  | 关闭 A20 地址线
    out    92h, al        ; /

    sti            ; 开中断

    mov    ax, 4c00h    ; `.
    int    21h        ; /  回到 DOS
; END of [SECTION .s16]

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

LABEL_SEG_CODE32:
    mov    ax, SelectorData
    mov    ds, ax            ; 数据段选择子
    mov    ax, SelectorTest
    mov    es, ax            ; 测试段选择子
    mov    ax, SelectorVideo
    mov    gs, ax            ; 视频段选择子

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

    mov    esp, TopOfStack

    ; 下面显示一个字符串
    mov    ah, 0Ch            ; 0000: 黑底    1100: 红字
    xor    esi, esi
    xor    edi, edi
    mov    esi, OffsetPMMessage    ; 源数据偏移
    mov    edi, (80 * 10 + 0) * 2    ; 目的数据偏移。屏幕第 10 行, 第 0 列。
    cld
.1:
    lodsb
    test    al, al
    jz    .2
    mov    [gs:edi], ax
    add    edi, 2
    jmp    .1
.2:    ; 显示完毕

    call    DispReturn

    call    TestRead
    call    TestWrite
    call    TestRead

    ; 到此停止
    jmp    SelectorCode16:0

; ------------------------------------------------------------------------
TestRead:
    xor    esi, esi
    mov    ecx, 8
.loop:
    mov    al, [es:esi]
    call    DispAL
    inc    esi
    loop    .loop

    call    DispReturn

    ret
; TestRead 结束-----------------------------------------------------------

; ------------------------------------------------------------------------
TestWrite:
    push    esi
    push    edi
    xor    esi, esi
    xor    edi, edi
    mov    esi, OffsetStrTest    ; 源数据偏移
    cld
.1:
    lodsb
    test    al, al
    jz    .2
    mov    [es:edi], al
    inc    edi
    jmp    .1
.2:

    pop    edi
    pop    esi

    ret
; TestWrite 结束----------------------------------------------------------

; ------------------------------------------------------------------------
; 显示 AL 中的数字
; 默认地:
;    数字已经存在 AL 中
;    edi 始终指向要显示的下一个字符的位置
; 被改变的寄存器:
;    ax, edi
; ------------------------------------------------------------------------
DispAL:
    push    ecx
    push    edx

    mov    ah, 0Ch            ; 0000: 黑底    1100: 红字
    mov    dl, al
    shr    al, 4
    mov    ecx, 2
.begin:
    and    al, 01111b
    cmp    al, 9
    ja    .1
    add    al, ‘0‘
    jmp    .2
.1:
    sub    al, 0Ah
    add    al, ‘A‘
.2:
    mov    [gs:edi], ax
    add    edi, 2

    mov    al, dl
    loop    .begin
    add    edi, 2

    pop    edx
    pop    ecx

    ret
; DispAL 结束-------------------------------------------------------------

; ------------------------------------------------------------------------
DispReturn:
    push    eax
    push    ebx
    mov    eax, edi
    mov    bl, 160
    div    bl
    and    eax, 0FFh
    inc    eax
    mov    bl, 160
    mul    bl
    mov    edi, eax
    pop    ebx
    pop    eax

    ret
; DispReturn 结束---------------------------------------------------------

SegCode32Len    equ    $ - LABEL_SEG_CODE32
; END of [SECTION .s32]

; 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, 11111110b
    mov    cr0, eax

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

Code16Len    equ    $ - LABEL_SEG_CODE16

; END of [SECTION .s16code]
时间: 2024-08-30 03:10:55

一个操作系统的实现(3)-保护模式进阶的相关文章

我是如何学习写一个操作系统(三):操作系统的启动之保护模式

前言 上一篇其实已经说完了boot的大致工作,但是Linux在最后进入操作系统之前还有一些操作,比如进入保护模式.在我自己的FragileOS里进入保护模式是在引导程序结束后完成的. 实模式到保护模式属于操作系统的一个大坎,所以需要先提一下 从实模式到保护模式 实模式和保护模式都是CPU的工作模式,它们的主要区别就是寻址方式 实模式出现于早期8088CPU时期.当时由于CPU的性能有限,一共只有20位地址线(所以地址空间只有1MB),以及8个16位的通用寄存器,以及4个16位的段寄存器.所以为了

[新手易懂]操作系统的实现与保护模式

一个操作系统是怎么实现的呢?让我们慢慢来学习,本文章将带大家来了解操作系统各种功能的实现以及保护模式.同时告诉大家中国自主开源操作系统UdoOS,UdoOS项目成立于2016年2月,是一款由中国人自主开发的基于自主开发的Udo内核的自主操作系统,在UdoOS中没有任何其他系统的代码,UdoOS官网:xh.14eowi.com 一.引导扇区 1.1  电脑的启动过程 CPU初始化 ↓ BIOS自检 ↓ 读取有效启动扇区 ↓ 载入0c700h内存 ↓ 跳到0c700h开始运行 1.2  什么是引导扇

80386与8086区别以及保护模式

在windows环境下,cpu的工作方式有三种:实模式:和8086工作一样(一般为开机时初始化计算机的时候),此时一个任务独占cpu 保护模式:即计算开机后所处的状态,此时可以运行多个程序,多线程. 虚拟8086模式:即计算机处于保护模式时运行的dos程序,此时处于保护模式,即在保护模式下模拟实模式,使之前8086下能运行的软                                                                                件在保护模式

第04章 保护模式入门

1 实模式 1.1 实模式缺点 保护模式强调的是保护,是在Intel 80286中首次出现. 实模式的特点: 实模式下,操作系统和用户程序属于同一特权级. 用户程序所使用的地址都指向真实的物理地址,也就是说逻辑地址等于物理地址 用户程序可以使用任意段基址,修改内存中任意数据. 访问超过64KB数据需要切换段基址 一次只能运行一个程序 共20条地址线,最大可寻址的内存为1MB 为了克服这种不安全的内存管理.处理器厂商,开发出保护模式.物理内存不能直接被程序访问,程序内部的地址需要被转化位物理地址再

一个操作系统的实现(11)-让操作系统进入保护模式

这节首先介绍了突破引导扇区只有512字节的原理,然后介绍了FAT12文件系统,最后通过实验加载loader并将控制权交给loader来实现突破512字节的束缚. 突破512字节的限制 前面所用的引导扇区只有512字节.然而实际上操作系统在启动过程需要做的事情是很多的.所以需要通过某种方法突破512字节的限制. 那么如何突破512字节的限制呢?一种方法是再建立一个文件,通过引导扇区把它加载到内存,然后把控制权教给它.这样,512字节的束缚就没有了. 这里被引导扇区加载进内存的并不是操作系统的内核.

一个操作系统的实现(2)-认识保护模式

今天开始学习intel处理器的保护模式.书的第二章 这节讲述的是如何从实模式进入保护模式.用的例子是在保护模式下向屏幕上输出字符P 如何进入保护模式呢?主要步骤如下: 0. 进入保护模式的步骤 准备GDT 用lgdt加载gdtr 打开A20 置r0的PE位位1 跳转,进入保护模式 下面是书的例子: 1. 进入保护模式实例 ; ========================================== ; pmtest1.asm ; 编译方法:nasm pmtest1.asm -o pm

CPU保护模式深入探秘

原文链接为:http://www.chinaunix.net/old_jh/23/483510.html 保护方式的体系结构 主要问题:          保护方式的寄存器模型          保护方式的描述符与页表项          保护方式的存储器管理与地址转换          多任务机制与保护实现          虚拟 8086 模式 一.保护方式的寄存器模型   新增四个寄存器 (指针 -----  指向内存中的特殊的数据表):  全局描述符表寄存器 GDTR  局部描述符表寄存

一个操作系统的实现中jmp dword SelectorCode32:0的理解

; 准备切换到保护模式 mov eax, cr0 or eax, 1 mov cr0, eax ; 真正进入保护模式 jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs, ; 并跳转到 Code32Selector:0 处 这段时间在一个操作系统的实现 好书啊,感谢作者 这两天一直纠结在    ; jmp    dword SelectorCode32:0    ; 这句话,看了些资料,现将自己理解的分享一下,不对的地方,大家指点一

操作系统开发之——进入保护模式

依然直接贴代码: %macro Descriptor 3 dw %2 & 0FFFFh ; 段界限 1 (2 字节) dw %1 & 0FFFFh ; 段基址 1 (2 字节) db (%1 >> 16) & 0FFh ; 段基址 2 (1 字节) dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性 1 + 段界限 2 + 属性 2 (2 字节) db (%1 >> 24) & 0FFh