zoukankan      html  css  js  c++  java
  • 内核01

    段描述符

    数据段描述符

    1566800109674

    代码段描述符

    1566800127026

    系统段描述符

    1566800138206

        A - 访问                             E - 向下扩展
         AVL - 供程序员使用                   G - 粒度
         B - BIG                           P - 段是否有效
         C - CONFORMING                        R - 可读
         D - 默认                              W - 可写
         DPL - 描述符特权级

    通用描述符

    1566800368599

       L     - 64位代码段(IA-32E模式)
       AVL   - 可供系统软件使用
       BASE   - 段基地址
       D/B   - 默认操作大小(0=16位段;1=32位段)
       DPL   - 描述符特权级别
         G   - 粒度
    LIMIT   - 段限长
         P   - 段是否有效
         S   - 描述符类型(0=系统段 , 1=代码段或数据段)
       TYPE   - 段的类型

    描述符概述

    在实模式中(以前16位CPU所使用的模式) , 在内存中的任何地址上都能够执行代码, 所有的内存地址都是可以被读写的, 这非常不安全. 然而并没有其它手段能够限制或者说禁止代码去读写某个内存地址(例如,在保护模式下,读写地址0是错误的,会导致程序崩溃,在实模式下就不会崩溃). CPU为了提供限制/禁止的手段, 提出了保护模式. 保护模式实际上就是保护了内存, 使得内存中能够被执行代码,能够被读写的地址可以被人为得控制. 在保护模式中, 系统和用户进程是被隔离开的. 用户进程无法修改系统的内存,也无法执行系统的代码.这些也是通过保护模式达成的功能. 保护模式所实现的功能, 很大程度上依赖分段机制. 在实模式下, 分段机制很简单, 它没有限制段是否的读,写,执行属性, 也没有限制一段内存有什么权限,能够在什么权限下被读,写,执行, 只是规定了, 要使用内存, 就需要使用段基地址*16 + 段内偏移的方式来寻址. 在保护模式下, 它兼容实模式下的寻址方式(段基地址*16 + 段内偏移),并且在这基础之上给一个段增加了段基地址, 段的长度, 段的属性这三个属性以实现保护模式下的部分功能. 在保护模式下, 描述一段的基地址在哪里, 段有多长,段有何属性的结构被称之为段描述符.其结构如下所示:

    段描述符结构

    typedef struct Descriptor{
       unsigned int base; // 段基址
       unsigned int limit; // 段限长
       unsigned short attribute; // 段属性
    }

    在保护模式下, 增加了很多机制, 使得段产生了不少种类:

    • 数据段(用于存储数据,供程序读写)

    • 代码段(用于执行代码)

    • 系统段(用于操作系统提供特殊功能)

    每个段描述既能够描述出一段内存从哪开始,到哪结束, 还能描述这个段是什么类型(代码段,数据段,系统段) ,当然, 也能够描述这个段是否可读,是否可写,是否可执行, 甚至还能描述这个段的权限是什么, 在什么权限下才能使用这一段内存.

    段基址

    在描述中总长度为32位(4字节) . 表示段的开始地址.

    段限长

    在描述中总长度为24位, 表示段的最大长度, 也就所, 此位段表示的值最大为: 0xFFFFF, 也就是1Mb , 此长度表示的是单位, 至于一个单位的长度到底是多少字节, 依赖于段属性的G位(粒度位)

    如果粒度位(G位)等于0 , 段的大小范围为1字节1Mb(0~0xFFFFF) , 每个单位为1字节.

    如果粒度位(G位)等于1 , 段的大小范围是1字节4Gb(0~0xFFFFFFFF), 每个单位为4Kb

    段属性

    P位

    P位用于记录当前段描述符是否有效.

    p==0 : 无效, 系统不会使用该段描述符

    p==1 : 段描述符有效.

    S位

    描述符类型(0=系统段 , 1=代码段或数据段), 这个位的值决定了Type字段的值是何种含义.

    Type

    当S位等于1的时候, Type字段描述的是数据段或代码段

    当Type字段的最高位(在段描述符中的11位)等于0时, 是数据段

    当一段是数据段时, Type字段的(8,9,10位分别为A,W,E, 其中,

    A - 数据段是否已经被访问, 等于1表示已经被访问

    W - 数据段是否可写, 等于1表示可写.否则为只读

    E - 数据段的扩展方向, 等于1表示向下扩展(向下扩展也可称为向外扩展) , 等于0时表示向上扩展(向内扩展)

    1566800429313

    代码段/数据段

    当Type字段的最高位(在段描述符中的11位)等于1时, 是代码段

    当一个段是代码段时, Type字段的8,9,10位分别为A,W,C, 其中, A,W的作用和数据段一样.

    C` - 段是否是一致代码段, 等于1表示是`一致代码段`, 等于0表示是`非一致代码段

    1566800481129

    系统段

    当S位等于0的时候 , Type字段的值描述的是系统段. 系统段中的描述符类型一般都是门描述符. 当这个描述符是一个段描述符之时 , Type字段就没有像代码段或数据段中的A,W,E标志了, Type字段的值决定了这个描述符的作用: 1566800498168

    D/B位

    D/B位的会作用到代码段(CS段寄存器) , 栈段(SS段寄存器),数据段(DS,ES段寄存器),当使用这些段寄存器时, 将会受到不同的影响:

    • 可执行代码段(CS段)

      此标志位被称为D位, 这个位会影响指令和操作数的寻址模式.

      D/B == 1 - 指令默认的寻址模式是32位,操作数默认为32位或8位.

      D/B == 0 - 指令默认使用16位寻址模式, 操作默认大小为16位或8位.

      指令前缀67H可以用来切换寻址模式. 例如, 当前D==1时, 寻址模式是32位, 切换之后,寻址模式就变成16位模式

      指令前缀66H可以用来切换操作数大小, 例如, 当前D==0时, 操作数大小默认为32,切换之后,操作数大小为16位.

    • 栈段(SS段)

      此标志位被称为B位,

      B == 1 - 默认使用32位的ESP寄存器操作栈

      B== 0 - 默认使用16位的SP寄存器操作栈

    • 向下展开的数据段

      CPU虽然提供了这个机制, 但是操作系统并没有使用这个机制

    段寄存器和段描述符

    CPU提供了段描述之后, 操作系统就可以使用这种机制来做出各种各样的限制. 例如, 以下汇编代码

    mov eax , dword ptr ds:[0x403000]

    在这条汇编指令当中, 保护模式的分段机制在无形中产生对该指令产生了影响:

    • 指令正在访问的地址是 段基地址*16 + 段内偏移

      如果在16位的实模式下,一般就是ds*16+0x403000 , 但是在32位的保护模式下, 16位的段寄存器并不能存储一个64位的段描述符.

    • 段寄存器保存的值被称为段选择子.

    • 真正的段描述符存储在内存中, 由GDTR寄存器记录其基地址和大小. 那么 , 16位的段寄存器如果才能和64位的段描述符对应起来?

    全局描述符表(GDT)

    在一个系统中, 描述符的种类有多个, 分别有数据段,代码段, 系统段. 系统段又分为多种,有调用门,中断门,陷阱门,任务门. 因此, 在一个系统中, 描述符是存在多个的. 这些描述符被统一打包存储在内存中, 它们所形成的一个数组被称之为全局描述符表. 全局描述符的小标则保存于16位的段寄存器中. 一个16位的段寄存器实际由以下部分组成: 1566800540924 段寄存器实际的长度为96位, 16位的值, 只是寄存器的可见部分, 段寄存器还有80位是隐藏部分 , 这个隐藏部分只能被CPU所操作,无法通过任何指令来操作它. 这可见部分的16位的值也并非全部用于保存全局描述符的下标, 它被划分为以下格式: 1566800552386 也就是说, 只有13位是用于保存全局描述符表的下标. T1 - 用于记录,保存的下标是GDT(全局描述符表)的还是LDT(本地描述符表)的(windows操作系统没有LDT) RPL - 当前请求级别 , 用作权限检查. 一共有4个值: 0~3 , 数值越小,权限越大, 0代表最高权限.

    由于段寄存器用于保存段选择子, 因此, 给一个段寄存器赋值,就不单单是赋值一个数字了,例如:

    mov ax,2Bh
    mov ds,ax

    这条指令可看成将0x2B赋值给ds寄存器, 实际不是. 将0x2b的二进制展开: 0000 0000 0010 1011 , 段选择子的格式为: 13 : 1 : 3. 那么在0x2b这个数中, 描述符表索引,T1位,RPL分别为:

                   0000 0000 00101 0  11
                   \_____________/ - --
                           |       |   |
                           |       |   +---> RPL = 3
                           |       +-------> T1 = 0(GDT)
                           +----------------> 索引 = 5        

    也就说, 0x2b这个数代表的是GDT表中第5个段描述符. 当前请求级别为最低权限的2. mov ds,ax这条指令执行之后做了什么? CPU执行这条指令后, 会将GDT表中第5个段描述符存储在段寄存器隐藏部分, 将段选择子存储到16位可见部分. 当然, 在做这些之前, CPU还需要做权限检查.

    权限检查

    段描述符中 , 有一个属性是DPL , 这个属性总共有2个二进制位. 大小和段选择子中RPL一样. 这二者正是用于作权限检查的. DPL指的是描述符特权级别, 它决定了在什么特权下,才能够访问此描述符. 其值从0~3共4个,0最表示最高权限, 3表示最低权限, 只有权限高于等于此DPL所记录的权限,才能访问描述符. RPL指的是请求级别, 值的是以何种权限去请求GDT表中的段描述符. 也就是说, 当指令mov ds,ax; // ax==2B 执行是, CPU会将数值2B作为段选择子,使用这个数的低两个二进制位作为RPL, 使用这个数的高13为作为描述符表的索引, 去获取描述符, 但如果要获取的描述符的DPLRPL要小(值越小权限越高) , 这条指令就无法取出这个段描述符,就完成不了赋值, CPU还会报一个异常.

    除此之外, CPU还有其它检查, 权限检查是最后一项 , 这些检查依次为:

    • 段描述符有效位检查

      检查段描述符的P为是否为1 , 如果为0 , 说明该描述符无效,CPU会触发一个异常.

    • 段类型检查

      例如, 将一个可读可写但不可执行的段加载到CS段寄存器是错误的,因为CS + IP执行的是代码,如果这个段不能执行,那就没有意义. 将一个只读的段加载到SS是错误的, 因为栈段是可以被改写的, 如果这个段加载到SS却不能修改,也是没有意义的.

    • 段权限数据读写检查

      无论当前执行的是什么指令, 都会使用CS段选择子中的低2位来作为CPL(表示当前执行级别) , 使用被加载的段选择子的低2位作为RPL(表示当前请求级别) , 使用被加载的段选择子的高13位作为描述符表的下标,并从表中取出段描述符,得到该描述符表的DPL(表示描述符特权级别). 当CPL > DPL 或者 RPL > DPL 时, 表示权限不够, 段描述符就会加载是被 也就是说, 当CPLRPL只要其中一个比DPL要大, 操作就会失败. 这是因为操作系统不希望用户程序能够随意切换段描述符, 在32位保护模式下, 每个段描述符的基地址都是0, 段限长都是4Gb, 但其类型是不同的. 系统在创建一个用户进程的时候, 会将用户线程的CS段寄存器的低2位置为2 ,也就是最低的权限的CPL. 这时, 这个系统就永远不能通过正常方式来获取高权限的段描述符, 也就无法访问和修改系统的内存了.

    • 段权限执行检查 在汇编中, 有一些指令是可以跨段跳转的. 例如:

    jmp 33:401000 
    call 33:401000

    上述两条指令被称为远跳转指令和远调用指令, 指令后的操作数分为两部分 : 段选择子和段内偏移. 这两条指令在执行后, 会将操作数中的段选择子对应的段描述符加载到CS中, 此时, CPU也会做检查:

    1. 检查请求的段描述符的`S`是否为1, 如果是1, 表示是请求的段是一个数据段或代码段, 再检查`Type`的高位是否       是1 , 如果是1 , 表示请求的段是代码段. 如果其中一个不是,就无法加载,指令无法执行,CPU还会报异常.
    2. 继续检查,`Type`的`C`标志, 如果是一致代码段, 则要求`CPL`>=`DPL` , 也就是只能低权限转移到高权限, 如果是非一致代码段, 则要求 ` CPL==DPL` 并且, `RPL<=DPL`, 也就是平级才能转移.
         但是转移之后, `CPL`和`RPL`不会改变.

    3. 如果检查`S`位是0 , 则表示请求的是系统段.              

    下面是伪代码:

    //段选择子的结构:
    // [描述符下标:13   | T1:1 | RPL:2]
    unsigned short segSel = 0x33;// 0x33就是要切换的段选择子,在指令jmp 33:401000 中给出。
    unsigned int RPL = segSel & 0x11;  // 取段选择自的低2位作为RPL
    unsigned int CPL = CS & 0x11 ; // 取`CS`段寄存器的值的低2位作为CPL
    if( SegDes.S == 1 && SegDes.Type & 0x1000 ){
        if(SegDes.Type.E == 1){ // 一致代码段
            if( CPL >= segDes.DPL ){
                CS = segSel; // 可以切换。
           }else{
                throw "异常";
           }
       }else{ // 非一致代码段
            if(CPL == SegDes.DPL && RPL <= SegDes.DPL){
                CS = segSel;
           }else{
                throw "异常";
           }
       }
    }else{
        throw "异常";
    }

    系统段描述符 - 门描述符

    很多时候, 用户层的代码需要切换到内核层执行代码。 因为有些代码执行时需要用到0环权限.

    切换0环权限实际就是将CS段寄存器的CPL改成0(也就是0环权限).

    但在3环时,CSCPL是2, 是无法直接修改的(如果要修改,就需要切换段选择子, 切换段选择子,就需要使用CPL,RPL和段描述符中的DPL比较)

    当段描述符中的S位等于0 , 表示这个描述符是一个门描述符。

    门描述符一般用于从3环进入到0环,并能够将3环权限切换成0环权限。

    门描述符的种类有:

    • 调用门(Windows操作系统没有使用此机制)

    • 中断门(IDT表中的中断处理函数就是这种门描述符)

    • 陷阱门(IDT表中的陷阱处理函数就是这种门描述符)

    • 任务门(用于任务切换)

    调用门

    调用门的出现是为了便于在不同的权限直接切换.

    1566800688420

    一个调用门中, 保存了以下信息:

    • 要执行的函数的地址

    • 要执行的函数的参数个数

    • 段选择子 (这个段选择子用于切换权限)

    字段解析:

    • Offset in Segment - 函数在段内的偏移, 实际就是函数的地址, 这个地址的被拆分成高16位和低32位保存.

    • Segment Selector - 段选择子, 使用调用门时, 这个选择子就是被切换成CS的选择子.

    • Param Count - 调用门中保存的函数的参数个数. 注意, 每个参数应当是4字节的.

    如果发生了权限切换, 系统也会将用户栈切换成内核栈, 也就是权限切换时, CS段寄存器的值会被改掉(不改掉切换不了切换段描述符) , 还会将SS段选择子,切换为内核的段选择子, 将ESP切换成内核的ESP, 切换前,SS,ESP的值都会被保存. 在切换回来之后,才进行还原.

    因为栈要进行切换, 当函数被调用时, 用户栈中的函数参数会被拷贝到内核栈, 因此, 需要在调用门描述符中指定参数的个数是多少, 否则系统将无法为函数拷贝参数到内核栈中.

    调用门的设置和使用

    因为在Windows中没有使用调用门, 因此, 在GDT表中是没有调用门描述符的.

    想要试验一个调用门,就需要自己使用windbg开启双击调试,并自行在GDT中设置一个.

    设置的方式如下:

    1. 构造如下结构体:

    typedef struct _CALLGATEDESCRIPT{
           unsigned int functionAddrLow : 16; // 被调用函数地址的低16位
           unsigned int SegmentSelector : 16; // 想要切换的段选择子
           unsigned int paramCount : 5; // 参数个数
           unsigned int none : 3; // 无
           unsigned int Type : 4; // 系统段类型, 必须为12(1100b: 32位调用门)
           unsigned int S : 1; // S位,必须为0
           unsigned int DPL : 2; // 描述符特权级别
           unsigned int P : 1; // 描述符有效位
           unsigned int functionAddrHig : 16; //被调用函数地址的高16位
       }CALLGATEDESCRIPT;
    1. 设置如下:

    unsigned long long createCallGateDescript(unsigned short selector,/要切换的段选择子/
                                               unsigned int functionAddr,/要通过调用门执行的函数/
                                                 int functionParamSize/函数参数所占用的字节数/)
       {
           CALLGATEDESCRIPT cgd = { 0 };
           cgd.P = 1; // 描述符有效位, 必须设置为1
           cgd.S = 0; // 系统段描述符, 必须设置0
           cgd.Type = 12; // 调用门描述符,必须设置为12
           cgd.DPL = 3; // 设置为3,表示此描述符可被3环所使用.
           cgd.SegmentSelector = selector;// 要切换的段选择子
           cgd.functionAddrHig = (functionAddr & 0xFFFF0000) >> 16;
           cgd.functionAddrLow = functionAddr & 0x0000FFFF;
           cgd.paramCount = functionParamSize / 4; // 参数个数,如果函数没有参数,填0,如果有参数,则填参数占用栈的字节数 / 4
           return (unsigned long long)&cgd;
       }
    1. 使用调用门的之前, 需要将调用门的描述符写进系统的GDT表中, 操作如下:

      1. 使用windbg双机调试, 输入指令rgdtr获取GDT表的地址

    kd> rgdtr
       gdtr=80b95000
    1. dq命令查看GDT表中哪些是空闲的, 值为0的都是空闲的.1566800760465

    2. 构造一个可以在3环代码中使用的段选择子, 构造规则:

      1. 段选择子的格式为: [ 描述符在表中的下标:13 | T1:1 | RPL:2]

      2. 假如在GDT表,第9项是空闲的, 门描述符将保存在此处, 则对应的段选择子为:

        十进制: 9-0-3 ==> 二进制: 1001-0-11 ==> 合并为 100 1011 ,十六进制数值为0x4B

      1. 在代码中使用此描述符:

    // 前4字节是EIP,后2字节是CS
    // 后2字节是0x004B, 此数值就是在第二步中构造出来的.
    char buff[ ] = { 0,0,0,0,0x4b,00 };
    _asm call fword ptr ds:[buff];

    代码:

    #include <iostream>
    #include <iomanip>


    #pragma pack(1)
    typedef struct _CALLGATEDESCRIPT {
        unsigned int functionAddrLow : 16; // 被调用函数地址的低16位
        unsigned int SegmentSelector : 16; // 想要切换的段选择子

        unsigned int paramCount : 5; // 参数个数
        unsigned int none : 3; // 无
        unsigned int Type : 4; // 系统段类型, 必须为12(1100b: 32位调用门)
        unsigned int S : 1; // S位,必须为0
        unsigned int DPL : 2; // 描述符特权级别
        unsigned int P : 1; // 描述符有效位
        unsigned int functionAddrHig : 16; //被调用函数地址的高16位
    }CALLGATEDESCRIPT;

    struct SELECTOR {
        unsigned short index : 13;
        unsigned short T1 : 1;
        unsigned short RPL : 2;
    };

    unsigned long long createCallGateDescript(unsigned short selector,unsigned int functionAddr, int functionParamSize)
    {
        CALLGATEDESCRIPT cgd = { 0 };
        cgd.P = 1; // 描述符有效位, 必须设置为1
        cgd.S = 0; // 系统段描述符, 必须设置0
        cgd.Type = 12; // 调用门描述符,必须设置为12
        cgd.DPL = 3; // 设置为3,表示此描述符可被3环所使用.
        cgd.SegmentSelector = selector;// 要切换的段选择子
        cgd.functionAddrHig = (functionAddr & 0xFFFF0000) >> 16;
        cgd.functionAddrLow = functionAddr & 0x0000FFFF;
        cgd.paramCount = functionParamSize / 4; // 参数个数,如果函数没有参数,填0,如果有参数,则填参数占用栈的字节数 / 4
        return *(unsigned long long*)&cgd;
    }

    int g_num;
    short g_ss;
    int g_esp;

    //通过调用门调用的 函数
    void _declspec(naked) GateFun()
    {
        g_num = 100;
        _asm mov [ g_esp ] , esp;
        _asm mov ax , ss;
        _asm mov word ptr [g_ss],ax
        _asm retf;
    }

    int main()
    {
        printf( "调用门函数地址:%08X " , GateFun );
        printf( "切换的段选择子:%04X " , 8 );/*8是内核中的代码 段选择子*/

        unsigned long long descript =
            createCallGateDescript(8/*8是内核中的代码段选择子*/ , ( unsigned int )GateFun , 0 );


        printf("请将这个段描述符写入到GDT[9]中: ");
        std::cout << std::hex <<std::uppercase<< std::setfill('0')<<std::setw(8)<< descript<<' ';
        system( "pause" );



        // 获取当前寄存器的值.
        _asm mov[ g_esp ] , esp;
        _asm mov ax , ss;
        _asm mov word ptr[ g_ss ] , ax

        printf( "调用前 esp=%08X, ss=%04X " , g_esp , g_ss );

        // 前4字节是EIP,后2字节是CS(0x004b)
        char buff[ ] = { 0,0,0,0,0x4b,00 };

        // 执行流程:
        // 1. 从buff这块内存中取出段选择子:0x4b
        //
        //       解释                       SEL T RPL
        //     十进制                         9 0 3
         // 2. 将段选择子分解,100 1011 ==> 1001 0 11, 得到GDT表中的下标:9
        // 3. 取出GDT表中第9项描述符, 是一个调用门描述符.
        // 4. 将调用门描述符中的段选择子加载到CS段寄存器
        // 5. 将调用门描述符中的函数地址设置到EIP寄存器.
        _asm call fword ptr ds:[buff];

        printf( "调用后 esp=%08X, ss=%04X " , g_esp , g_ss );
        printf( "g_num=%d " , g_num );
        system( "pause" );
    }

    windbg 输入eq 80b95068(上图右边红色框的地址).

    1566819613281

    任务门,中断门和陷阱门

    中断门和陷阱门的描述符其实和调用门一模一样.

    1566800820799

    1566800833104

    任务门描述符, 中断门描述符和陷阱门描述符都是保存在IDT(中断描述符表)中.

    其使用的过程是:

    产生中断或异常后,

    1. CPU会使用中断号找到IDT表中的中断描述符/陷阱描述符,

    2. 取出描述符后, 得到门描述符中的段选择子.

    3. 通过此段选择子找到GDT表中的段描述符,

    4. 从GDT表中取出的段描述符中得到段基地址

    5. 使用段基地址 + 门描述符中的函数偏移 , 得到函数地址.

    6. 调用该函数.

  • 相关阅读:
    闰年or平年判断
    输入一个日期判断是否正确的几种方法
    网页布局+下拉隐藏栏
    360导航布局
    [LeetCode] Longest Common Prefix
    [LeetCode] Length of Last Word
    [LeetCode] Valid Palindrome II
    [Qt] Qt信号槽
    [LeetCode] Split Linked List in Parts
    [LeetCode] Find Pivot Index
  • 原文地址:https://www.cnblogs.com/ltyandy/p/11414646.html
Copyright © 2011-2022 走看看