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

今天开始学习intel处理器的保护模式。书的第二章

这节讲述的是如何从实模式进入保护模式。用的例子是在保护模式下向屏幕上输出字符P

如何进入保护模式呢?主要步骤如下:

0. 进入保护模式的步骤

  1. 准备GDT
  2. 用lgdt加载gdtr
  3. 打开A20
  4. 置r0的PE位位1
  5. 跳转,进入保护模式

下面是书的例子:

1. 进入保护模式实例

; ==========================================
; pmtest1.asm
; 编译方法:nasm pmtest1.asm -o pmtest1.bin
; ==========================================

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

org     0100h
        jmp     LABEL_BEGIN

[SECTION .gdt]
; 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      ; 显存首地址
; GDT 结束

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

; GDT 选择子
SelectorCode32          equ     LABEL_DESC_CODE32       - LABEL_GDT
SelectorVideo           equ     LABEL_DESC_VIDEO        - LABEL_GDT
; END of [SECTION .gdt]

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

        ; 初始化 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

        ; 为加载 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  处
; END of [SECTION .s16]

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

LABEL_SEG_CODE32:
        mov     ax, SelectorVideo
        mov     gs, ax                  ; 视频段选择子(目的)

        mov     edi, (80 * 11 + 79) * 2 ; 屏幕第 11 行, 第 79 列。
        mov     ah, 0Ch                 ; 0000: 黑底    1100: 红字
        mov     al, ‘P‘
        mov     [gs:edi], ax

        ; 到此停止
        jmp     $

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

用到的Descriptorpm.inc中定义,关于Descriptor定义的内容如下:

; 描述符
; 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 ; 共 8 字节

刚开始看到上面的代码,我有点束手无策。因为也是最近才开始学习汇编,上面的程序我连指令都认不全。所以下面这一节对上面程序中语法的部分做一些讲解

2. 关于实例中汇编语法的讲解

%include "pm.inc":包含文件。类似c语言中的包含.h文件。

org 07c00horg是origin的缩写。告诉编译器下一条汇编语句的偏移地址是07c00h

[SECTION .gdt]:AT&T汇编语言格式,用于定义一个节。这里是定义一个结构体数组,数组名称是GDT,数组内部是三个Descriptor结构。

LABEL_GDT: Descriptor 0, 0, 0Descriptor是在pm.inc中定义的宏,8个字节。上面有列出来内部的定义。个人猜测定义中的%1%2%3是这里传进去的参数,按照位置分别是1、2、3。猜测跟shell中的位置参数类似。(现在先猜测一下,到影响继续学习的时候再深究)。上面定义的Descriptor这个宏能够用比较自动化的方法把段基址、段界限和段属性安排在一个描述符中合适的位置。这儿也不是很了解,不知道自动化是如何实现的

GdtLen equ $ - LABEL_GDTequ是伪指令。这句话的意思是用GdtLen来代替$
- LABEL_GDT
。从这儿看类似于c语言中的define

GdtPtr          dw      GdtLen - 1      ; GDT界限
                dd      0               ; GDT基地址

这里定义一个结构体数组GdtPtr,共有6个字节。前2字节(处于低位)是GDT的界限;后4字节(处于高位)是GDT的基地址。

SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT:这句话定义GDT的选择子(sector)SelectorCode32。在下面讲解GDT里面会有详细介绍。

继续向下看

[BITS 16]:用来指明此节一个16位代码段。

lgdt [GdtPtr]lgdt指令用来将GdtPtr这个结构体装入寄存器GDTR

cli:关中断,对应的开中断指令是sti

这里面还出现了新的寄存器eax,下面的图说明了eaxaxahal的关系:

00000000 00000000 00000000 00000000
|===============EAX===============|--32个0,4个字节,2个字,1个双字
                  |======AX=======|--16个0,2个字节,1个字
                  |==AH===|        --8个0,1个字节
                          |===AL==|--8个0,1个字节

eax是32位的寄存器,但它实际上只是在原有的8086CPU的寄存器ax上增加了一倍的数据位数而已。所以eax和ax二者并不是独立的,而是整体与部分的关系。举例来说,对eax直接赋值,若更改了低16位自然会改变了ax值,同样ax又会影响eax整体。而ah,al寄存器和ax之间的关系也是如此。同样还有ebx,ecx,edx。(上面摘抄互联网并作了一些补充)

IA32还加了两个段寄存器fsgs。用来减缓es的压力。用法与es相同。

上面这些指令比较生疏。其他的指令在学习8086汇编的时候都是学过,比较熟悉。

那指令都认识了,但是对于上面代码的运行还是一头雾水,接下来对代码的含义进行分析。

3.
关于实例如何实现实模式到保护模式的切换

