保护模式入门

段寄存器

一共有8个:ES、CS、SS、DS、FS、GS、LDTR、TR。每个段寄存器结构如下:

1
2
3
4
Base 32bit
Limit 32bit
Attribute 16bit
Selector 16bit (只有这16位是可见的)

读写方法:用mov时,mov ax,es只能读16位可见部分,mov ds,ax能写96位。读写LDTRsldtlldt,读写trstrltr

成员如下,其中GS在Windows中不用:

段寄存器 Selector Attribute Base Limit
ES 0023 RW 0 0xFFFFFFFF
CS 001B RX 0 0xFFFFFFFF
SS 0023 RW 0 0xFFFFFFFF
DS 0023 RW 0 0xFFFFFFFF
FS 003B RW 0x7FFDE000 0xFFF

在调试时,单步调试触发单步调试异常进入内核,内核会把GS清零。段寄存器在内联汇编实现时编译器可能会瞎改。

段描述符

分为全局描述符表GDT和局部描述符表LDT,但后者Windows不用。CPU有个寄存器GDTR存储GDT的位置和大小:

1
2
3
4
struct GDTR {
DWORD GDTBase; //地址
SHORT limit; //大小
};

用WinDBG读GDT:

1
2
3
r gdtr ;读GDT地址
r gdtl ;读GDT大小
dq 8003f000 l20 ;从GDT地址读0x20个64位地址 没有就默认0x10个

段选择子

指向定义该段段描述符的结构:

1
2
3
RPL 2bit 请求特权级别
TI 1bit 0时查GDT 1时查LDT
Index 13bit 该值乘8再加GDT/LDT基地址 即为要加载的段描述符

段描述符

注意Intel的图全是从下到上,从右往左:(。注意WinDBG读取数据时可能用XXXXXXXX`XXXXXXXX表示后4字节为低位,前4字节为高位,每4字节从右往左是从高到低。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0:
Segment Limit 00-15
Base Address 16-31

4:
Base 00-07
Type 08-11
S 12
DPL 13-14
P 15
Seg. Limit 16-19
AVL 20
0 21
D/B 22
G 23
Base 24-31

P:为1段描述符有效,为0段描述符无效。

Base:被分到仨地方了,拼起来就是完整的。

Limit&G:俩Limit要拼起来用。当G为0时,在Limit前补000即为最终Limit结果,范围即为1B~1MB;当G为1时,在Limit后补FFF即为最终Limit结果,范围即为4KB~4GB。

S:为1表示代码段或数据段描述符,为0表示系统段描述符。

DPL:描述符特权级别,访问该段所需权限。

AVL:指示是否可供系统软件使用。

TYPE

段描述符的这个域太复杂了,单独拎出来讲讲。

当S为1时:

当第11位为0时,8-10位分别为A、W、E属性,说明描述符类型为Data;当第11位为1时,8-10位分别为A、R、C属性,说明描述符类型为Code。

属性:A访问位、E向下拓展位、R可读位、W可写位、C一致位。

当C为1表示一致代码段,为0时非一致代码段。

当E为1时表示向下拓展。向下拓展时Base到Base+Limit间区域无效,其余有效。向上拓展时Base到Base+Limit间区域有效,其余无效。

当S为0时:后面遇到再讲吧。

D/B

对于CS段来说,为1采用32位寻址方式,为0采用16位寻址方式。

对于SS段来说,为1时隐式堆栈访问指令用32位堆栈指针寄存器ESP,位0时使用16位堆栈指针寄存器SP。这里隐式堆栈访问指令为PUSHPOPCALL等。

对于向下拓展来说,这里称Base到Base+Limit间为向上拓展有效,4G中其他部分为向下拓展有效。当为1时向下拓展有效只能为前低64K空间。

段权限检查

CPL表示CPU当前权限级别,DPL表示访问该段所需权限,RPL表示所用权限。

对于一致代码段/共享段:内核态不能访问用户态数据,但用户态可访问内核态;对于非一致代码段:用户态与内核态不能互相访问,只能同级访问。

数据段权限检查:权限上CPL<=DPL && RPL<=DPL

代码段权限检查:对于非一致代码段要求权限上CPL==DPL && RPL<=DPL,对于一致代码段要求权限上CPL>=DPL

代码跨段跳转

除CS外,其他段寄存器都可以通过MOVLESLSSLDSLFSLGS进行修改,CS不能直接修改,可以用JMP FARCALL FARRETFINTIRETED等同时修改CS和EIP。段间跳转用长跳转JMP FAR就够了。

实例

例如代码JMP 0x20:0x004183D7

将0x20按照段选择子差分,TI=0查GDT表,Index=4找到对应段描述符。

权限检查,略。

加载段描述符到CS寄存器。

CPU将CS.Base+Offset写入EIP后跳转到要执行的CS:EIP处,段间跳转结束。

调用门

短调用就是常见的CALL指令。长调用指令为CALL CS:EIP,其中CS指向调用门段选择子,EIP废弃,CS一换EIP和SS配套的得跟着一块儿换。

调用门结构与普通段描述符很相似:

1
2
3
4
5
6
7
8
9
10
11
12
0: 段选择子 段偏移 即为调用后EIP位置
Offset in Segment 00-15
Segment Selector 16-31

4:
Param. Count 00-04 调用该调用门所需参数数目
0 05-07
Type 08-11
0 12
DPL 13-14
P 15
Offset in Segment 16-31

当DPL小于CPL需要提权。

长调用不提权:发生改变的寄存器有ESP、EIP、CS,当CALL时先压入调用者CS再压入返回地址,RET同理。

长调用提权:发生改变的寄存器有ESP、EIP、CS和SS,当CALL时依次压入调用者SS、ESP、CS,最后压入返回地址,RET同理。

大致步骤:根据CS查GDT,找到对应段描述符(应该是个调用门),进行权限检查并把段描述符中另一个代码段的段选择子加载到CS中,选择子指向段的Base+Offset即为真正要执行的地址。用RETF返回。

中断门

IDT包含3种门描述符:任务门描述符、中断门描述符、陷阱们描述符,每个描述符8字节,WinDBG下读IDT首地址和大小:

1
2
r idtr
r idtl

中断门结构:

1
2
3
4
5
6
7
8
9
10
11
0:
Offset 00-15
Segment Selector 16-31

4:
NULL 00-04
0 05-07
0?110 08-12 ?处为32位为1
DPL 13-14
P 15
Offset 16-31

指令格式为INT n,n为中断门索引号。

执行步骤:无权限切换时依次压入EFLAG、CS、EIP,有权限切换时最先多压入SS;索引到IDT,中断门索引号即为IDT表项下标,没有RPL只需权限校验CPL;用IRETIRETD指令返回。

陷阱门

跟中断门相似,就是要把IF位置零。