段描述符
数据段描述符

代码段描述符

系统段描述符

??? A - 访问???????????????????????????? E - 向下扩展
???? AVL - 供程序员使用?????????????????? G - 粒度
???? B - BIG?????????????????????????? P - 段是否有效
???? C - CONFORMING??????????????????????? R - 可读
???? D - 默认????????????????????????????? W - 可写
???? DPL - 描述符特权级
通用描述符

?? L???? - 64位代码段(IA-32E模式)
?? AVL?? - 可供系统软件使用
?? BASE?? - 段基地址
?? D/B?? - 默认操作大小(0=16位段;1=32位段)
?? DPL?? - 描述符特权级别
???? G?? - 粒度
LIMIT?? - 段限长
???? P?? - 段是否有效
???? S?? - 描述符类型(0=系统段,1=代码段或数据段)
?? TYPE?? - 段的类型
描述符概述
在实模式中(以前16位CPU所使用的模式),在内存中的任何地址上都能够执行代码,所有的内存地址都是可以被读写的,这非常不安全. 然而并没有其它手段能够限制或者说禁止代码去读写某个内存地址(例如,在保护模式下,读写地址0是错误的,会导致程序崩溃,在实模式下就不会崩溃). CPU为了提供限制/禁止的手段,提出了保护模式. 保护模式实际上就是保护了内存,使得内存中能够被执行代码,能够被读写的地址可以被人为得控制. 在保护模式中,系统和用户进程是被隔离开的. 用户进程无法修改系统的内存,也无法执行系统的代码.这些也是通过保护模式达成的功能. 保护模式所实现的功能,很大程度上依赖分段机制. 在实模式下,分段机制很简单,它没有限制段是否的读,写,执行属性,也没有限制一段内存有什么权限,能够在什么权限下被读,执行,只是规定了,要使用内存,就需要使用段基地址*16 + 段内偏移
的方式来寻址. 在保护模式下,它兼容实模式下的寻址方式(段基地址*16 + 段内偏移
),并且在这基础之上给一个段增加了段基地址,段的长度,段的属性这三个属性以实现保护模式下的部分功能. 在保护模式下,描述一段的基地址在哪里,段有多长,段有何属性的结构被称之为段描述符.其结构如下所示:
段描述符结构
typedef struct Descriptor{
?? unsigned int base; // 段基址
?? unsigned int limit; // 段限长
?? unsigned short attribute; // 段属性
}
在保护模式下,增加了很多机制,使得段产生了不少种类:
数据段(用于存储数据,供程序读写)
代码段(用于执行代码)
系统段(用于操作系统提供特殊功能)
每个段描述既能够描述出一段内存从哪开始,到哪结束,还能描述这个段是什么类型(代码段,数据段,系统段),当然,也能够描述这个段是否可读,是否可写,是否可执行,甚至还能描述这个段的权限是什么,在什么权限下才能使用这一段内存.
段基址
在描述中总长度为32位(4字节) . 表示段的开始地址.
段限长
在描述中总长度为24位,表示段的最大长度,也就所,此位段表示的值最大为: 0xFFFFF
,也就是1Mb
,此长度表示的是单位,至于一个单位的长度到底是多少字节,依赖于段属性的G
位(粒度位)
如果粒度位(G
位)等于0,段的大小范围为1字节
到1Mb
(0~0xFFFFF),每个单位为1字节.
如果粒度位(G
位)等于1,段的大小范围是1字节
到4Gb
(0~0xFFFFFFFF),每个单位为4Kb
段属性
P位
P位用于记录当前段描述符是否有效.
p==0
: 无效,系统不会使用该段描述符
p==1
: 段描述符有效.
S位
描述符类型(0=系统段,1=代码段或数据段),这个位的值决定了Type
字段的值是何种含义.
Type
当S位等于1的时候,Type字段描述的是数据段或代码段
当Type字段的最高位(在段描述符中的11位)等于0时,是数据段
当一段是数据段时,Type字段的(8,9,10位分别为A
,W
,E
,其中,
A
- 数据段是否已经被访问,等于1表示已经被访问
W
- 数据段是否可写,等于1表示可写.否则为只读
E
- 数据段的扩展方向,等于1表示向下扩展(向下扩展也可称为向外扩展),等于0时表示向上扩展(向内扩展)

代码段/数据段
当Type字段的最高位(在段描述符中的11位)等于1时,是代码段
当一个段是代码段时,Type字段的8,C
,A
,W
的作用和数据段一样.
C` - 段是否是一致代码段,等于1表示是`一致代码段`,等于0表示是`非一致代码段

系统段
当S位等于0的时候,Type字段的值描述的是系统段. 系统段中的描述符类型一般都是门描述符. 当这个描述符是一个段描述符之时,Type
字段就没有像代码段或数据段中的A,W,E
标志了,Type
字段的值决定了这个描述符的作用:

D/B位
D/B位的会作用到代码段(CS
段寄存器),栈段(SS
段寄存器),数据段(DS
,ES
段寄存器),当使用这些段寄存器时,将会受到不同的影响:
-
可执行代码段(CS
段)
此标志位被称为D
位,这个位会影响指令和操作数的寻址模式.
D/B
== 1 - 指令默认的寻址模式是32位,操作数默认为32位或8位.
D/B
== 0 - 指令默认使用16位寻址模式,操作默认大小为16位或8位.
指令前缀67H
可以用来切换寻址模式. 例如,当前D
==1时,寻址模式是32位,切换之后,寻址模式就变成16位模式
指令前缀66H
可以用来切换操作数大小,例如,当前D==0
时,操作数大小默认为32,操作数大小为16位.
-
栈段(SS
段)
此标志位被称为B
位,
B
== 1 - 默认使用32位的ESP
寄存器操作栈
B
== 0 - 默认使用16位的SP
寄存器操作栈
-
向下展开的数据段
CPU虽然提供了这个机制,但是操作系统并没有使用这个机制
段寄存器和段描述符
CPU提供了段描述之后,操作系统就可以使用这种机制来做出各种各样的限制. 例如,以下汇编代码
mov eax,dword ptr ds:[0x403000]
在这条汇编指令当中,保护模式的分段机制在无形中产生对该指令产生了影响:
-
指令正在访问的地址是 段基地址*16 + 段内偏移
如果在16位的实模式下,一般就是ds*16+0x403000
,但是在32位的保护模式下,16位的段寄存器并不能存储一个64位的段描述符.
段寄存器保存的值被称为段选择子.
真正的段描述符存储在内存中,由GDTR
寄存器记录其基地址和大小. 那么,16位的段寄存器如果才能和64位的段描述符对应起来?
全局描述符表(GDT)
在一个系统中,描述符的种类有多个,分别有数据段,代码段,系统段. 系统段又分为多种,有调用门,中断门,陷阱门,任务门. 因此,在一个系统中,描述符是存在多个的. 这些描述符被统一打包存储在内存中,它们所形成的一个数组被称之为全局描述符表. 全局描述符的小标则保存于16位的段寄存器中. 一个16位的段寄存器实际由以下部分组成:

段寄存器实际的长度为96位,16位的值,只是寄存器的可见部分,段寄存器还有80位是隐藏部分,这个隐藏部分只能被CPU所操作,无法通过任何指令来操作它. 这可见部分的16位的值也并非全部用于保存全局描述符的下标,它被划分为以下格式:

也就是说,只有13位是用于保存全局描述符表的下标. T1 - 用于记录,保存的下标是
GDT
(全局描述符表)的还是
LDT
(本地描述符表)的(windows操作系统没有
LDT
) RPL - 当前请求级别,用作权限检查. 一共有4个值: 0~3,数值越小,权限越大,0代表最高权限.
由于段寄存器用于保存段选择子,因此,给一个段寄存器赋值,就不单单是赋值一个数字了,例如:
mov ax,2Bh
mov ds,ax
这条指令可看成将0x2B
赋值给ds
寄存器,实际不是. 将0x2b
的二进制展开: 0000 0000 0010 1011
,段选择子的格式为: 13 : 1 : 3
. 那么在0x2b
这个数中,描述符表索引,T1位,RPL分别为:
?????????????? 0000 0000 00101 0 11
?????????????? _____________/ - --
?????????????????????? |?????? |?? |
?????????????????????? |?????? |?? +---> RPL = 3
?????????????????????? |?????? +-------> T1 = 0(GDT)
?????????????????????? +----------------> 索引 = 5????????
?
也就说,0x2b
这个数代表的是GDT
表中第5
个段描述符. 当前请求级别为最低权限的2
. mov ds,ax
这条指令执行之后做了什么? CPU执行这条指令后,会将GDT
表中第5
个段描述符存储在段寄存器隐藏部分,将段选择子存储到16位可见部分. 当然,在做这些之前,CPU还需要做权限检查.
权限检查
段描述符中,有一个属性是DPL
,这个属性总共有2个二进制位. 大小和段选择子中RPL
一样. 这二者正是用于作权限检查的. DPL
指的是描述符特权级别,它决定了在什么特权下,才能够访问此描述符. 其值从0~3
共4个,0最表示最高权限,3表示最低权限,只有权限高于等于此DPL
所记录的权限,才能访问描述符. RPL
指的是请求级别,值的是以何种权限去请求GDT表中的段描述符. 也就是说,当指令mov ds,ax; // ax==2B
执行是,CPU会将数值2B
作为段选择子,使用这个数的低两个二进制位作为RPL
,使用这个数的高13
为作为描述符表的索引,去获取描述符,但如果要获取的描述符的DPL
的值比RPL
要小(值越小权限越高),这条指令就无法取出这个段描述符,就完成不了赋值,CPU还会报一个异常.
除此之外,CPU还有其它检查,权限检查是最后一项,这些检查依次为:
jmp 33:401000
call 33:401000
上述两条指令被称为远跳转指令和远调用指令,指令后的操作数分为两部分 : 段选择子和段内偏移. 这两条指令在执行后,会将操作数中的段选择子对应的段描述符加载到CS
中,此时,CPU也会做检查:
1. 检查请求的段描述符的`S`是否为1,如果是1,表示是请求的段是一个数据段或代码段,再检查`Type`的高位是否 是1,表示请求的段是代码段. 如果其中一个不是,就无法加载,指令无法执行,CPU还会报异常.
2. 继续检查,`Type`的`C`标志,如果是一致代码段,则要求`CPL`>=`DPL`,也就是只能低权限转移到高权限,如果是非一致代码段,则要求 ` CPL==DPL` 并且,`RPL<=DPL`,也就是平级才能转移.
???? 但是转移之后,`CPL`和`RPL`不会改变.
?
3. 如果检查`S`位是0,则表示请求的是系统段.??????????????
?
下面是伪代码:
//段选择子的结构:
// [描述符下标:13?? | T1:1 | RPL:2]
unsigned short segSel = 0x33;// 0x33就是要切换的段选择子,在指令jmp 33:401000 中给出。
unsigned int RPL = segSel & 0x11;? // 取段选择自的低2位作为RPL
unsigned int CPL = CS & 0x11 ; // 取`CS`段寄存器的值的低2位作为CPL
if( SegDes.S == 1 && SegDes.Type & 0x1000 ){
??? if(SegDes.Type.E == 1){ // 一致代码段
??????? if( CPL >= segDes.DPL ){
??????????? CS = segSel; // 可以切换。
?????? }else{
??????????? throw "异常";
?????? }
?? }else{ // 非一致代码段
??????? if(CPL == SegDes.DPL && RPL <= SegDes.DPL){
??????????? CS = segSel;
?????? }else{
??????????? throw "异常";
?????? }
?? }
}else{
??? throw "异常";
}
系统段描述符 - 门描述符
很多时候, 用户层的代码需要切换到内核层执行代码。 因为有些代码执行时需要用到0环权限.
切换0环权限实际就是将CS
段寄存器的CPL
改成0(也就是0环权限).
但在3环时,CS
的CPL
是2,是无法直接修改的(如果要修改,就需要切换段选择子,切换段选择子,就需要使用CPL
,RPL
和段描述符中的DPL比较
)
当段描述符中的S
位等于0 , 表示这个描述符是一个门描述符。
门描述符一般用于从3环进入到0环,并能够将3环权限切换成0环权限。
门描述符的种类有:
调用门
调用门的出现是为了便于在不同的权限直接切换.

一个调用门中,保存了以下信息:
要执行的函数的地址
要执行的函数的参数个数
段选择子 (这个段选择子用于切换权限)
字段解析:
Offset in Segment
- 函数在段内的偏移,实际就是函数的地址,这个地址的被拆分成高16位和低32位保存.
Segment Selector
- 段选择子,使用调用门时,这个选择子就是被切换成CS
的选择子.
Param Count
- 调用门中保存的函数的参数个数. 注意,每个参数应当是4字节的.
如果发生了权限切换,系统也会将用户栈切换成内核栈,也就是权限切换时,CS
段寄存器的值会被改掉(不改掉切换不了切换段描述符),还会将SS
段选择子,切换为内核的段选择子,将ESP
切换成内核的ESP
,切换前,SS
,ESP
的值都会被保存. 在切换回来之后,才进行还原.
因为栈要进行切换,当函数被调用时,用户栈中的函数参数会被拷贝到内核栈,需要在调用门描述符中指定参数的个数是多少,否则系统将无法为函数拷贝参数到内核栈中.
调用门的设置和使用
因为在Windows中没有使用调用门,在GDT表中是没有调用门描述符的.
想要试验一个调用门,就需要自己使用windbg
开启双击调试,并自行在GDT中设置一个.
设置的方式如下:
构造如下结构体:
typedef struct _CALLGATEDESCRIPT{
?????? unsigned int functionAddrLow : 16; // 被调用函数地址的低16位
?????? unsigned int SegmentSelector : 16; // 想要切换的段选择子
?????? unsigned int paramCount : 5; // 参数个数
?????? unsigned int none : 3; // 无
?????? unsigned int Type : 4; // 系统段类型,必须为12(1100b: 32位调用门)
?????? unsigned int S : 1; // S位,必须为0
?????? unsigned int DPL : 2; // 描述符特权级别
?????? unsigned int P : 1; // 描述符有效位
?????? unsigned int functionAddrHig : 16; //被调用函数地址的高16位
?? }CALLGATEDESCRIPT;
?
设置如下:
unsigned long long createCallGateDescript(unsigned short selector,/要切换的段选择子/
?????????????????????????????????????????? unsigned int functionAddr,/要通过调用门执行的函数/
???????????????????????????????????????????? int functionParamSize/函数参数所占用的字节数/)
?? {
?????? CALLGATEDESCRIPT cgd = { 0 };
?????? cgd.P = 1; // 描述符有效位,必须设置为1
?????? cgd.S = 0; // 系统段描述符,必须设置0
?????? cgd.Type = 12; // 调用门描述符,必须设置为12
?????? cgd.DPL = 3; // 设置为3,表示此描述符可被3环所使用.
?????? cgd.SegmentSelector = selector;// 要切换的段选择子
?????? cgd.functionAddrHig = (functionAddr & 0xFFFF0000) >> 16;
?????? cgd.functionAddrLow = functionAddr & 0x0000FFFF;
?????? cgd.paramCount = functionParamSize / 4; // 参数个数,如果函数没有参数,填0,如果有参数,则填参数占用栈的字节数 / 4
?????? return (unsigned long long)&cgd;
?? }
-
使用调用门的之前,需要将调用门的描述符写进系统的GDT表中,操作如下:
使用windbg双机调试,输入指令rgdtr
获取GDT表的地址
kd> rgdtr
?? gdtr=80b95000
dq
命令查看GDT表中哪些是空闲的,值为0的都是空闲的.

-
构造一个可以在3环代码中使用的段选择子,构造规则:
段选择子的格式为: [ 描述符在表中的下标:13 | T1:1 | RPL:2]
-
假如在GDT表,第9项是空闲的,门描述符将保存在此处,则对应的段选择子为:
十进制: 9-0-3 ==> 二进制: 1001-0-11 ==> 合并为 100 1011,十六进制数值为0x4B
在代码中使用此描述符:
// 前4字节是EIP,后2字节是CS
// 后2字节是0x004B,此数值就是在第二步中构造出来的.
char buff[ ] = { 0,0x4b,00 };
_asm call fword ptr ds:[buff];
代码:
#include <iostream>
#include <iomanip>
?
?
#pragma pack(1)
typedef struct _CALLGATEDESCRIPT {
??? unsigned int functionAddrLow : 16; // 被调用函数地址的低16位
??? unsigned int SegmentSelector : 16; // 想要切换的段选择子
?
??? unsigned int paramCount : 5; // 参数个数
??? unsigned int none : 3; // 无
??? unsigned int Type : 4; // 系统段类型,必须为12(1100b: 32位调用门)
??? unsigned int S : 1; // S位,必须为0
??? unsigned int DPL : 2; // 描述符特权级别
??? unsigned int P : 1; // 描述符有效位
??? unsigned int functionAddrHig : 16; //被调用函数地址的高16位
}CALLGATEDESCRIPT;
?
struct SELECTOR {
??? unsigned short index : 13;
??? unsigned short T1 : 1;
??? unsigned short RPL : 2;
};
?
unsigned long long createCallGateDescript(unsigned short selector,unsigned int functionAddr,int functionParamSize)
{
??? CALLGATEDESCRIPT cgd = { 0 };
??? cgd.P = 1; // 描述符有效位,必须设置为1
??? cgd.S = 0; // 系统段描述符,必须设置0
??? cgd.Type = 12; // 调用门描述符,必须设置为12
??? cgd.DPL = 3; // 设置为3,表示此描述符可被3环所使用.
??? cgd.SegmentSelector = selector;// 要切换的段选择子
??? cgd.functionAddrHig = (functionAddr & 0xFFFF0000) >> 16;
??? cgd.functionAddrLow = functionAddr & 0x0000FFFF;
??? cgd.paramCount = functionParamSize / 4; // 参数个数,则填参数占用栈的字节数 / 4
??? return *(unsigned long long*)&cgd;
}
?
int g_num;
short g_ss;
int g_esp;
?
//通过调用门调用的 函数
void _declspec(naked) GateFun()
{
??? g_num = 100;
??? _asm mov [ g_esp ],esp;
??? _asm mov ax,ss;
??? _asm mov word ptr [g_ss],ax
??? _asm retf;
}
?
int main()
{
??? printf( "调用门函数地址:%08Xn",GateFun );
??? printf( "切换的段选择子:%04Xn",8 );/*8是内核中的代码 段选择子*/
?
??? unsigned long long descript =
??????? createCallGateDescript(8/*8是内核中的代码段选择子*/,( unsigned int )GateFun,0 );
?
?
??? printf("请将这个段描述符写入到GDT[9]中: ");
??? std::cout << std::hex <<std::uppercase<< std::setfill(‘0‘)<<std::setw(8)<< descript<<‘n‘;
??? system( "pause" );
?
?
?
??? // 获取当前寄存器的值.
??? _asm mov[ g_esp ],ss;
??? _asm mov word ptr[ g_ss ],ax
?
??? printf( "调用前 esp=%08X,ss=%04Xn",g_esp,g_ss );
?
??? // 前4字节是EIP,后2字节是CS(0x004b)
??? char buff[ ] = { 0,00 };
?
??? // 执行流程:
??? // 1. 从buff这块内存中取出段选择子:0x4b
??? //
??? //?????? 解释?????????????????????? SEL T RPL
??? //???? 十进制???????????????????????? 9 0 3
???? // 2. 将段选择子分解,100 1011 ==> 1001 0 11,得到GDT表中的下标:9
??? // 3. 取出GDT表中第9项描述符,是一个调用门描述符.
??? // 4. 将调用门描述符中的段选择子加载到CS段寄存器
??? // 5. 将调用门描述符中的函数地址设置到EIP寄存器.
??? _asm call fword ptr ds:[buff];
?
??? printf( "调用后 esp=%08X,g_ss );
??? printf( "g_num=%dn",g_num );
??? system( "pause" );
}
?
windbg 输入eq 80b95068(上图右边红色框的地址).

任务门,中断门和陷阱门
中断门和陷阱门的描述符其实和调用门一模一样.


任务门描述符,中断门描述符和陷阱门描述符都是保存在IDT
(中断描述符表)中.
其使用的过程是:
产生中断或异常后,
CPU会使用中断号找到IDT
表中的中断描述符/陷阱描述符,
取出描述符后,得到门描述符中的段选择子.
通过此段选择子找到GDT
表中的段描述符,
从GDT表中取出的段描述符中得到段基地址
使用段基地址 + 门描述符
中的函数偏移,得到函数地址.
调用该函数.