操作系统:GDT简介&实模式到保护模式的跳转

前言(瞎逼逼)

自从AFO之后就想着开一个什么新坑,但是包括但不限于大学申请之类的破事儿让我的确忙活了一阵子。。。闲下来的时候突然想起来以前曾经开过一个小头(当然很快就因为姿势水平不够就放弃了)的编写操作系统,于是自己就想着把儿时的梦想继续坚持下去吧

这篇博客主要是依靠于渊的《Orange's 一个操作系统的实现》里面的代码来给出的解释,所以可能会有很多的汇编。

实模式

CPU自启动的时候一开始会首先进入实模式,此时我们只能访问1MB的内存,并且任何程序都可以访问内存段内的任意位置,这样显然是不安全的。但是现在的32位CPU可以访问4GB的内存,我们主要是通过进入保护模式来达到这个目的的(保护模式带来的好处并不限于访问内存的扩大)

GDT

我们先来想一下,实模式下CPU的寻址方式是:

\[段地址:段内偏移 \]

这种方式在以前固然好,但是新的32位CPU提供了32位地址线。老的16位寄存器已经不够用了,我们现在需要一种新的内存寻址方式,GDT就此诞生了

GDT本质上一个表,表内包含了一段内存的段基址,段界限,段属性。实模式下我们使用段地址:偏移地址通过地址处理器来直接访问内存,但是实模式下段地址的意义发生了根本的变化。段地址不再表示实际内存中的某一段,而是表示的我们要访问的段在GDT表里面的偏移。通过访问GDT表的特定表项来得出段基址,紧接着通过段内偏移来访问内存。此时的段地址我们有另外一个名字——段选择子。我们通过定义不同的选择子来对应到不同的表项里面去,接着就可以访问特定内存了。

总结起来就是这个样子的:

1

当然,要使用GDT我们必须先有一个GDT,下面是GDT的定义部分:

[SECTION .gdt]
LABEL_GDT:              Descriptor      0,          0,           0 ;空描述符
LABEL_DESC_CODE32:      Descriptor      0,SegCode32Len-1,  DA_C+DA_32 ;32位代码段
LABEL_DESC_VIDEO:       Descriptor 0B8000h,0ffffh    ,DA_DRW ;显存映射到内存中的地址

然后加上GDT的长度和界限

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

在这里面的 LABEL_DESC_CODE32 和 GdtPtr 都要-1,因为访问指定表项所对应的段基址的时候都是包含基址的,长度自然要-1

再往后面就是定义段选择子了,其实很简单,就是表项相对于GDT表的偏移

SelectorCode32 equ LABEL_DESC_CODE32-LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO-LABEL_GDT

然后我们就要开始准备载入GDT了。这里 LABEL_DESC_CODE32 的基地址并不急着给出,因为你也不知道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 ax,0003h
    int 10h

这里除了一些基本的指令以外就是我调用了10号中断来清了屏,因为bochs一开始屏幕上会有一堆BIOS提示。

且声明了栈段和sp。

    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

这里我们就要开始处理之前留下来的32位代码段基地址的问题了。要注意的是,由于此时我们仍然处于16位下,并且GDT并没有被载入到i内存中去,所以 LABEL_DESC_CODE32+2 指向的仍然是标号指向的位置,也就是表项内部(和之前在实模式下寻址方式一样,不要搞混了)

    xor eax,eax
    mov ax,ds
    shl eax,4
    add eax,LABEL_GDT
    mov dword [GdtPtr+2],eax

CPU是通过寄存器GDTR来得知GDT的基地址和界限的,GDTR的示意图在这里:

2

我们要做到的就是在GdtPtr里面填充符合标准的数据然后使用后lgdt指令来加载GDTR寄存器,也就是上面的这段代码所做的事情。

然后就可以快乐地加载GDTR了

    lgdt [GdtPtr]

接着我们需要屏蔽掉所有16位中断。因为保护模式下中断处理的方式都是不同的(比如说寻址),如果计算机在保护模式之后又跑去处理实模式下的中断代码的化会发生错误

    cli ;关中断

再就是打开A20地址线了。关于A20地址线的历史性问题这里就不再赘述,感兴趣的化可以去《x86汇编语言 从实模式到保护模式》的11.5章节里面找到细节的描述。

具体来讲,打开A20地址线的方法就是操作92号端口来实现的:

    in al,92h
    or al,00000010b
    out 92h,al

接下来就是置控制寄存器cr0的PE(Protected Enable)位到1来开启保护模式

    mov eax,cr0
    or eax,1
    mov cr0,eax

然后就是跳转到32位保护模式下的代码段了:

    jmp dword SelectorCode32:0

这里有一些细节要注意。因为此时GDTR已经获得了GDT的位置,所以这里就已经是通过GDT来寻址了。并且此时偏移地址仍然是在16位下进行编译的,所以如果偏移地址不是0而是一个很大的值,普通的 jmp SelectorCode32:0x12345678 可能会让偏移位上的地址发生截断。所以我们通过声明dword类型的地址避免编译器把偏移地址给自动截断

接下来就是32位代码段的代码了,有一些之前使用过的标号是在这里声明的,可以和之前的代码结合起来看:

[SECTION .s32]
[BITS 32]

LABEL_SEG_CODE32:
    mov ax,SelectorVideo 
    mov gs,ax
    mov edi,(80*11+39)*2
    mov ah,0ch
    mov al,'p'
    mov [gs:edi],ax
    jmp $

SegCode32Len equ $-LABEL_SEG_CODE32

就是在屏幕中央打印一个红色的 'p'

所有代码:

%include "pm.inc"

org 07c00h
jmp LABEL_BEGIN

[SECTION .gdt]
LABEL_GDT:              Descriptor      0,          0,           0
LABEL_DESC_CODE32:      Descriptor      0,SegCode32Len-1,  DA_C+DA_32
LABEL_DESC_VIDEO:       Descriptor 0B8000h,0ffffh    ,DA_DRW

GdtLen equ $-LABEL_GDT
GdtPtr dw GdtLen-1
dd 0

SelectorCode32 equ LABEL_DESC_CODE32-LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO-LABEL_GDT

[SECTION .s16]
[BITS 16]
LABEL_BEGIN:
    mov ax,cs
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov sp,0100h

    mov ax,0003h
    int 10h

    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_GDT
    mov dword [GdtPtr+2],eax

    lgdt [GdtPtr]

    cli

    in al,92h
    or al,00000010b
    out 92h,al

    mov eax,cr0
    or eax,1
    mov cr0,eax

    jmp dword SelectorCode32:0

[SECTION .s32]
[BITS 32]

LABEL_SEG_CODE32:
    mov ax,SelectorVideo 
    mov gs,ax
    mov edi,(80*11+39)*2
    mov ah,0ch
    mov al,'p'
    mov [gs:edi],ax
    jmp $

SegCode32Len equ $-LABEL_SEG_CODE32

运行截图:

3

后续

很憋屈的是,这段代码依然是放在引导扇区里面的。所以如果你用bximage新创建了一个1.44mb大小的img的化,需要在尾部写入0xaa55以让cpu识别到引导扇区,否则就会报 no bootable device的错

第一次写OS相关的博客,可能会有一些地方出错。。。

posted @ 2020-08-30 16:50  菜鸡mk  阅读(1432)  评论(0编辑  收藏  举报