虚拟内存有什么含义?
有如下的两种含义:
1.x86进程的4GB空间,x64进程的8TB空间
2.整个计算机的虚拟内存(内存映射)
为什么会存在虚拟内存
windows系统是多任务轮询系统,物理内存不足,主要是为了解决内存空间不足的问题
控制转移
R0和R3不同特权级之间的跳转---调用门,中断门,陷阱门
中断异常
硬中断,软异常
256个中断向量号
输入输出
IOPL、IO允许位、优先级
特权指令
R0权限才可以使用的指令
所有的规则都是CPU厂商规定的规则
X86 CPU的三个模式:
实模式,保护模式,x86模式
| 实模式 | 保护模式 | 虚拟8086模式 |
内存寻址方式 | 段式寻址 | 支持内存分页和虚拟内存 | 段式寻址 |
寻址范围 | 任意寻址 | 有限 | 任意寻址 |
举例系统 | dos系统 | Windows系统 | ? |
现在的操作系统大多都是处于保护模式之下的
保护模式的定义
基于安全性和稳定性的考量而产生的一种CPU模式
保护模式与其他模式的根本区别是什么?
所谓保护,就是对权限做出规定,根本在于进程内存受到了保护
保护模式的分类
段保护模式,页保护模式
为什么要有段保护呢?
多任务轮询,代码和数据分开,提高访问的效率
分页机制的目的是什么?
解决计算机内存不足的问题,避免内存碎片化
段寄存器
以下三种是附加段寄存器
ES:
FS:指向线程环境块的首地址TEB,在创建线程时创建的,用于支持操作系统的相关功能
GS:指向进程环境块的首地址PEB
在80386中的段寄存器中存储的是16位的段选择子
段寄存器的结构
组成 | Base | Limit | Attribute | Selector |
数据宽度 | 32位 | 32位 | 16位 | 16位 |
是否可见 | 不可见 | 不可见 | 不可见 | 可见 |
描述 | 基地址(当前段的起始地址) | 大小限制(当前段的整个长度) | 属性(当前段是否可读可写可执行) | 段选择子 |
struct Segment{
WORD Selector;
WORD Attribute;
DWORD Base;
DWORD Limit;
}
在OD中查看段寄存器的结构信息
段寄存器 | Selector | Attribute | Base | Limit |
ES | 002B | 可读、可写 | 0 | 0xFFFFFFFF |
CS | 0023 | 可读、可执行 | 0 | 0xFFFFFFFF |
SS | 002B | 可读、可写 | 0 | 0xFFFFFFFF |
DS | 002B | 可读、可写 | 0 | 0xFFFFFFFF |
FS | 0053 | 可读、可写 | 258000 | 0xFFF |
GS | 002B | - | 0 | 0xFFFFFFFF |
段寄存器的读写
使用mov指令完成相关操作
读段寄存器
#include <stdio.h>
#include <windows.h>
int main() {
WORD selector1, selector2, selector3, selector4, selector5 = 0;
_asm {
mov selector1, cs
mov selector2, ds
mov selector3, es
mov selector4, fs
mov selector5, gs
}
printf("%x %x %x %x %x\n", selector1, selector2, selector3, selector4, selector5);
return 0;
}
对寄存器的读操作,只能够读取寄存器的16位的selector选择子的部分
写段寄存器
以下是函数的核心代码
push ds //保存ds段寄存器
mov ax,cs //将cs段寄存器的段选择子赋值给ax
mov ds,ax //使用cs段寄存器覆盖ds段寄存器
mov word ptr ds:[data],ax
pop ds //还原ds段寄存器
编译时出现异常
为什么会报错呢?
因为此时ds段寄存器已经被覆盖为了CS段寄存器的值,CS的权限是可读可执行,没有可写
即段寄存器的权限不足导致的,
(在赋值时使用DS段寄存器来赋值,而不使用CS寄存器)
如果使用同样具有写权限的SS段寄存器堆DS段寄存器进行重新赋值,尝试运行如下
mov ax, ss
mov ds, ax
mov word ptr ds:[data],ax
编译器能成功编译上述代码,并且程序运行过程中没有报错
上述代码证明,段寄存器的Attribute在写入时会被更改!
LIMIT属性
由上述的表中得知FS段的limit值为0xFFF
#include <stdio.h>
#include <windows.h>
int main(){
unsigned char base;
_asm{
mov al,fs:[0x1000] //超过limit:0xfff,无法正常运行
mov base,al
}
printf("%x\n",base);
return 0;
}
编译成功但程序运行时报错
修改limit为0xFFF之内的值
程序正常运行
那么其他的值是否会发生改变呢?
此处需要注意的是
在读段寄存器时,得到的是16位的段选择子的值
在写段寄存器时,是对整个96位的段寄存器进行修改
GDT
如何确定全局描述符表的位置?
GDTR寄存器
在windbg中使用一下指令
r gdtr //读取gdt表的起始位置
r gdtl //读取gdt表的大小
得到了:
GDT表的起始位置 | GDT表的大小 | |
值 | 0x8003f000 | 0x3ff |
数据宽度 | DWORD(4字节) | WORD(2字节) |
得到了GDT表的起始位置后,就可以查看GDT表的内容了:
复制代码 隐藏代码
dq 0x8003f000
表中的存储项是什么呢?
段描述符:用来描述段的信息的,每个段对应一个段描述符
如何定位段描述符呢?
通过段选择子来定位
段选择子的结构
| Index | TI | RPL |
含义 | 索引 | 表指示器 | 请求特权等级 |
全称 | Index | Table Indicator | Requested Privilege Level |
数据宽度 | 13位 | 1位 | 2位 |
相关的属性说明如下
Index:在GDT中定位段描述符的索引
TI:决定了到GDT中查找还是LDT中查找
| TI==0 | TI==1 |
选择的表 | GDT | LDT |
注:在windows中并不会使用LDT表,所以TI恒等于0
RPL:请求特权等级
根据段选择子确定段描述符的具体过程
例子:以段选择子 = 0x001B为例
首先将段选择子转换为二进制 : 0000 0000 0001 1011
将其按段选择子的结构填入:
Index | TI | RPL | |
二进制值 | 0000 0000 0001 1 | 0 | 11 |
十进制值 | 3 | 0 | 3 |
含义 | 索引为3 | 查询GDT表 | 请求特权等级为3 |
得到的索引为3
拿到索引之后就可以定位对应的段描述符了
对应的段描述符地址 = GDT表首地址 + 索引× 段描述符长度 = GDT表首地址 + 索引 × 8(注意这里的单位为字节,64位=8字节)
所以:对应的段描述符地址 = 0x8003f000 + 3×8= 0x8003f000 + 24 = 0x8003f000 + 0x18 = 0x8003f018
加载段描述符至段寄存器
加载段描述符至段寄存器的方法有以下几种:
1. 使用指令LDS(Load Segment)加载段描述符至DS(Data Segment)寄存器。
2. 使用指令LES(Load Effective Address)加载段描述符至ES(Extra Segment)寄存器。
3. 使用指令LSS(Load Segment Selector)加载段描述符至SS(Stack Segment)寄存器。
4. 使用指令LFS(Load Far Pointer to DS)加载段描述符至FS(File Segment)寄存器。
5. 使用指令LGS(Load Far Pointer to DS)加载段描述符至GS(General Segment)寄存器。
6. 使用指令LTR(Load Task Register)加载段描述符至TR(Task Register)寄存器。
注意:CS不能使用上述指令进行修改,因为CS不可写
以上汇编指令的具体使用
LDS,LES,LFS,LGS,LSS其指令格式都是
LDS reg16,mem32
其意义是同时给一个段寄存器和一个16位通用寄存器同时赋值
具体如下:reg16=mem32的低字,DS=mem32的高字
例如、
地址 100h 101h 102h 103h
内容 00h 41h 02h 03h
如果指令 LDS AX,[100h]
则结果为 AX=4100h DS=0302h
具体例子
#include <stdio.h>
#include <windows.h>
char buffer[6]={0x44,0x33,0x22,0x11,0x1B,0x00};
int main(){
_asm{
push ds
lds eax,fword ptr ds:[buffer] //fword为6字节
pop ds
}
return 0;
}
下断点观察
执行前
执行后
对比执行前后
EAX | DS | |
执行前 | 0xCCCCCCCC | 0x0023 |
执行后 | 0x11223344 | 0x001B |
内存关系寻址图:
下面给出内存寻址的流程中,GDT、段描述符、段选择子的关系图:
段寄存器剩下的80位是怎么进行赋值的呢?
段寄存器剩下的80位是通过段描述符来填充的
段描述符的大小是8字节
段描述符的结构
如何将段描述符中的数据加载到段寄存器中的呢?数据有什么对应关系呢?
段寄存器组成 | Base | Limit | Attribute |
数据宽度 | 32位 | 32位 | 16位 |
描述 | 基地址(当前段的起始地址) | 大小限制(当前段的整个长度) | 属性(当前段是否可读可写可执行) |
对应段描述符 | 段描述符中的3段base | 段描述符中的2段limit和G位共同决定 | |
对应位 | 高4字节:24~31 0~7 ,低4字节:16~31 | 高4字节:16~19,低4字节:0~15 | 高4字节:8~23 |
加载前,首先就是要检查段描述符的P标志位
接着判断粒度标志位G
会发现这里的Limit分为了两段,但两段的总长度也只有4+16=20位,和段寄存器中的Limit的32位不匹配
这里的Limit的最大表示范围是:0~2的20次方-1=0~0xFFFFF
而段寄存器中的Limit的最大表示范围是:0~2的32次方-1=0~0xFFFFFFFF
很显然现在的Limit无法表示段寄存器中的Limit了,为了解决这个问题,引入了G位:粒度(用来表示Limit的单位)
G==0 | G==1 | |
含义 | limit以字节为单位 | limit以4096=2的12次方 字节为单位 |
表示范围 | 0~0xFFFFF | 0xFFF~0xFFFFFFFF |
也就是说:G位决定了limit的单位
S位
S位用来标记当前描述符的类型
S==0 | S==1 | |
含义 | 系统段描述符 | 代码段或数据段描述符 |
段描述符可以分为系统描述符、代码段描述符和数据段描述符
根据S位的不同(描述符类型的不同)对应的Type域也不同
也就是说:S位决定了Type域的含义
Type域
Type占据了第11位到第8位,共4位
S位为1时
当S==1时,该段描述符为代码段描述符或者数据段描述符
十进制 | 11 | 10 | 9 | 8 | 描述符类型 | 描述 |
E(是否向下扩展) | W(是否可写) | A(是否可访问) | ||||
0 | 0 | 0 | 0 | 0 | 数据段描述符 | 只读 |
1 | 0 | 0 | 0 | 1 | 数据段描述符 | 只读,可访问 |
2 | 0 | 0 | 1 | 0 | 数据段描述符 | 可读可写 |
3 | 0 | 0 | 1 | 1 | 数据段描述符 | 可读可写可访问 |
4 | 0 | 1 | 0 | 0 | 数据段描述符 | 只读,向下扩展 |
5 | 0 | 1 | 0 | 1 | 数据段描述符 | 只读,向下扩展,可访问 |
6 | 0 | 1 | 1 | 0 | 数据段描述符 | 可读可写,向下扩展 |
7 | 0 | 1 | 1 | 1 | 数据段描述符 | 可读可写,向下扩展,可访问 |
C(是否可以从较低的特权级别调用) | R(是否可读) | A(是否可访问) | ||||
8 | 1 | 0 | 0 | 0 | 代码段描述符 | 只可执行 |
9 | 1 | 0 | 0 | 1 | 代码段描述符 | 只可执行,可访问 |
10 | 1 | 0 | 1 | 0 | 代码段描述符 | 可执行可读 |
11 | 1 | 0 | 1 | 1 | 代码段描述符 | 可执行可读,可访问 |
12 | 1 | 1 | 0 | 0 | 代码段描述符 | 只可执行,可从较低特权级别调用 |
13 | 1 | 1 | 0 | 1 | 代码段描述符 | 只可执行,可从较低特权级别调用,可访问 |
14 | 1 | 1 | 1 | 0 | 代码段描述符 | 可执行可读,可从较低特权级别调用 |
15 | 1 | 1 | 1 | 1 | 代码段描述符 | 可执行可读,可从较低特权级别调用,可访问 |
主要说明一下向下扩展和非向下扩展(向上扩展):
向上扩展就是:该段描述符所描述的地址的有效范围是从Base开始到Base+Limit结束
为下图中的红色部分:
与之相反,向下扩展则是:该段描述符所描述的地址的有效范围是除了 从Base开始到Base+Limit结束 之外的部分
为下图中的红色部分
S位为0时
当S==0时,该段描述符为系统描述符
系统描述符有分为以下类型:
十进制 | 11 | 10 | 9 | 8 | 描述 |
0 | 0 | 0 | 0 | 0 | 保留 |
1 | 0 | 0 | 0 | 1 | 16位 任务状态段TSS(可用) |
2 | 0 | 0 | 1 | 0 | LDT |
3 | 0 | 0 | 1 | 1 | 16位 任务状态段TSS(繁忙) |
4 | 0 | 1 | 0 | 0 | 16位 调用门 |
5 | 0 | 1 | 0 | 1 | 任务门 |
6 | 0 | 1 | 1 | 0 | 16位 中断门 |
7 | 0 | 1 | 1 | 1 | 16位 陷阱门 |
8 | 1 | 0 | 0 | 0 | 保留 |
9 | 1 | 0 | 0 | 1 | 32位 任务状态段TSS(可用) |
10 | 1 | 0 | 1 | 0 | 保留 |
11 | 1 | 0 | 1 | 1 | 32位 任务状态段TSS(繁忙) |
12 | 1 | 1 | 0 | 0 | 32位 调用门 |
13 | 1 | 1 | 0 | 1 | 保留 |
14 | 1 | 1 | 1 | 0 | 32位 中断门 |
15 | 1 | 1 | 1 | 1 | 32位 陷阱门 |
Type域决定了当前段的属性
D/B位
D/B位分别作用于3种情况:
- 对CS段的影响
- 对SS段的影响(SS段也属于数据段)
- 对向下扩展的数据段的影响
D/B == 0 | D/B == 1 | |
CS段 | 采用16位寻址方式 | 采用32位寻址方式 |
SS段 | 使用16位堆栈指针寄存器SP | 使用32位堆栈指针寄存器ESP |
向下扩展的数据段 | 段上限为64KB | 段上限为4GB |
主要说明一下对 向下扩展的数据段的影响
D/B位为1时
D/B位为0时
DPL
DPL=Descriptor privilege level(描述符特权等级)为:访问这个描述符所需的特权级别(环ring)
CPU权限分级的作用
将系统权限和应用程序权限更好的分离出来,让操作系统更好的管理当前的系统资源
让系统更加的稳定:比如说普通的程序崩溃导致的结果是程序未响应或者停止运行,但不会影响到系统的正常运行
但是当驱动程序出现问题之后,就容易出现BSOD的问题
8086中的实模式程序在级别0(最高特权级别)上执行,而8086中的虚拟模式在级别3执行所有程序
如何去判断一个程序的特权级别
CPL是段寄存器CS和SS的段选择子的后两位,也就是说当段寄存器为CS和SS时,其段选择子的RPL就是CPL
通过OD附加应用程序后查看其段寄存器:
段寄存器 | 段选择子(Selector) | 二进制段选择子 | 二进制RPL | 十进制RPL |
ES | 23 | 0010 0011 | 11 | 3 |
CS | 1B | 0001 1011 | 11(CPL) | 3(CPL) |
SS | 23 | 0010 0011 | 11(CPL) | 3(CPL) |
DS | 23 | 0010 0011 | 11 | 3 |
FS | 3B | 0011 1011 | 11 | 3 |
根据CS和SS的段选择子可以得到CPL为3,印证了应用程序的CPU权限分级为RING3
EPL(有效特权级别)
上面提到了CPL(当前特权级别)和RPL(请求特权级别)
所谓的有效特权级别EPL(Effective Privilege Level),顾名思义就是最终的确定可否执行的特权
EPL = max(RPL,CPL)
即 EPL 等于 RPL和CPL的最大值,EPL为RPL和CPL中较低的权限
DPL(描述符特权级别)
关于DPL,在保护模式笔记四 段描述符结构中已经略微说明了,现在展开细说
DPL的作用
DPL存储在段描述符中,规定了访问该段所需要的特权级别是什么;即 如果想要加载某个段描述符,就必须具备对应的特权级别
DPL权限检查
当加载一个段描述符时,首先CPU要判断其P位(有效位),如果该段描述符有效,则继续进行DPL权限检查
所谓的DPL权限检查 就是 判断 DPL是否满足:EPL=max(RPL,CPL)<=DPL是否成立
只有当EPL<=DPL时,权限检查才通过,段描述符才能够被加载到段寄存器中
RPL | CPL | EPL | DPL | 权限检查是否通过 |
0 | 0 | 0 | 0 | √ |
0 | 0 | 0 | 3 | √ |
0 | 3 | 3 | 0 | × |
0 | 3 | 3 | 3 | √ |
3 | 0 | 3 | 0 | × |
3 | 0 | 3 | 3 | √ |
3 | 3 | 3 | 0 | × |
3 | 3 | 3 | 3 | √ |
总结
RPL | CPL | EPL | DPL | |
含义 | 请求特权级别 | 当前特权级别 | 有效特权级别 | 描述符特权级别 |
说明 | 用什么权限去访问一个段 | CPU当权的权限级别 | max(RPL,CPL) | 访问该段所需要的特权级别 |
代码跨段跳转
段间跳转分为两种情况:
- 要跳转的段是一致代码段(共享代码段)
- 要跳转的段是非一致代码段
同时修改CS和EIP的指令
指令 | 含义 |
JMP FAR | 远跳转 |
CALL FAR | 远调用 |
RETF(return far) | 远返回 |
INT(interrupt) | 中断 |
IRET(interrupt return) | 执行到中断程序或过程的远返回 |
只改变EIP的指令
指令 | 含义 |
JMP | 跳转 |
CALL | 调用 |
JCC(jump condition code) | 跳转指令状态码/条件跳转 |
RET | 返回 |
此处选择JMP FAR 进行分析
JMP Selector:Offset
形如:JMP 0x20:0x00452610
- Selector为段选择子
- Offset为要跳转的偏移
CPU是如何执行的呢?
- 拆分段选择子
- 根据段选择子查表得到段描述符
- 权限检查
- 加载段描述符
- 代码执行
1.首先确定段选择子
0x20 即 0000 0000 0010 0000
Index | TI | RPL | |
二进制值 | 0000 0000 0001 1 | 0 | 11 |
十进制值 | 3 | 0 | 3 |
含义 | 索引为3 | 查询GDT表 | 请求特权等级为3 |
四种情况可以跳转:
- 代码段
- 调用门
- TSS任务段
- 任务门
对应的段描述符地址 = GDT表首地址 + 索引× 段描述符长度 = GDT表首地址 + 索引 × 8
所以:对应的段描述符地址 = 0x8003f000 + 3×8= 0x8003f000 + 24 = 0x8003f000 + 0x18 = 0x8003f018
使用windbg查看对应的段描述符地址
得到对应的段描述符为:00cffb00`0000ffff
LDT
IDTR
中断描述符表寄存器,用于存放中断描述符表IDT的32位线性基地址和16位表长度值
TR
任务寄存器,用于存放当前任务TSS段的16位段选择符、32位基地址、16位段长度和描述符属性值
什么是段选择子?
就是在GDT/LDT中的偏移,就是索引
在保护模式下,段寄存器是96位:32位基址,32位界限域,16位属性,16位段选择子
段寄存器的值都是操作系统默认给分配的
控制寄存器
CR0
PE位,为0是实模式,为1是保护模式
TS任务切换,R3进R0的时候,TS置1
WP写保护,当写保护位置1,对只读页面进行写操作的时候,会产生页故障
段寄存器的结构