看上面的代码,

程序首先被加载到内存07c00h处,然后直接跳转到LABEL_BEGIN处。

在LABEL_BEGIN处,程序首先使dses寄存器指向与cs相同的段,上节里面说了,这是为了以后进行数据操作的时候能定位到正确的位置。然后初始化栈。

接下来初始化32位代码段描述符(32位代码段就是指程序最下面的[SECTION .s32])。这段初始化代码就是将下面那个32位代码段基址写到GDT中对应的描述符结构中。你看GDT结构体中LABEL_DESC_CODE32那一项的段界限与属性都定义好了,只有段基址没有定义,上面关于初始化32位代码描述符的作用就是初始化描述符中的基址。

要说上面的这步是干什么的,那么首先需要了解IA32的寻址过程了,下面有详细的介绍。不了解的需要先跳到下面关于GDT的讲解,再回来继续看。

到这里,GDT已经初始化好了,接下来的lgdt
[GdtPtr]
是把GDT的基地址和段界限加载到GDTR这个寄存器中。看看上面的GdtPtr结构体,它可不是随意定义的。它的结构与gdtr寄存器的结构是相同的,看看下面gdtr的结构,在对比上面介绍的GdtPtr,你就知道了。

32位基址 16位界限
H-------------------------------------------------------------------------L

再下面是关中断,因为进入保护膜是之后中断处理机制与现在是不同的,所以在进入之前需要关中断。如果不关中断将会出现错误。

关中断之后的代码就是纯粹为了进入保护模式做准备的了。这里主要有两个步骤:

首先打开地址线A20。关于A20,书上是这样说的:

那么什么是A20呢?这又是一个历史问题。8086中,“段:偏移”这样的模式能表示的最大内存是FFFF:FFFF,即10FFEFh。可是8086只有20位的地址总线,只能寻址到1MB,那么如果试图访问超过1MB的地址时会怎样呢?实际上系统并不会发生异常,而是回卷(wrap)回去,重新从地址零开始寻址。可是,到了80286时,真的可以访问到1MB以上的内存了,如果遇到同样的情况,系统不会再回卷寻址,这就造成了向上不兼容,为了保证百分之百兼容,IBM想出一个办法,使用8042键盘控制器来控制第20个(从零开始数)地址位,这就是A20地址线,如果不被打开,第20个地址位将会总是零。显然,为了访问所有的内存,我们需要把A20打开,开机时它默认是关闭的。这里打开A20的方式是让92h这个端口的第1位(从低位0开始)的值置为1

接下来将cr0这个寄存器的第0位置为1。为什么要这么做呢?这是因为当该位为0时,CPU运行于实模式,为1时,运行于保护模式。所以当将cr0的第0位置1之后,我们就相当欲闭合了进入保护模式的开关。

也就是说,“mov cr0, eax”这一句之后,系统就运行于保护模式之下了。但是,此时cs的值仍然是实模式下的值,我们需要把代码段的选择子装入cs。所以,我们需要第71行的jmp指令:

jmp dword SelectorCode32:0

根据寻址机制我们知道,这个跳转的目标将是描述符DESC_CODE32对应的段的首地址,即标号LABEL_SEG_CODE32处。

到这里,执行jmp指令后,就真正进入了保护模式。

进入保护模式后,就开始运行[SECTION .s32]段的代码。这段代码比较简单:就是在屏幕的第12行80列输出一个红色的P,然后进入无线循环。

至此,整个程序运行完毕。

上面的介绍中只是粗略的讲了一下GDT,下面对IA32为什么引入GDT进行详细介绍。

4.
GDT(Global Descriptor Table)

如果你熟悉Intel 8086汇编,那么你一定知道Intel 8086是16位的CPU,它有着16位的寄存器(Register)、16位的数据总线(Data Bus)以及20位的地址总线(Address Bus)和1MB的寻址能力。一个地址是由段和偏移两部分组成的,物理地址遵循这样的计算公式:

物理地址(Physical Address)=段值(Segment)×16+偏移(Offset)

其中,段值和偏移都是16位的。

从80386开始,Intel家族的CPU进入32位时代。80386有32位地址线,所以寻址空间可以达到4GB。所以,单从寻址这方面说,使用16位寄存器的方法已经不够用了。这时候,我们需要新的方法来提供更大的寻址能力。

