Windows内核(9)——保护模式下的内存管理(分段机制)
Windows
保护模式下的内存管理:
注:主要参考来自 内核第二讲,内存保护的实现,以及知识简介,局部描述符,全局描述符. - iBinary - 博客园 (cnblogs.com)
Win32汇编–Windows 的内存管理机制 - 木屐 - 博客园 (cnblogs.com) (来源小甲鱼)
80386指的是32位系统
三种模式
实模式: 指的是操作系统在启动的时候,这时候访问的内存都是实际的物理内存.也就是在这个时机,系统会在实模式下填写全局描述符表 GDT
保护模式: 当各种表填写好了,那么我们的内存也被保护了.这个是否我们的进程就不会直接访问物理地址了.进而产生了保护行为,我们的内存就有了 可读 可写,可执行一说了.
虚拟86模式: 操作系统启动的是否,运行的都是实际的16位汇编.那么现在我们假设有一个16位程序要启动.那么修改了我们物理地址的内存,那么保护模式不就没用了.所以为了防止这一情况的产生,操作系统做了一个虚拟86模式,也就是说可以运行16位汇编程序.
保护模式如何保护内存
操作系统启动的会从实模式启动,然后会切换到保护模式,此时操作系统会通过做表,然后通过查表,进而判断内存是否可以访问或者读写.
在16位汇编中,我们可以通过段+偏移的方式来寻找内存.管理内存.那么我们现在要对内存做管理.那么就要分段了.
什么是分段管理?
基本概念:
逻辑地址: 逻辑地址指的是 段 + 偏移的方式,我们程序中每一行代码都是逻辑地址.
线性地址: 线性地址我们可以理解为逻辑上连续的物理地址.
物理地址: 物理地址就是内存条的地址,也就是我们说的实际地址.
段+偏移 查表,会查到线性地址,每个段+偏移都会查到一块物理地址.
00401000~00402000,在逻辑上我们看的是连续的,但是通过查表转换为物理地址的是否则不是连续的. (有可能通过查表,得出的物理地址不是连续的.但是逻辑地址是连续的. )
为什么要查表得到物理地址?
进程间的数据肯定不是共享的,虽然逻辑地址可能都是0x400000但是物理地址不是一样的,这样就可以实现进程隔离
表格是如何做
最基本的就是希望通过这个表,能够查出:
1.段起始地址
2.大小
3.结束地址.
4.当前内存的保护属性.
段描述符(Segment Descriptor)
段描述符分为全局描述符表(GDT)和局部描述符表(LDT),并且它们的大小都是64位。
段描述符是一个8字节(64位)的结构体(被拆开成两个部分),包含段的各种信息。其结构如下:
1 | struct Segment_Descriptor |
低 4 字节(32 位)
- Base 15:00 (0-15位):
- 段的基地址的低 16 位。
- Limit 15:00 (16-31位):
- 段限长的低 16 位。
高 4 字节(32 位)
- Base 23:16 (32-39位):
- 段的基地址的中间 8 位。
- Type (40-43位):
- 描述符类型字段,包含了段类型和访问权限等信息。
- 常见的值:
- 代码段(执行段):可执行、不可写、可读。
- 数据段:不可执行、可写、可读。
- 各位的作用:
- 第 40 位:是否访问过。
- 第 41 位:可读/写标志。
- 第 42 位:是否向下扩展(数据段)/是否一致性段(代码段)。
- 第 43 位:可执行标志(1 为代码段,0 为数据段)。
- S (44位):
- 描述符类型标志,0 为系统描述符(如任务状态段 TSS),1 为代码或数据段。
- DPL (45-46位):
- 特权级别(Descriptor Privilege Level),用来确定访问该段所需的最低特权级别。取值范围为 0(最高特权级)到 3(最低特权级)。
- P (47位):
- 存在标志(Present),1 表示段存在于内存中。
- Limit 19:16 (48-51位):
- 段限长的高 4 位。
- AVL (52位):
- 系统保留位,可供操作系统使用。
- L (53位):
- 64 位代码段标志(Long Mode),32 位模式下应为 0。
- D/B (54位):
- 默认操作大小标志(D 位),对于代码段,1 表示 32 位操作(即默认操作是 32 位的),0 表示 16 位操作。对于堆栈段,1 表示默认堆栈指针为 ESP,0 表示默认堆栈指针为 SP。
- 有时也称为 Big 位(B 位)。
- G (55位):
- 粒度标志(Granularity),0 表示段限长的单位为字节,1 表示单位为 4KB。
- Base 31:24 (56-63位):
- 段的基地址的高 8 位。
TYPE位展开
处理器在访问段时,会根据Type
字段和DPL
字段检查访问权限。例如:
- 若尝试写操作但
RW位
为0(只读),则访问会被拒绝。 - 当前代码段的特权级别(CPL)和目标段的
DPL
也会进行比较,确定是否允许访问。
如何让一个段不可读,不可写,不可执行?很简单,把p标志位设置为0即可
如何让一个段可读可写可执行?
如果想要一块内存可读可执行,可写.那么需要建立两个表.分别让TYPE为 = 2,然后 = 8即可.
查询Data段的权限,利用的寄存器是DS去做索引(在下面段选择子会说为什么是寄存器做索引)
查询Code段的权限,利用的寄存器是CS去做索引
所以可以做两张表,一个描述Data,一个表述Code
查询Code段的权限,利用的寄存器是CS去做索引
内存映射与共享
内存映射(Memory Mapping)是将虚拟地址空间的一部分映射到物理内存的一部分。共享内存(Shared Memory)是指不同进程之间共享同一块物理内存。内存映射和共享通常通过段描述符和页表来实现。
内存映射的基本过程
- 查找空闲段描述符:
- 系统会在段描述符表(如 GDT 或 LDT)中查找一个空闲的段描述符。
- 填充段描述符:
- 段描述符按照一定的格式填好,包括基地址(Base Address)、段限长(Limit)、段类型(Type)、特权级别(DPL)等。
- 设置段描述符的 P 标志:
- 段描述符中的 P 标志(Present 位)表示段是否存在。P 标志设置为 1 表示段存在,设置为 0 表示段不存在。如果 P 标志为 0,该段描述符不可用。
如果是恶意程序,直接找到这个软件的段描述符的p,全改为0,这样段全部无效,软件直接崩溃
线性地址的范围怎么计算
base Address + limit 则可以形成一块内存区域.
假设段A的基地址是 00012345h,段界限为5678h,并且是以字节为单位(G = 0),那么
段基地址+ 段界限 = 000179BDh, 那么在00012345h ~ 000179BDH则是段A的线性地址
计算就是 段基地址 + 段界限 (也可以理解为,起始地址+一块区域.则这块区域就是线性地址)
如果属性中 G位 = 1,那么就是按照4K去计算.那么就是这样算0012345h + (5678 *4K) + 0FFFh = 0568b344h
那么段A的线性地址就是从 0012345h ~ 0568b344h
段选择子,进行查表
段寄存器的作用:
在保护模式下,段寄存器就不再有用了呢?
答案是否定的!实际上段寄存器更有用了,虽然在寻址上不再有分段的限制问题,但在保护模式下,一个地址空间是否可以被写入,可以被多少优先级的代码写入,是不是允许执行等涉及保护的问题就出来了?
要解决这些问题,必须对一个地址空间定义一些安全上的属性。段寄存器这时就派上了用途,不妨将这些属性存放在段寄存器中!
80386的段寄存器仍然是16位的,无法放下保护模式下64位的段描述符。如何解决这个新的问题呢?解决办法是把所有段的段描述符顺序放在内存中的指定位置,组成一个段描述符表(Descriptor Table)
而段寄存器中的16位用来做索引信息,指定这个段的属性用段描述符表中的第几个描述符来表示。
这时,段寄存器中的信息不再是段地址了,而是段选择器(Segment Selector)。可以通过它在段描述符表中“选择”一个项目以得到段的全部信息。
段选择子(Segment Selector)
段选择子是一个16位的值,用来选择一个段描述符。段选择子的结构如下:
- 索引(Index): 13位,用于在GDT(全局描述符表)或LDT(局部描述符表)中索引段描述符。
- TI位(Table Indicator): 1位,指示段选择子是指向GDT(0)还是LDT(1)。
- RPL(Request Privilege Level): 2位,表示请求特权级。
描述符,全局描述符,局部描述符
80386中引入了两个新的寄存器来管理段描述符表。一个是48位的全局描述符表寄存器GDTR,一个是16位的局部描述符表寄存器LDTR。那么,为什么有两个描述符表寄存器呢?
GDTR指向的描述符表为全局描述符表GDT(Global Descriptor Table)。它包含系统中所有任务都可用的段描述符,通常包含描述操作系统所使用的代码段、数据段和堆栈段的描述符及各任务的LDT段等;全局描述符表只有一个。
LDTR则指向局部描述符表LDT(Local Descriptor Table)。80386处理器设计成每个任务都有一个独立的LDT。它包含有每个任务私有的代码段、数据段和堆栈段的描述符,也包含该任务所使用的一些门描述符,如任务门和调用门描述符等。
总结就是 全局描述符是全部进程共同享有的,局部描述符是每一个进程独有的。
全局描述符表(GDT)
- 共享性质:GDT 是所有进程共同共享的表格,通常由操作系统在系统启动时初始化。
- 描述符类型:GDT 包含系统段描述符(如任务状态段 TSS)、代码段描述符、数据段描述符等。
- 用途:主要用于定义内核和操作系统核心部分的段描述符。进程间共享的段(如内核段)也定义在 GDT 中。
问:GDT最多多少项?
局部描述符表(LDT)
- 独有性质:每个进程可以有一个独立的 LDT,这些 LDT 是进程私有的。
- 描述符类型:LDT 通常包含用户模式下使用的段描述符,例如用户进程的代码段和数据段描述符。
- 用途:主要用于定义特定进程的段,这些段在进程间不共享,提供了每个进程独立的内存段。
GDT 和 LDT 的比较
- 作用范围:
- GDT:系统范围内有效,定义了全局段,所有进程都可以访问。
- LDT:进程范围内有效,定义了进程特有的段,仅该进程可访问。
- 创建和管理:
- GDT:由操作系统在启动时创建并初始化,通常不会频繁修改。
- LDT:由操作系统为每个进程创建和管理,进程切换时切换 LDT。
- 访问控制:
- GDT:通常包含操作系统和内核段描述符,具有较高的特权级别。
- LDT:包含用户进程的段描述符,具有较低的特权级别,主要用于用户模式下的内存访问。
其中LDT = A进程,那么就执行A进程的操作.切换进程,并且保存进程的各种信息.以及各种表. 如果我们换成了B进程,那么就会切换到B进程.
CPU从逻辑地址获取物理地址的过程梳理
例如运行一个EXE,逻辑地址0xAC2710里面存的值是0x55,那么它真正的物理地址是多少呢?
因为这是代码段,所以段选择子是CS,这里是0x1B
解析段选择子
0x001B:0000 0000 0001 1011
段选择子的结构如下:
- 索引(Index): 13位,用于在GDT(全局描述符表)或LDT(局部描述符表)中索引段描述符。
- TI位(Table Indicator): 1位,指示段选择子是指向GDT(0)还是LDT(1)。
- RPL(Request Privilege Level): 2位,表示请求特权级。
所以
Index位:0000 0000 0001 1 换成数字就是3,下标为3
TI位:0
RPL位:11
段选择子中的RPL和段描述符的区别
RPL 表示当前请求者的权限级别,可能会根据上下文变化。例如,用户态代码(RPL = 3)请求访问一个段时,其 RPL 会反映用户态的特权级别。
DPL 固定表示段的访问要求。代码段、数据段、系统段等的 DPL 决定了什么特权级别的代码可以访问这些段。
解析GDT表
于是CPU现在就去找TI位指定的GDT表
我们可以用Windbg查询GDT表
可以看到GDT指向的地址是0x80b93800
查看这个地址,上面根据段选择子得到的Index为3,也就是第三项,因为段描述符每一项是64位,也就是8个字节,因此第三项是箭头指向的位置
接下来就是解析一下这个GDT:00cffb00 0000ffff
- 基地址(Base Address): 分为基地址低位(Base Low)、基地址中位(Base Middle)、基地址高位(Base High),总共32位。
- 段限长(Limit): 分为段限长低位(Limit Low)和段限长高位(Limit High),总共20位。
- 类型(Type): 4位,用于指定段的类型和访问权限。
- S位(Descriptor Type): 描述符类型(系统段或代码/数据段)。系统段负责切换线程,API调用,和内存没关系,但是数据段(也叫存储段),就是负责内存权限的
- DPL(Descriptor Privilege Level): 描述符的特权级别。(用来判断是3环还是0环)决定哪一环可以用
- P位(Present): 段是否存在。1表示有效,0表示无效
- AVL(Available for system use): 系统可用位。
- L位(Long mode): 用于64位模式的指示位。
- DB位(Default operation size / Big): 指示默认操作大小(16位或32位)。0表示16位段,1表示32位段
- G位(Granularity): 段限长的粒度(字节或4KB)
Base有32bit,但是Limit只有20bit,颗粒度设置为4KB,表示的地址最大为(2^20-1) * 4k 这样表示0x00000000~0xfffff000,则可以节省表的大小
limit计算公式 2^20 * 4k +0xfff
解析结果如下:
Segment Limit:0xffffff
Base Address:0x00000000
Type:0xb
s:0x1
DPL:0x3(3环可用,和RPL匹配)
p:0x1
AVL:0x0
L:0x0
D/B:1
G:1(说明粒度为1)
limit=(0xffffff-1) * 4k + 0xffff = 4G,也就是在 Base Address 开始之后的4G的空间里面,属性是Type
Type为0xB
对照着表得到的属性是Execute/Read,accessed
逻辑地址=>物理地址 = Base_Address +逻辑地址0xAC2710
所以当前举的例子的物理地址就是0xAC2710(在未开启分页机制,线性地址就是物理地址)
GDTR全局描述符表寄存器
GDTR(全局描述符表寄存器,Global Descriptor Table Register)是一个48位寄存器,用于存储全局描述符表(GDT)的基地址和限制(limit)。它的组成和作用如下:
组成
- 基地址(Base Address):
- 32位(32-bit):存储GDT的基地址,即GDT在内存中的起始地址。
- 用于指示全局描述符表在内存中的起始位置。
- 限制(Limit):
- 16位(16-bit):存储GDT的大小,以字节为单位。
- 限制值表示GDT的最大字节偏移量,从基地址开始的范围。
- 项的数目为:(gdtr.limit+1)/8
用Windbg发现gdtr寄存器只有32位?
但是其实这两个是同一个寄存器,gdtr是指Base_Address,gdtl指的是Limit
特权指令:
LGDT(Load Global Descriptor Table Register):该指令用来加载GDTR的值,设置全局描述符表的位置和长度。
SGDT(Store Global Descriptor Table Register):该指令用来存储当前GDTR的值,通常用于调试和操作系统初始化过程。
在Ring3是可以用SGDT这个指令来拿当前GDTR的信息的,但是不能用LGDT,也就是在三环,只能读取但是不能设置(要是在三环能设置那还得了)
1 |
|
但是64位不能内联汇编,用不了特权指令咋整
可以用联合编译,或者用_sgdt这些内部函数
多核CPU
但是有一个问题,就是我们发现,打印出来的Base_Address有时候会不一样,这是为什么?
这是由于我们的处理器是多核的,所以系统会为每一个Cpu都创建一个表,所以Base_Address会不一样
确定核数:
1 | //Ring3 |
在指定CPU上运行代码:
1 | //Ring3 |
分段机制已经被放弃
分段的缺点:
- 外部碎片:由于段的大小不固定,可能会导致外部碎片问题。
- 复杂性:分段机制相对复杂,需要段描述符表和相应的硬件支持。
分页机制 (Paging)
分页机制是一种内存管理方案,它将内存划分为固定大小的页(如4KB),并将这些页映射到物理内存中的页框。页表存储了虚拟地址到物理地址的映射关系。
分页的优点:
- 消除外部碎片:由于页的大小固定,不会出现外部碎片问题。
- 简化内存管理:操作系统可以更容易地管理内存,因为所有页的大小相同。
- 虚拟内存支持:可以将不常用的页换出到硬盘,提高内存利用率。
用Windbg查看,发现4GB空间Base全为0,Limit为0xffffffff
说明逻辑地址直接等于了线性地址,变相放弃了分段机制!
下一章详细说分页机制