当我们谈论开机的时候我们都在谈论什么(二)——保护模式

本文所谈论到的cpu是Intel的x86体系架构的cpu,作者才疏学浅,其他体系的暂不讨论。本文跳转的保护模式是32位保护模式,所涉及到的通用寄存器均是32位。

读者可以未经作者允许随意转载,但请注明出处并且保证文章的完整性

本文主要谈一下几个方面:
1、保护模式和实模式是个什么鬼
2、怎么由保护模式进入实模式
3、实模式跳转到保护模式的示例代码

在讲之前照例讲几句闲话:
1、实模式是操作系统之源,没有它,操作系统就是无本之木、无水之源
2、现在操作系统的书很少会有涉及这个方面的内容,可能这部分不属于操作系统原理的范畴,但是并不代表不重要
3、没有这部分知识,对OS的一些谈论都只能停留在我知道XXX是这么设计的而不是我尝试过

一、实模式和保护模式

X86体系的cpu有两种工作模式:实模式和保护模式,实模式就是之前8086的运行状态,寻址能力差一些,没有保护机制。进而引入了保护模式,保护模式说白了也就是在保护内存的访问,以防止程序访问了其他程序的数据;实模式下16位数据线和20位地址线,所以寻址能力只有1M,但是保护模式32条地址线 ,寻址能力可以达到4G。
计算机刚开机的时候运行在实模式下(这个刚开机指的是刚刚加电,CPU RESET值产生了以后,这时候CPU就可以工作了,顺便一提这时候BIOS还在ROM中没有加载进来),之后经过一系列的操作,进入保护模式。
关于实模式和保护模式,暂且讨论这么多。

二、怎么进入保护模式

1、实模式和保护模式下的寻址

前面讲到实模式有20位地址线,但是保护模式有32位地址总线。实模式下的地址是段基址:偏移量。段基址存在cs(代码段寄存器),ds(数据段寄存器中)等等这些段寄存器中。16位段寄存器的内容乘以16(10H),加上16位偏移地址形成20位的物理地址,所以实模式下可以表示段的最大长度是64K(16位偏移量可以寻64k的地址)。

到了保护模式下,地址变成了32位。因为保护模式允许段的长度是0~4G,也就是说段基址和偏移量都变成了32位。那么问题来了, 16位的段寄存器(保护模式下段寄存器还是16位)怎么表示32位段基址呢?intel提出了一个解决方案:把段基址存在一个表里,段寄存器里边填充的内容就是子项这个表的索引,这个索引被称为段选择子,这个表被称为GDT(Global Description Table,全局描述符表),表里填充的每一项内容被称为描述符。

前面我们还提到了,保护模式对内存访问提供了保护机制。是怎么保护的呢?权限,这也是最节省空间的一种保护方式了。所以不难理解描述符并不仅仅是由段基址构成,还有各种各样的权限。

2、进入保护模式之前需要做什么

根据之前的描述首先要做的就是填充gdt表,没有这个表cpu就没办法寻址。
其次,386之后的每个cpu既支持实模式也支持保护模式,也就是说地址总线既可以20位也可以32位。这个是怎么做到的呢?很简单,计算机刚开机的时候地20位地址总线(A20)处于关闭状态,等要进入保护模式的时候再打开。地址总线不是20位就是32位,所以这里我们并不需要把后面的每一位挨个打开,只需要打开A20就好了。

这里还要提一下计算机怎么知道现在处于那个模式下的呢。答案就存在cr0寄存器里边。计算机有四个很重要的控制寄存器,cr0,cr2,cr3,cr4(cr1并当前没有使用)。这四个寄存器估计很多人对cr3很熟悉,cr2和cr3都是在用于支持分页机制的,这个不是本文的重点,不再详述。cr0是存储控制操作模式和处理器状态的寄存器,是个32位寄存器,但是里边有很大一部分当前并没有使用,我们本文只需要说明这个寄存器的第0位——PE位。这一位就是用来表示计算机当前处于实模式还是保护模式——0表示实模式,1表示保护模式。计算机刚开机的时候这一位是0,所以我们跳转到保护模式之前的最后一个步骤就是把这一位置1。

大概捋一下这个过程就是:
1、申请gdt并填充内容,
2、加载gdtr(专门给gdt用的寄存器)
3、打开A20
4、设置PE位
5、激动人心的jmp

没错就是这样的,这样就进入了保护模式了,有没有很激动,有没有很开心。那让我们撸起袖管、摩拳擦掌开始写代码吧~~~顺便趁着你开心讲明白,这部分代码要用汇编来实现。不过你不用担心,都是很简单的代码,连逻辑都没有就实现上面我们总结的五个步骤就可以了。

三、示例代码

贴上代码:

[section .gdt]
LABLE_GDT:          Descriptor       0,            0,0  ;空描述符
LABLE_DESC_CODE32:  Descriptor       0,SegCode32Len-1,DA_C + DA_32  ;

GdtLen      equ     $-LABLE_GDT ;gdt长度
GdtPtr      dw      GdtLen-1    ;gdt界限
            dd      0           ;gdt基地址
SelectorCode32      equ     LABLE_DESC_CODE32       - LABLE_GDT

DA_32       EQU 4000h
DA_C        EQU 98h
SegCode32Len        equ     $-LABLE_SEG_CODE32

%macro Descriptor 3
    dw  %2 & 0FFFFh                 
    dw  %1 & 0FFFFh             
    db  (%1 >> 16) & 0FFh           
    dw  ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) 
    db  (%1 >> 24) & 0FFh           
%endmacro 

上面都是一些需要初始化的数据,第一个是申请一个空的是一个空的gdt表,段基址段界限和段属性都是0,第二个是保护模式下要执行的代码所在的代码段的描述符,段基址是0,段界限是$-LABLE_SEG_CODE32,属性是40098h,在这里我先说这个代表的意思是这是一个32位、存在于内存中的、可执行代码段,描述符的格式每一本操作系统的书都有描述,请读者自行查阅,把40098h换成2进制并查看为1的位对应的意思即可,在此不详述。

下面来看代码的执行:

    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,LABLE_SEG_CODE32
    mov     word[LABLE_DESC_CODE32 + 2],ax
    shr     eax,16
    mov     byte [LABLE_DESC_CODE32 + 4],al
    mov     byte [LABLE_DESC_CODE32 + 7],ah

    ;为加载gdtr做准备
    xor     eax,eax
    mov     ax,ds
    shl     eax,4
    add     eax,LABLE_GDT
    mov     dword [GdtPtr+2],eax    ;GdtPtr+2表示gdt的基地址

    lgdt    [GdtPtr]

    cli             ;关中断

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

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

    jmp     dword SelectorCode32:0          ;修改cs值

用cs的值初始化ds(代码段寄存器),es(附加段寄存器),ss(堆栈段寄存器),并设置sp(堆栈指针寄存器)寄存器的初值位0100h。清除eax(32位通用寄存器)的值,对当前的cs进行一些加工填充到描述符里,对当前的ds进行一些加工用以填充gdt。加载gdtr,并关中断打开A20,修改PE位,跳转到保护模式下的代码段。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值