ES CS SS DS FS GS LDTR TR

段寄存器长度:96位

实模式

实际地址=段地址左移4位+偏移量

eg:

1
2
3
mov ds:[si],ax
等价
mov [ds*16+si],ax

保护模式:

保护模式下,cpu依旧通过短地址和偏移量寻址,但是段寄存器不直接保存偏移,下面是段寄存器在保护模式下的结构:

1
2
3
4
5
6
7
struct SegMent//示意
{
WORD Selector;//段选择子
WORD Atrributes;//属性
DWORD Base;
DWORD Limit;//段界限
}

段选择子:

002B:(前8位是0)00101 0 11

其中低2位为当前请求权限级别RPL:从r0到r3,其中CS寄存器的低2位存着cs中eip的偏移,即cs指向当前代码段,它的RPL被称为CPL,同理于指向堆栈的SS寄存器

第3位表示查询哪张表:一般有两张表:GDT和LDT表,0表示GDT,1表示LDT(LDT必须嵌套在GDT中,同时可以有多张LDT)

如果是1:会装载某一个ldt表,通过段选择子在gdt中找到ldt执行后续操作

前5位表示段描述符索引,指向GDT中存放段描述符的地址,即gdt[x],n为前五位的值,所以

1
段描述符位置 = gdt首地址 + x*8

其中段描述符存放了除了段选择子之外的属性

image-20240603014954353

image-20240603134721994

其中:

绿色表示段基址
蓝色表示段界限
黄色表示段属性

段属性中,先看P,DPL,S,TYPE这几段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
P占一位:
P=0 该段不在内存中
P=1 该段在内存中
DPL占2位:
表示特权等级0~3

S占一位:
S=0 该段是系统段/门
S=1 该段是数据段或代码段

TYPE占4位:
如果S=1
4位分别为:执行位,一致/方向位,读写位,访问位
执行位:为0表示这是数据段,为1表示是代码段(是否可执行)
一致/方向位:执行位为1:为0表示普通段,为1表示一致代码段;执行位为0:为0表示向上增长,为1表示向下增长
读写位:执行位为0:为1表示可读可写,为0表示可读;执行位为1:为1表示可读可执行,为0表示可执行
访问位:为1表示已访问过,为0表示未访问过

其中一致位表示是不是一致代码段。
这里需要先看一下这个定义:

操作系统保护模式下把代码段分为一致代码段和非一致代码段的原因是:内核程序和用户程序要分开,内核程序不能被用户程序干扰。但是有时候用户程序也需要读取内核的某些数据,于是操作系统就从内核程序中分配一些可以供用户程序访问的段,但是不允许用户程序写入数据,用户程序访问这些段时遵循以下规则:

  1. 内核程序不知道用户程序的数据,不调用用户程序的数据,也不转移到用户程序中来
  2. 用户程序只能访问到内核的某些共享段,这些段称为一致代码段
  3. 用户程序不能访问内核不共享的段

所以一致代码段就是操作系统内核拿出来的共享段:

性质是

  1. 特权级高的程序不允许访问特权级低的数据:即内核态不允许调用用户态的数据。
  2. 特权级低的程序可以访问到特权级高的程序,但是特权级不会改变,即不会从用户态切换到内核态。

所以如果是低特权级到高特权级,可以直接访问,且权限不变,如果是高特权级到低特权级,触发常规保护错误。

非一致代码段:为了避免低特权级的访问而被操作系统保护起来的系统代码

性质是

  1. 只允许同特权级访问。
  2. 绝对禁止不同特权级直接访问:内核态不去用户态,用户态也不使用内核态。
  3. 通常低特权级代码必须通过门调用来实现对高特权级代码段的访问和调用。

所以如果特权级不相等,触发常规保护错误

这里的特权等级检测是根据DPL和CPL的值来判断的

1
2
3
4
5
DPL>=CPL  
可以进入一致代码段

DPL==CPL and RPL<=DPL
可以进入普通代码段/非一致代码段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
如果S=0,系统段
TYPE只定义了12个值
0x1 可用286TSS
0x2 该段存储了LDT(局部描述符表)
0x3 忙286TSS
0x4 286调用门
0x5 任务门
0x6 286中断门
0x7 286陷阱门
0x8
0x9 可用386TSS
0xa
0xb 忙386TSS
0xc 386调用门
0xd
0xe 386中断门
0xf 386陷阱门

另外4个属性

1
2
3
4
5
6
7
8
9
10
11
12
AVL 占1
无定义
L 占1
如果代码段是64位,为1(此时D为0),否则0
D/B1
如果对应代码段D:这是32位代码段为1,否则为16位代码段为0
如果对应栈段B:是被SS寄存器指向的数据段
1:采用32位栈指针寄存器,否则为0,采用16位栈指针寄存器
如果对应向下扩展的数据段B
1:段的上界为4GB,为0:上界为64KB
G 占一位
0:段界限粒度(单位)为字节,为1:段界限单位为4KB

那么又有一个问题:

为什么段寄存器一共有96位,这里不是一共才64(段描述符)+16(段选择子)=80位吗

这里会发现:段描述符中的limit段是3字节,最大值为ffffff,又有G可以表示最大单位为4KB,再加属性里面的保留的一(十六进制)位空位
(ffffff-1)*4096=ffffffff
(8+4)x4+8x4+16=96

这个不像数组,界限为FFFFFFFF那么它可以取到0~FFFFFFFF

参考资料

RPL保存在选择子里,那么CPL是保存在哪里的_cpl 在哪里-CSDN博客

x86保护模式——全局描述符表GDT详解_gdt全局描述符表 作用-CSDN博客

段选择器成员

image-20240618175919652

FS地址不一定和图中相同

通过以下代码查看对应段寄存器的读写属性

(由于)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<Windows.h>
#include<tchar.h>
#include <iostream>

int main()
{
int var = 0;
__asm
{
mov ax, ss;
mov ds, ax;
mov edx, 1;
mov dword ptr ds : [var] , edx;
}
std::cout << var;
}

这个程序可以通过,且表明了ss可读,ds可写的属性

将其换为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<Windows.h>
#include<tchar.h>
#include <iostream>

int main()
{
int var = 0;
__asm
{
mov ax, cs;
mov ds, ax;
mov edx, 1;
mov dword ptr ds : [var] , edx;
}
std::cout << var;
}

发现调试状态可以执行,直接运行会在输出时报错

关于为什么不能直接用立即数给段寄存器赋值:

网上的解释:

为什么立即数不能直接赋给段寄存器-CSDN社区

不懂QAQ


在线程和进程不改变时,在写之后的读不会读gdt,在手工修改进程线程时,会读一次gdt