Linux X86保护模式分段验证
2014-10-14 13:43
337 查看
Linux X86保护模式分段验证
机器语言指令中出现的内存地址,都是逻辑地址,需要转换成线性地址,再经过MMU转换成物理地址才能够被访问到。例如可以写一个简单的C函数获取变量的逻辑地址:
程序打印出: 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,如图:
![](http://img.blog.csdn.net/20141014134051560?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbGloYW5jaGVuZw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
图(1)gdtr寄存器
gdtr寄存器共64位,其中limit代表GDT表长界限,从0开始;address代表着表的基地址,(注意是线性地址)。
段选择符索引存在段寄存器中称为段选择子。段寄存器位宽为16bit,如图:
![](http://img.blog.csdn.net/20141014134118704?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbGloYW5jaGVuZw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
图(2)段选择子
端选择子包含三部分:段描述符索引(index),TI,请求特权级(RPL),段选择符索引代表着该段选择符在GDT或LDT表中的位置,TI确定段选择符在GDT表还是在LDT表中,TI = 1,则选择符在GDT中,否则在LDT中。PRL代表段选择子的特权级,共有4个特权级(0级,1级,2级,3级)。任务中每一个段都有一个特定的级别,每当程序试图访问某一个段的时候,将该程序拥有的特权级与端的特权级进行比较,以决定能否访问该段。
故而通过address + 8 * index即可计算出段描述符的地址。其中8代表段描述符的长度,为8字节,具体每一字节的含义如下:
![](http://img.blog.csdn.net/20141014133935312?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbGloYW5jaGVuZw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
图(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。可以通过以下程序来验证段选择子与段选择符:
内核态程序:
通过dmesg观察内核打印:
可以看出内核态CS段选择子值为96,即为0000000001100000,其中index为二进制1100即为12, TI为0即段选择符在GDT中,RPL为0即为最高级别。 段选择子为8字节数据,即为调试信息中的:0x00CF93000000FFFF,通过图(2)可以看出,该段的基地址为0x00000000.
用户态程序:
程序打印结果:
可以看出,用户态CS寄存器为123,即0000000001111011,其中index为二进制1111即为15, TI为0即段选择符在GDT中,RPL为3即为用户级别。GDT的基地址为0xf7bdc000,可以看出应用程序试图读GDT的时候,程序出现了段错误。这个原因主要是用户态程序的活动范围为0~0xC0000000, 超过这个范围的地址,用户程序无读写权限,故而出现了段错误。但是可以记录段选择符索引,通过内核态程序获取。可以得出该段的段基址也是0x00000000.
所以说,Linux中逻辑地址等于线性地址。为什么这么说呢?因为Linux所有的段(用户代码段、用户数据段、内核代码段、内核数据段)的线性地址都是从0x00000000 开始,长度4G,而线性地址=段基地址+逻辑地址,而所有的段基地址为0x00000000, 也就是说逻辑地址等于线性地址。
机器语言指令中出现的内存地址,都是逻辑地址,需要转换成线性地址,再经过MMU转换成物理地址才能够被访问到。例如可以写一个简单的C函数获取变量的逻辑地址:
void show_var_addr() { int a = 0; printf(“var a’s address = 0x%08x\n”, (int)&a); } |
那么段基值怎么获取呢?在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"); } |
[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] ----------- |
用户态程序:
#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 段错误 (核心已转储) |
所以说,Linux中逻辑地址等于线性地址。为什么这么说呢?因为Linux所有的段(用户代码段、用户数据段、内核代码段、内核数据段)的线性地址都是从0x00000000 开始,长度4G,而线性地址=段基地址+逻辑地址,而所有的段基地址为0x00000000, 也就是说逻辑地址等于线性地址。
相关文章推荐
- x86实模式到保护模式及Linux启动协议的演变
- 学x86保护模式时对linux 0.00作的C语言改写
- <<Linux内核完全剖析 --基于0.12内核>>学习笔记 第4章 80x86保护模式及其编程 4.3 分段机制
- linux-0.11中保护模式建立过程的分析[2]
- linux-0.11中保护模式建立过程的分析[3]
- Linux0.11内核--32位保护模式GDT(全局描述符表)
- linux内核完全剖析之(1)——80x86保护模式
- Linux kernel boot process——从实模式(real mode)到保护模式(protected mode),再到分页(paging)
- linux下保护模式之CPL,RPL,DPL总结
- x86保护模式的几点思考
- <自已动手写操作系统>学习扎记之保护模式(x86)
- 从linux0.11学习linux内核设计之模式转换:实模式-保护模式(1)
- linux-0.11中保护模式建立过程的分析[4]
- X86保护模式编程总结(4)
- X86保护模式编程总结(3)
- x86保护模式的几点思考——IRQ、中断号和中断向量
- 80x86保护模式系列教程(2)分段管理机制
- X86保护模式编程总结(2)
- X86保护模式编程总结(6)
- x86保护模式的几点思考