03_段权限检查机制

前言

在上一篇文章中,我们通过实验证明了段寄存器拥有96位的结构。 那么,这80位隐藏缓存中的数据(Base, Limit, Attribute)究竟是从哪里来的?

答案是:GDT(全局描述符表) 。

GDT可以理解巨大的数组,数组中的每一个元素(8字节)就是一个段描述符。而我们手中的段寄存器(如 CS, DS)里存的16位数据,即为数组的索引,我们称之为段选择子 。

本节我们将继续分析这两个结构,并研究它们是如何配合CPU完成权限的检查(Ring0/Ring3 隔离)。

一、 段描述符 (Segment Descriptor)

段描述符是GDT表中的基本单元,每个描述符占用8个字节(64 位)。它详细定义了一个内存段的基址、大小以及最重要的权限属性。

1. 为什么结构看起来是碎片化的?

观察下图,你会发现Base(基址)Limit(界限) 并不是连续存放的,而是被拆得四分五裂:
在这里插入图片描述

  • Base (32位) :被拆分为三段 (Base 31-24, Base 23-16, Base 15-0)。
  • Limit (20位) :被拆分为两段 (Limit 19-16, Limit 15-0)。

设计原理:向后兼容性。这种设计并非随意为之,而是Intel架构演进的历史产物:

  • 80286时代:处理器是16位的,地址线只有24位。当时的描述符虽然也是64位,但高位有大量保留位。
  • 80386时代:处理器升级为32位,地址线扩展到32位。为了让基于286编写的操作系统能在新CPU上直接运行,Intel必须保持描述符的低位结构不变。
  • 结果:硬件设计者只能利用原先未使用的高位保留字段,将 Base扩充的高8位和Limit扩充的高4 位填充进去。这种架构扩展策略虽然导致了视觉上的结构碎片化,但在硬件层面实现了完美的兼容。

2. 核心属性详解 (Attributes)

在 64 位数据中,除了基址和界限,剩下的属性位决定了内存的身份和权限。

字段名称关键作用
PPresent存在位
P=1:段在内存中,可正常访问。
P=0:段无效。访问此类段会触发NP (Segment Not Present) 异常,操作系统通常利用此机制实现内存换页。
DPLDescriptor Privilege Level特权级(门锁)
范围 0-3。0代表最高权限 (Ring 0内核) ,3代表最低权限 (Ring 3用户)。这是CPU护机制的核心门锁。
SSystem描述符类型
S=1:数据段/代码段 (我们最常接触的)。
S=0:系统段 (如TSS、调用门、中断门)。
TypeType具体类型 (取决于S位)。
若 S=1:决定该段是可读写 (Data) 还是可执行 (Code) ,以及是否被访问过 (Accessed)。
GGranularity粒度位
G=0:Limit单位是字节,最大段长1MB。
G=1:Limit 单位是4KB,最大段长4GB (Limit×4KB+0xFFFLimit \times 4KB + 0xFFFLimit×4KB+0xFFF)。
D/BDefault Operation Size位宽位
1 = 32位段 (使用 EIP/ESP);0 = 16位段 (使用IP/SP)。

二、 段选择子 (Segment Selector)

段选择子就是我们在汇编指令中操作的那个16位数值(例如MOV DS, AX 中的 AX)。在上一节中我们已经详细分析过它的结构,这里只做核心回顾。

它主要包含三个部分:

  1. Index (索引) :GDT数组的下标。CPU 通过 GDTR.Base + (Index * 8) 定位描述符。
  2. TI (表指示位) :决定查GDT (TI=0) 还是LDT (TI=1)。
  3. RPL (请求特权级) :它代表了你是以什么权限身份去请求访问这个段的。

三、 权限检查机制

在保护模式下,能不能把一个选择子加载到段寄存器(如DS, ES),不仅看段是否存在,更要看权限是否匹配

这是理解Windows内核保护的关键,我们需要理解三个概念:

  1. CPL (Current Privilege Level)当前特权级。即当前代码运行在什么环(CS寄存器的低位)。
  2. DPL (Descriptor Privilege Level)描述符特权级。目标内存段要求的最低权限。
  3. RPL (Requested Privilege Level)请求特权级。选择子里的权限。

数据段访问规则 (DS/ES/FS/GS)

CPU硬件执行的检查公式如下:

MAX(CPL, RPL) ≤ DPL

规则解析:

  • 在x86架构中,数值越小代表权限越高(0为最高,3为最低)。
  • 上述公式表示:当前运行权限 (CPL)选择子中的请求权限 (RPL) ,在数值上都必须小于等于目标段的DPL
  • 也就是说访问者的权限级别必须高于或等于目标段要求的权限级别。

举例说明:

  • 失败:用户态代码 (CPL=3) 试图访问内核数据段 (DPL=0)。MAX(3, RPL) > 0,检查失败,触发0xC0000005异常。
  • 成功:内核态代码 (CPL=0) 访问用户数据段 (DPL=3)。MAX(0, RPL) ≤ 3,检查通过。

四、权限验证

为了验证权限检查机制,我们使用VC++ 6.0内联汇编,通过LES指令加载段描述符。LES 指令会加载ES段寄存器,并同时触发 CPU的硬件权限检查。

我们将尝试加载Ring 3选择子0x1B(Index=3, TI=0, RPL=3),先使用windbg查看如下:
在这里插入图片描述

  • Pl (DPL): 3
  • RPL (0x1B的低2位): 3
  • CPL: 3 (调试器上下文,当前运行在Ring 3)
  • 检查公式:MAX(3, 3) <= 3成立!所以放行。
  • 代码实战:
int main() 
{

	char buf[6] = {
        0x78, 0x56, 0x34, 0x12,
		0x1B, 0x00
    };
	
	unsigned short es_val_before = 0;
    unsigned short es_val_after = 0;

	__asm
	{
		mov ax,es
		mov es_val_before,ax

		// 执行加载,尝试改为0x1B
		// les: 将buf高2字节加载到ES,低4字节加载到 EAX
		les eax,fword ptr [buf]

		mov ax, es
        mov es_val_after, ax
	}
	
	printf("Load Success! Privilege check passed.\n");
    printf("Before LES: 0x%04X\n", es_val_before);
    printf("After  LES: 0x%04X\n", es_val_after);


	system("pause");
    return 0;
}

执行成功,结果如下:
在这里插入图片描述

五、总结

保护模式的权限检查,就是CPU硬件在段寄存器加载时强制执行的一道身份验证,确保只有高权限的程序才能访问高权限的内存,从而把内核和应用程序隔离开来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值