Linux X86保护模式分段验证

来源:互联网 发布:qq魅力值软件 编辑:程序博客网 时间:2024/05/29 09:04

Linux X86保护模式分段验证

机器语言指令中出现的内存地址,都是逻辑地址,需要转换成线性地址,再经过MMU转换成物理地址才能够被访问到。例如可以写一个简单的C函数获取变量的逻辑地址:

void show_var_addr()

{

int a = 0;

printf(“var a’s address = 0x%08x\n”, (int)&a);

}

程序打印出: var a’s address =0x80495b0, 0x80495b0则为逻辑地址,必须加上隐含的DS 数据段的基地址,才能构成线性地址。也就是说 0x80495b0 是当前任务的DS数据段内的偏移。

那么段基值怎么获取呢?在8086分段中,我们知道段基值存放在段寄存器cs,ds,ss等段寄存器中;而在x86保护模式分段中,段基值存放在段选择符中,每一个段选择符由8个字节组成,这8个字节包含的信息有段基值,段所属特权级等信息。而段描述符存放全局段描述符表GDT中或局部段描述符LDT中,GDT中共有32个段描述符表项,故而占用256个字节。在linux中,绝大部分使用GDT, 而LDT使用较少,具体原因这里先不分析。32个段描述符存放在GDT中,只要知道GDT的基地址,以及段选择符索引就可以获取段选择符,进而获取段基址。

其中,GDT基地址存放在寄存器gdtr中,gdtr寄存器位宽为64bit,如图:


图(1)gdtr寄存器

gdtr寄存器共64位,其中limit代表GDT表长界限,从0开始;address代表着表的基地址,(注意是线性地址)。

段选择符索引存在段寄存器中称为段选择子。段寄存器位宽为16bit,如图:


图(2)段选择子

端选择子包含三部分:段描述符索引(index),TI,请求特权级(RPL),段选择符索引代表着该段选择符在GDT或LDT表中的位置,TI确定段选择符在GDT表还是在LDT表中,TI = 1,则选择符在GDT中,否则在LDT中。PRL代表段选择子的特权级,共有4个特权级(0级,1级,2级,3级)。任务中每一个段都有一个特定的级别,每当程序试图访问某一个段的时候,将该程序拥有的特权级与端的特权级进行比较,以决定能否访问该段。

故而通过address + 8 * index即可计算出段描述符的地址。其中8代表段描述符的长度,为8字节,具体每一字节的含义如下:


图(3)端描述符

BASE代表段的基地址,共32位,分3部分存储在端描述符中;G代表粒度标志,如果该位为0,则段大小以字节为单位,否则以4096字节的倍数计;Limit共20位,决定段的长度,如果G为0,则段的大小在1字节到1MB之间变化,否则将在4KB到4GB之间变化;DPL为描述符特权级字段,用于限制段的存取,表示访问这个段要求CPU的最小优先级,因此DPL设为0的段只能当CPL为0时,才能够访问。

已知Linux中,设置GDT的第12和13项段描述符是 __KERNEL_CS 和__KERNEL_DS,第14和15项段描述符是__USER_CS 和__USER_DS。内核任务使用__KERNEL_CS和__KERNEL_DS,所有的用户任务共用__USER_CS和__USER_DS,也就是说不需要给每个任务再单独分配段描述符。内核段描述符和用户段描述符虽然起始线性地址和长度都一样,但DPL(描述符特权级)是不一样的。__KERNEL_CS和__KERNEL_DS 的DPL值为0(最高特权),__USER_CS 和__USER_DS的DPL值为3。可以通过以下程序来验证段选择子与段选择符:

内核态程序:

int gdt_test_init_module(void)

{

       int i;

       short  cs;

       struct desc_ptr gdt_descr;

       printk(KERN_INFO "--------\n");

       /* test!!! */

       asm volatile("mov %%cs, %0" : "=m" (cs));

       printk(KERN_INFO "cs = %d\n", cs);

       asm volatile("sgdt %0" : "=m" (gdt_descr));

       printk(KERN_INFO "size = %d, addr = 0x%08x\n", gdt_descr.size, gdt_descr.address);

       char* start =  gdt_descr.address + 8 * ((cs & 0xFFF8) >> 3); //cs段选择符首地址

       for(i = 0; i < 8; i++) {

              printk(KERN_INFO "%x ", *(start + i) & 0xFF);

       }

       printk(KERN_INFO "-----------\n");

}

 

通过dmesg观察内核打印:

[166206.645046] --------

[166206.645048] cs = 96

[166206.645050] size = 255, addr = 0xf7bdc000

[166206.645051] ff

[166206.645052] ff

[166206.645053] 0

[166206.645053] 0

[166206.645054] 0

[166206.645055] 93

[166206.645055] cf

[166206.645056] 0

[166206.645057] -----------

 

可以看出内核态CS段选择子值为96,即为0000000001100000,其中index为二进制1100即为12, TI为0即段选择符在GDT中,RPL为0即为最高级别。 段选择子为8字节数据,即为调试信息中的:0x00CF93000000FFFF,通过图(2)可以看出,该段的基地址为0x00000000.

用户态程序:

#include <stdio.h>

typedef struct {

    unsigned short size __attribute__((packed));

    unsigned int address __attribute__((packed));

} gdt_t;

 

int main(void)

{

    gdt_t gdt;

       short cs;

       int i;

       printf("--------\n");

       asm volatile("mov %%ds, %0" : "=m" (cs));

       printf("cs = %d\n", cs);

       asm volatile("sgdt %0" : "=m" (gdt));

    printf("size = %d, addr = 0x%08x\n", gdt.size, gdt.address);

       char* start =  gdt.address + 8 * ((cs & 0xFFF8) >> 3);

       for(i = 0; i < 8; i++) {

              printf("%x \n", *(start + i) & 0xFF);

       }

       printf("-----------\n");      

    return 0;

}

程序打印结果:

--------

cs = 123

size = 255, addr = 0xf7bdc000

段错误 (核心已转储)

可以看出,用户态CS寄存器为123,即0000000001111011,其中index为二进制1111即为15, TI为0即段选择符在GDT中,RPL为3即为用户级别。GDT的基地址为0xf7bdc000,可以看出应用程序试图读GDT的时候,程序出现了段错误。这个原因主要是用户态程序的活动范围为0~0xC0000000, 超过这个范围的地址,用户程序无读写权限,故而出现了段错误。但是可以记录段选择符索引,通过内核态程序获取。可以得出该段的段基址也是0x00000000.

 所以说,Linux中逻辑地址等于线性地址。为什么这么说呢?因为Linux所有的段(用户代码段、用户数据段、内核代码段、内核数据段)的线性地址都是从0x00000000 开始,长度4G,而线性地址=段基地址+逻辑地址,而所有的段基地址为0x00000000, 也就是说逻辑地址等于线性地址。

0 0
原创粉丝点击