• 保护模式


    Table of Contents

    寻址方式

    实模式

    段寄存器*16+段偏移

    保护模式

    和实模式中的段相比,保护模式的段有下面几点不同

    实模式保护模式
    段基址16位32位
    段界限0ffffh不固定
    段属性固定不固定
    由上表可知,实模式下一个段寄存器可以来表示一个段,但是保护模式下不可以,所以引入了描述符的概念。

    描述符(descriptor)

    用来描述一个段,由段基址,段界限,段属性3部分组成,但是这3部分是拆开被放入到8个字节中的。

    ;;==========================================================
    ; 描述符图示
    ; |  7  |  6  |  5  |  4  |  3  |  2  |  1  |  0  |
    ; |base2|           |     base1       |   limit1  |
    ;       |           \_________________________________
    ;       |                                             
    ;       | 7 | 6 | 5 | 4 | 3--0 | 7 | 6 | 5 | 4 | 3--0 |
    ;       | G | D | 0 |AVL|limit2| P |  DPL  | S | TYPE |
    ;
    ; 说明:
    ; 1. P: 存在(Present)位。
    ; 2. DPL: 特权级(Descriptor Privilege level)。
    ; 3. S: 描述符类型。(1:存储断,0:系统断或门)。
    ; 4. TYPE: 读写执行等属性。
    ; 5. G: 粒度(Granularity)。(0:字节,1:4k)。
    ; 6. D: ⑴ 可执行段(0:16位,1:32位)。
    ;       ⑵ 向下扩展数据段(上部界限 0:64k,1:4G)。
    ;       ⑶ 堆栈,(0:sp,1:esp)。
    ; 7. AVL: 软件可利用位,就是没用位。
    ;===========================================================
    

    gdt(global descriptor table)

    一个程序会有好多段,每个段都有一个描述符,而根据段的不同,描述符又有全局描述符,局部描述符等,所有的全局描述符就被连续放在了gdt里,gdtr寄存器存放了gdt基址和大小

    选择子(selector)

    两个字节,高13位用来指示对应描述符在描述表中的位置,低3位用来指示特权级和描述符类型(gdt还是ldt)。当低3位为0时,由于描述符为8个字节,所以选择子刚好是描述符相对于描述符起始位置的偏移。

    ;===========================================================
    ; 选择子图示:
    ; | 15--3  | 2 | 1 | 0 |
    ; | index  |TI |  RPL  |
    ; 说明:
    ; 1. RPL(Requested Privilege Level)
    ; 2. TI(Table Indicator): (0:GDT,1:LDT)
    ;===========================================================
    

    如何寻址

    通过gdtr寄存器获取gdt位置,通过段寄存器(选择子)找到描述符在gdt中的位置从而找到了描述符,通过偏移和描述符中的段基址找到地址

    模式之间的跳转

    进入保护模式

    初始化描述符

    主要是段基址的初始化,段基址=实模式下的段*16 + 段首在实模式下的偏移

    lgdt

    初始化gdt基地址

    关中断,打开A20地址线

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

    打开保护位,进入保护模式

    mov eax, cr0
    or  al, 1
    mov cr0, eax
    jmp dword SelectorCode32:0
    

    返回实模式

    设置段寄存器

    1. cs以外的段寄存器的设置:实模式下段寄存器的界限和属性都是固定的,如果返回之前没有设置,那么段寄存器会沿用保护模式时的界限和属性,从而冲突导致崩溃。所以通常增加一个Normal段,他的界限和属性符合实模式下属性,将他的选择子赋值给断寄存器,从而让他们的界限和属性符合要求。
    2. 没有类似于mov cs, ax的语句,你无法直接给cs赋值。cs的界限和属性只能在描述符里设定或者继承跳转之前的cs,所以书中16位代码段的界限必须是0ffffh。

    关闭保护位,关闭a20地址线,开中断,返回实模式

    和进入保护模式前相反

    ldt

    ldt的作用

    把一些协调作用的指令封装,我们称之为函数;把一些协调作用的函数(可能还要再加一些指令)封装,我们称之为段(代码段),而把一些协调作用的断(包括代码段,数据段,各种段)封装,我们就用到了ldt。

    在我狭隘的认识中,封装的作用一是使整个程序的结构变得清晰而不是一团糟,二是避免重复劳动。想象一下,如果一个程序有几万个段,那gdt里面的几万张描述符想想就让人有些恐怖,而且选择子的索引位仅有13位,gdt里面也根本无法容纳这么多描述符,所以ldt来了。把一些协调作用的段封装,把他们的描述符放到一个表(ldt),而这个表同时也是一个段,那么gdt里只需要放一张这个表的描述符就可以了。

    ldt的使用

    更直接地说,怎样使得程序能找到ldt里的段,或者简单地说,如何寻址。

    回忆下如何找到gdt里面的段

    根据gdtr寄存器找到gdt(使用gdt之前要先ldgt,以便让gdtr指示gdt位置),根据选择子的中的索引位在gdt中找到描述符的位置,根据描述符的段基址找到段。

    找到ldt

    基本和gdt一样,不同的是ldtr寄存器指示了ldt的位置(同样使用ldt之前要lldt),而选择子的TI位指示了描述符在ldt里

    总结下保护模式下的寻址

    1. 根据选择子的TI位确定是在gdt里找还是ldt里找
    2. 根据gdtr或者ldtr找到gdt或者ldt
    3. 根据选择子的Index位找到描述符
    4. 根据描述符找到段

    特权级

    3个特权级

    CPL

    一般来说,是指当前代码所在段的特权级,存储在CS和SS中,当程序跳转到不同特权级的代码段时,CPL将随之改变。但是当跳转的目标代码段是一致代码段时,CPL不会改变,用一个不恰当的比喻来说,对于一致代码段,你拥有使用权,但是没有所有权。

    RPL

    如果对选择子的结构还有映像,应该知道除了指示位置的Index位和指示是gdt还是ldt的IT位,还有两位就是RPL。简单的来说,DPL是你访问一个段需要的权利,而CPL和RPL则是你所拥有的权利,详细点,CPL是你本身(也就是一直有)所拥有的权利,RPL则是你某次访问所暂时给你的权利。只有CPL和RPL都符合DPL的要求,访问才是合法的。

    DPL

    1. 数据段,调用门,TSS:规定了最低特权级
    2. 非一致代码段:规定了特权级
    3. 一致代码段,通过门访问的非一致代码段:规定了最高特权级

    调用门

    作用

    先用一个例子来说明下通常情况一致代码段和非一致代码段的情形。一致代码段和非一致代码段都是国家的东西,拥有相当高的特权级,一致代码段就是那些公园啊,公路啊,路灯啊这类的,虽然是国家的(特权级很高),但是作为平民我也能用,但是非一致代码段不行,说了国家的,那只有国家才能用。可以作为平民的我很想用,怎么办?走后门呗。调用门的作用就是让我可以用本来不能用的东西,即非一致代码段。要注意的是走后门要用call而不能用jmp。

    结构

    ; 门描述符图示
    ; |  7  |  6  |  5  |  4  |  3  |  2  |  1  |  0  |
    ; |  offset2  |   attr    |  selector |  offset1  |
    ;  ___________/           \___________________
    ; | 7 | 6 | 5 | 4 | 3--0 | 7--5 |     4--0    |
    ; | P |  DPL  | S | TYPE |  0   | Param Count |
    

    可以看出和描述符相比,他没有段基址,但是多了一个Selector,这很容易理解,因为他本身不指示一个段。

    使用

    不通过门代码段之间的跳转:

    call    SelectorGCode:0
    

    通过门:

    CALL_GATE:  Gate        SelectorGCode,  0,  0, DA_386CGate
    ;; ...
    SelectorCGate   equ CALL_GATE   - DESC_GDT
    ;; ...
        call    SelectorCGate:0
    

    进入ring3

    TSS

    一个很复杂的结构,暂时只需要知道他的4-28个字节中存放了3个ss和3个esp(对应了0, 1, 2三个特权级),在每个ss和esp的末尾填充了2字节的0。因为只有特权级是由低到高的特转需要从TSS中加载,所以没有特权级3的ss和esp。

    长跳转(call,特权级是由低到高)的执行过程

    1. 根据DPL确定使用TSS结构中哪个ss和esp。为方便,把TSS中的记0,原来的记为3。
    2. 检验ss0,esp0和ss0的描述符
    3. 暂时保存ss3和esp3,目前为止,ss=ss3,esp=esp3
    4. 加载ss0,esp0。此时ss=ss0,esp=esp0
    5. push ss3,push esp 3
    6. 根据门描述符的Param Count字段将参数压栈(Param Count代表有几个参数)。
    7. push cs3, push eip3(call指令的下一条指令的地址),目前为止,cs=cs3,eip=eip3。
    8. 加载cs0,eip0,此时cs=cs0,eip=eip0。到此进入了目标段

    长跳转(ret,特权级是由低到高)的执行过程

    1. 加载cs3,eip3(会有检验),add esp0, 8。
    2. add esp0, ParamCount*4。
    3. 加载ss3, esp3。
    4. add esp3, ParamCount*4。
    5. 检查ds,es,fs,gs所指向的段的DPL,如果小于CPL,一个空描述符会被加载。

    进入ring3

    实际上进入ring3的过程就是模拟ret的过程。由于我们的目的只是需要进入ring3,所以ret过程中的参数就不用了。

    ;; go to ring 3
    push    SelectorStack3  ; ss3
    push    TopOfStack3 ; esp3
    push    SelectorCodeR3  ; cs3
    push    0       ; eip3,让程序返回到段首
    retf
    

    回到ring0

    同样,回到ring0的过程就是call的过程。

    1. 加载tss
    2. 通过调用门调用ring0的代码段

    过程中特权级的一些总结

    1. retf之前,我们都处于ring0。
    2. retf之后,我们来到ring3的代码段,需要注意的是,在ring3代码段我们用到了video段,根据数据段访问规则,我们要把video段的特权级设置成ring3。
    3. 调用门的特权级:我们在ring3的代码段里访问了调用门,调用门的规则同数据段,所以门描述符的特权级(DPL)要设置成ring3

    分页

    通过分页机制,所有的线性地址被映射到了物理地址。

    分页机制概述

    整体机制

    分页机制,即线性地址到物理地址的映射机制,就是数学中的f。

    1. 通过cr3找到页目录基地址(一张表,里面有1024个表项)。
    2. 通过过线性地址的(31-22)在叶目录里找到相应的表项,即PDE。
    3. 通过PDE找到页表基地址(完整的话每个PDE对应一张页表,即一共会有1024张页表,而每个页表也有1024个表项)。
    4. 通过线性地址的(21-12)在页表中找到相应的表项,即PTE。
    5. 通过PTE找到物理页首地址。
    6. 通过线性地址的(11-0)在物理页中找到相应地址

    PDE

    页目录的表项,共1024项。PDE共4个字节。因为每个PDE都指向一个页表,而每个页表一共有1024项,每项(即PTE)为4个字节,即每个页表为4096个字节,假设各个页表连续摆放,那么相邻页表的基地址应该相差4096。和选择子类似,他的低12位放置属性,高20位存放地址。

    PTE

    同PDE类似。

    代码实现

    填充页目录(即1024个PDE)

        mov ax, SelectorPageDir
        mov es, ax
        mov ecx, 1024
        xor edi, edi
        xor eax, eax
        mov eax, PageTblBase | PG_P | PG_USU | PG_RWW
    .1:
        stosd
        add eax, 4096
        loop    .1
    

    填充页表(即1024*1024个PTE)

        mov ax, SelectorPageTbl
        mov es, ax
        mov ecx, 1024*1024
        xor edi, edi
        xor eax, eax
        mov eax, PG_P | PG_USU | PG_RWW
    .2:
        stosd
        add eax, 4096
        loop    .2
    

    将cr3的内容指向页目录表的首地址

    mov eax, PageDirBase
    mov cr3, eax
    

    打开cr0的PG位使得分页机制生效

    mov eax, cr0
    or  eax, 8000000h
    mov cr0, eax
    

    根据内存大小分页

    int 15h,获取内存信息

    • 参数
      1. eax: 0e820h
      2. ebx: 初始为0,之后是返回的值
      3. es:di: => ARDS
      4. edix:0534D4150h('SAMP')
    • 返回值
      1. cf: 0表示没有错误,1表示出错
      2. ebx: 如果0结束,并且如果cf为0则成功返回内存信息
    • 代码实现
          mov ebx, 0
          mov di, _MemChkBuf
      .loop:
          mov eax, 0E820h
          mov ecx, 20
          mov edx, 0534D4150h
          int 15h
          jc  MEM_CHK_FAIL
          add di, 20
          inc dword [_dwMCRNumber]
          cmp ebx, 0
          jne .loop
          jmp MEM_CHK_OK
      MEM_CHK_FAIL:
          mov [_dwMCRNumber], 0
      MEM_CHK_OK:
      

    内存信息结构

    _ARDStruct:
        _dwBaseAddrL:       dd  0
        _dwBaseAddrH:       dd  0
        _dwLenL:        dd  0
        _dwLenH:        dd  0
        _dwType:        dd  0
    

    获取可用内存数

    根据dwType的值得到

        cmp dword [dwType], 1
        jne .3
        mov eax, [dwBaseAddrL]
        add eax, [dwLenL]
        cmp eax, [dwMemSize]
        jb  .3
        mov [dwMemSize], eax
    .3:
    

    根据内存大小分配页目录和页表

    分页的作用

    对于程序员来说,“分层“这个概念应该是耳熟能详的了,无论是java的虚拟机机制,网络的7层模型,甚至操作系统本身,都用到了分层。而分页就是分层的一个应用。

    如果没有分页机制,我们用到的线性地址实际上就是物理地址,也就是说我们将会直接和物理地址打交道。而当分页机制启用之后,我们就不需要和物理内存去打交道了,我们只需要面对线性地址,至于我们用到的那块内存到底在哪里,分页机制会处理好。

    具体来说。假设我们设计了好几套PDE和PTE,即存在好几套分页机制,那么通过改变cr3的值,就可以运用不同的分页机制。这样做的好处是,就像个例子中所体现的,我们在某一套分页机制A下使用某个线性地址addr,同时可以在另一套分页机制B下使用同一个线性地址addr而不用担心对A机制下的那块线性地址addr的内存产生不良的影响,因为通过不同的分页机制A和B,addr被映射到了不同的物理地址。

    中断

    实模式下的中断

    学过win32汇编的人都知道,对一些常用的功能,我们不用自己写一条条类似与"mov"这样的指令自己去实现,微软已经实现了这些功能,并把他们封装在api里,我们只需要调用api就可以了。

    同样在实模式下,对一些常用的功能,硬件厂商也实现了这些功能,当然,他们没有将代码封装在api里,因此我们不能像在windows直接用"call"(甚至用invoke这样的伪指令)根据api函数名去调用api(直接通过函数名调用api涉及到输入输出表)。

    当计算机刚刚启动,在内存的开始位置,即从0x0000:0x0000开始,存放着中断向量表。每个表项占4个字节。当碰到类似"int 13h"这样的指令时,计算机首先在"0x0000:0x13*4"位置找到中断向量"0xfe 0xe3 0x00 0xf0",然后跳转到f000:e3fe。

    保护模式下的中断

    和实模式下不同,保护模式下用中断描述符表(idt)代替了中断向量表,并且他的位置不在是固定放在内存起始位置,而是由idtr寄存器指示,就像由gdtr指示全局描述符表一样。

    由于保护模式下的寻址方式和实模式下不同,idt中的表项自然不能和实模式一样简单的用4个字节(2字节的段基址和2字节的段偏移)。实际上,idt的里存的是一个一个的门描述符,门的概念之前已经介绍过。

    这样,保护模式下"int N"的调用过程就比较清楚了,首先通过idtr寄存器找到idt的地址,然后通过N找到idt中的表项,即某一个门描述符,然后通过这个门描述符的选择子,找到中断实现代码所在的段,通过门描述符的偏移,找到中断实现代码的具体位置。

    外部中断

    通常我们把之前所讲的类似"int N"这种形式的中断称为内部中断,或者叫软中断,还有一种中断,不是通过程序内部的代码来触发,而是通过一些硬件动作来触发,我们称之为外部中断,或者叫硬中断。

    看一下保护模式下的中断向量表(部分):

    向量号描述
    0x0除法错div或idiv指令
    0x2非屏蔽中断非屏蔽外部中断
    0x3调试中断int 3指令
    0x14-0x1fintel保留,未使用
    0x20-0xff用户自定义外部中断或int n指令

    对于"int n"这样来触发中断的方式,我们已经知道中断过程如何执行,但是对于外部中断,我们还需要做一件事情:建立硬件中断与向量号的对应关系。即什么硬件中断,对应什么向量号,一旦这个关系建立,就可以根据硬件中断找到中断向量号,然后就向用"int N"指令一样来找到相关的中断代码。

    8259A

    事实上,之前忽略了一个问题,对于"int N"这种方式,我们并没有什么疑问,因为他本来就在内存里,cs、eip这些个寄存器指示着它,那就该他执行了。但是对于外部中断,计算机(cpu)是如果知道这个中断发生了呢?

    答案是通过cpu上的NMI和INTR两根引脚来接受中断信息。对于NMI(对应中断向量号2)我们不做讨论,我们主要讨论的是INIR。

    cup通过INIR引脚与主8259A相连,主8259A有通过某一根中断信号线(IRQ2)与从8259A相连,每一个8259A有8根中断信号线,这样两块8259A就有15根信号线(主片的一根用来连从片了)。

    现在已经比较清楚了,对我们的电脑,外部中断(可屏蔽)有15种,每种对应一个向量号,在计算机刚刚启动的时候,bios将主片的8根信号线被设置为对应向量号08H~0FH。而在保护模式下,08H~0FH向量号已经被占用了,所以只能重新设置对应关系。

    设置8259A

    设置的过程实际上就是往主片(对应端口为0x20和0x21)和从片(对应端口为0xA0和0xA1)写如一些特定的ICW。当然,要遵循一定的顺序。

    往主片和从片写入ICW1(0x20和0xa0)

    ICW1的结构:

    5-7对PC系统必须为0
    4对ICW1必须为1
    31=level triggered模式
    21=4字节,0=8字节中断向量
    11=单个,0=级联
    01=需要ICW4,2=不需要
    由表可以得知,对于我们一般的PC机,ICW1的值应该是0x11。
    mov     al, #0x11           ! initialization sequence
    out     #0x20, al           ! send it to 8259A-1
    .word   0x00eb, 0x00eb      ! jmp $+2, jmp $+2
    out     #0xA0, al           ! and to 8259A-2
    .word   0x00eb, 0x00eb
    

    以上代码摘自linux-0.11内核源代码的/boot/setup.s文件。其中".word 0x00eb, 0x00eb"作用相当于nop,但是nop所耗费的时钟较短,要差不多6-7个nop指令才能达到这个效果。

    往主片和从片(0x21和0xa1)写入开始中断线的对应中断向量号(ICW2)

    由保护模式下的中断向量表可以得知,0x20之前的中断向量已经被使用,所以我们把主片和从片的向量号分别对应到"0x20-0x27"、"0x28-0x2f"。

    mov     al, #0x20           ! start of hardware int's (0x20)
    out     #0x21, al
    .word   0x00eb, 0x00eb
    mov     al, #0x28           ! start of hardware int's 2 (0x28)
    out     #0xA1, al
    .word   0x00eb, 0x00eb
    

    往主片写入用哪根线连从片,往从片写入连主片的哪根线(ICW3)

    通常我们用ir2连从片因此:

    mov     al, #0x04           ! 8259-1 is master, ir2 link to 8259-2
    out     #0x21, al
    .word   0x00eb, 0x00eb
    mov     al, #0x02           ! 8259-2 is slave, link to 8259-1's ir2
    out     #0xA1, al
    .word   0x00eb, 0x00eb
    

    往主从片写入用的什么模式(ICW4)

    其实ICW4还有一些其他位,但是现在仅关注0位,1表示8086模式,0表示MCS 80/85模式。

    mov     al, #0x01           ! 8086 mode for both
    out     #0x21, al
    .word   0x00eb, 0x00eb
    out     #0xA1, al
    .word   0x00eb, 0x00eb
    

    屏蔽

    到这里为止,我们已经建立好了硬件中断和中断向量号之间的关系,现在,我们可以根据需要开启或者屏蔽一些硬件中断。

    mov     al, #0xFF           ! mask off all interrupts for now
    out     #0x21, al
    .word   0x00eb, 0x00eb
    out     #0xA1, al
    
  • 相关阅读:
    构造函数详解
    左值和左值引用、右值和右值引用
    Lambda函数
    std::thread详解
    运算符重载
    友元函数和友元类
    xadmin list_filter 外键数据不显示
    中缀表达式转后缀表达式
    Centos 7 minimal 联网
    python 运用三目判断对象中多个属性 有且非空
  • 原文地址:https://www.cnblogs.com/hookbrother/p/3398282.html
Copyright © 2020-2023  润新知