01_保护模式:搞懂段寄存器

一、为什么会有保护模式

8086实模式中,内存寻址如下:

逻辑地址 = 段寄存器(16) : 偏移(16)
物理地址 = 段值 << 4 + 偏移

在这种模式下,只能访问1MB的内存。而且实模式中没有权限检查,没有隔离,内存任意读写,这就导致内存写错系统直接崩溃。

后来80386引入保护模式 (Protected Mode):

  • 地址空间扩展到4GB
  • 引入权限级别(Ring0~Ring3),用于区别内核态/用户态
  • 引入段机制 + 分页机制,能够对内存访问进行安全检查

在保护模式中,段寄存器不再直接参与“地址 = 段值 << 4 + 偏移”,而是变成了:

  1. 段寄存器中存放的是段选择子 (Selector),一个16位编号
  2. 通过GDT/LDT获取段描述符 (Descriptor)
  3. 从描述符中得到:Base(段基址)、Limit(段界限)Attributes(属性中包含段类型、特权级别等信息)
  4. 计算出线性地址:线性地址 = Base + 偏移
  5. 最终通过线性地址映射到物理内存中

二、保护模式下有哪些段寄存器?

x86里一共有 8 个段相关寄存器:

  • CS:代码段寄存器(指令取址用)

  • DS:数据段寄存器(普通数据访问默认用)

  • SS:栈段寄存器(push/pop、call/ret 等用)

  • ES / FS / GS:额外的数据段,主要给“字符串指令”或“特殊用途”用

  • LDTR:局部描述符表寄存器(指向LDT)

  • TR:任务寄存器(指向当前TSS)

三、段选择子(Selector)

段寄存器可见的部分只有16位,这16位就是段选择子:

blog.csdnimg.cn/direct/80a873ce558a4bd0b2b02fb8cfdf8c19.png)

  • Index(高 13 位):段描述符在 GDT/LDT 中的下标

  • TI(Table Indicator,第 2 位):

    • 0:在 GDT 里找
    • 1:在 LDT 里找
  • RPL(Requested Privilege Level,低 2 位):请求特权级,一般是0或3

当前我们使用windows xp进行实验,使用ollydbg打开notepad.exe:
在这里插入图片描述
其中DS/ES/SS都是0x23,我们把它拆成二进制看一下:

0x23 = 0010 0011b

Index =13= 0000000000100b = 4
TI    =2= 0  → 在GDT中查
RPL   =2= 11b = 3  → Ring 3(用户态)

所以0x23的含义为在GDT中取第4项描述符,这个段是以Ring 3权限访问的。

四、段描述符(Descriptor)

段描述符就是真正定义这个段的结构,在GDT或者LDT中可以查看。段描述符的基本布局如下:
在这里插入图片描述

  • Base(基址,总共32位)
    • 低16位:Base[15:0]
    • 中8位:Base[23:16]
    • 高8位:Base[31:24]
  • Limit(段界限,总共20位)
    • 低 16 位:Limit[15:0]
    • 高 4 位:Limit[19:16]
  • 属性(Attribute)
    • Type:代码段/数据段/系统段,是否可读写、是否可执行等
    • S:描述符类型(系统段/代码数据段)
    • DPL:描述符特权级(0~3)
    • P:存在位(Present)
    • G:粒度位(Granularity)
    • D/B:默认操作数大小或者栈指针大小(32/16 位)
    • AVL 等少量辅助位

对于Limit只有20位,看似只能描述1MB(20位)空间。但实际上需要配合G位来确定这个段的大小:

  • G = 0:按字节粒度
    段大小 = Limit + 1 字节
  • G = 1:按 4KB 粒度
    段大小 = (Limit << 12) | 0xFFF

所以在G=1时,段能够描述4GB空间,线性地址范围:

0x00000000 ~ 0xFFFFFFFF

这就是所谓的平坦段,整个0~4GB被看成一个大段,配合分页机制来做真正的隔离。

五、GDT 与 GDTR

1. GDT:全局描述符表

  • GDT 是一个数组:每项 8 字节,每项一个描述符
  • 基地址 + 限长存放在一个特殊寄存器 GDTR
  • 所有 TI=0 的段选择子,都是在GDT中查描述符

在 Windows XP 下,系统启动后会把 GDT 放在内核空间某个地址上。

2. GDTR 的结构与读取

GDTR 是一个48位寄存器:

  • 低 16 位:GDT限长(字节数 - 1)
  • 高 32 位:GDT基地址

在代码里不能直接用 mov 读 GDTR,需要专门的指令:

  • SGDT m:把 GDTR 的 48 位值存到内存操作数 m

    • 权限要求:Ring 0/3 都可以使用(XP 下用户态也可以用)
  • LGDT m:从内存中加载 GDTR

    • 权限要求:只允许 Ring 0

使用C语言进行读取,代码如下:

#include "stdafx.h"

#pragma pack(push, 1)
typedef struct _GDTR_BUF
{
    unsigned short limit;
    unsigned int base;
} GDTR_BUF;
#pragma pack(pop)

int main()
{
    GDTR_BUF gdtr = { 0 };
    unsigned char buff[6]; 
	
    __asm {
        sgdt buff
    }
	
    gdtr.limit = *(unsigned short*)&buff[0];
    gdtr.base  = *(unsigned int*)&buff[2];
	
    printf("GDT Limit = 0x%04X\n", gdtr.limit);
    printf("GDT Base  = 0x%08X\n", gdtr.base);
	

    return 0;
}

使用windbg读取如下:

0: kd> r gdtr, gdtl
gdtr=8003f000 gdtl=000003f

六、LDT 与 LDTR

GDT 是全局的,对整个系统(或者每个 CPU)唯一;
LDT(Local Descriptor Table)是局部的,原本设计是给每个任务/进程有自己的私有段。

  • LDT 的基址 + 限长存放在 LDTR 寄存器里
  • LDTR 本身的值来自 GDT 中的一个“系统段描述符”(LDT Descriptor)
  • 当段选择子的 TI = 1 时,就去 LDT 找对应的段描述符

在 Windows XP 中,普通应用基本不会使用LDT;因为windows使用了平坦模式。所有进程都共用 0~4GB 的线性空间,依靠页表来做进程隔离,不需要给每个进程单独搞一个LDT私有段表。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值