在实模式下,16位的寄存器需要用“段:偏移”这种方法才能达到1MB的寻址能力,如今我们有了32位寄存器,一个寄存器就可以寻址4GB的空间,是不是从此段值就被抛弃了呢?实际上并没有,新政策下的地址仍然用“段:偏移”这样的形式来表示,只不过保护模式下“段”的概念发生了根本性的变化。实模式下,段值还是可以看做是地址的一部分的,段值为XXXXh表示以XXXX0h开始的一段内存。而保护模式下,虽然段值仍然由原来16位的cs、ds等寄存器表示,但此时它仅仅变成了一个索引,这个索引指向一个数据结构的一个表项,表项中详细定义了段的起始地址、界限、属性等内容。这个数据结构,就是GDT(还可能是LDT)。GDT中的表项也有一个专门的名字,叫做描述符(Descriptor)。

也就是说,GDT的作用是用来提供段式存储机制,这种机制是通过段寄存器和GDT中的描述符共同提供的。其中描述符有多种:代码段欲数据段描述符、系统段描述、门描述符。上面的程序用到了代码段的描述符,它的结构如下:

上面除了BYTE5和BTYE6中的一堆属性看上去有点复杂以外,其他三个部分倒还容易理解,它们分别定义了一个段的基址和界限。不过,由于历史问题,它们都被拆开存放。至于那些属性,我们暂时先不管它。

好了,我们回头再来看看代码,Descriptor这个宏用比较自动化的方法把段基址、段界限和段属性安排在一个描述符中合适的位置,有兴趣的读者可以研究这个宏的具体内容。本例的GDT中共有3个描述符,为方便起见,在这里我们分别称它们为DESC_DUMMY、DESC_CODE32和DESC_VIDEO。其中DESC_VIDEO的段基址是0B8000h,顾名思义,这个描述符指向的正是显存。

现在我们已经知道,GDT中的每一个描述符定义一个段,那么cs、ds等段寄存器是如何和这些段对应起来的呢?你可能注意到了,在[SECTION.s32]这个段中有两句代码是这样的:

mov ax, SelectorVideo
mov gs, ax

看上去,段寄存器gs的值变成了SelectorVideo,我们在上文中可以看到,SelectorVideo是这样定义的:SelectorVideo equ LABEL_DESC_VIDEO-LABEL_GDT。直观地看,它好像是DESC_VIDEO这个描述符相对于GDT基址的偏移。实际上,它有一个专门的名称,叫做选择子(Selector),它也不是一个偏移,而是稍稍复杂一些,它的结构如图3.5所示。

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
描述符索引 TI RPL

不难理解,当TI和RPL都为零时,选择子就变成了对应描述符相对于GDT基址的偏移,就好像我们程序中那样。这点还是不太了解

看到这里,你肯定已经明白了mov [gs:edi], ax的意思,gs值为SelectorVideo,它指示对应显存的描述符DESC_VIDEO,这条指令将把ax的值写入显存中偏移位edi的位置。

总之,整个寻址方式如下图所示:

上面关于GDT的内容引用自书本。

到这里,整个程序讲解完毕。

书上还有关于描述符属性的详细解释和突破软盘引导512字节的限制。

时间: 2024-08-03 01:50:20

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

一个操作系统的实现(4)-认识LDT

看到这里,你应该已经很了解GDT了,如果还不了解GDT.请看这篇文章:一个操作系统的实现(2)-认识保护模式,认识保护模式那篇文章的最后详细介绍了由16位寻址升级到32位寻址而引入的GDT. LDT(Local Descriptor Table):从名字上面就可以看出来它与GDT(Gobal Descriptor Table)的区别.GDT是全局描述符表,LDT是局部描述符表(相对于GDT). 下面仍然是从代码的角度讲解什么是LDT.主要讲解在上一节的基础上增加的代码.在文章的最后会附上所有代码

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

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

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

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

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

上节内容是从实模式进入到保护模式,只是进入保护模式打印了一个字母P.但是没有体现出保护模式的优势,也没有从保护模式中返回.这节就是要体验保护模式下读写大地址内存的能力和从保护模式返回到实模式. 这节要做的内容如下:首先在屏幕的第11行输出In Protect Mode now. ^-^.然后在屏幕第12行输出内存中起始地址为5MB的连续的8个字节.然后向这个以5MB开始的内存中写入ABCDEFGH.再次在第13行输出这8个字节.结果演示如下: 源代码300多行,很长,分段讲述,主要讲新增的部分

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

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

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

《Orange&#39;S:一个操作系统的实现》笔记(一)

感觉自己对于操作系统始终没有一个清楚的概念,尤其最近困扰于实模式.保护模式以及寻址方式等一些概念.转而一想,所有的程序,最终都是操作的计算机资源,需要和操作系统打交道,所以操作系统有必要深入了解一下.最终想要自己动手编写一个简单的版本,上网查.网友对于于渊的<Orange'S:一个操作系统的实现>和<30天自制操作系统>评价挺高的,先选<orange>为学习手册.<30>为参考手册,开始自己的操作系统之旅. 首先是平台的搭建问题,首先因本人编程一般都是在自己