保护模式(二)--页机制

c++

Posted by YiMiTuMi on April 6, 2021

保护模式主要保护内存靠段和页的机制,段保护简单点说就是一个寄存器去访问一个虚拟地址跨段之间的保护,页保护是通过虚拟地址去查找物理地址时,对当前地址的读写等保护。

在X86处理器中地址分段模式包括一下2种形式:

1)平坦模式:在平坦模式下,系统能访问一个连续的、不分段的地址空间,所有的段被映射到同一个地址空间(虚拟的),所有的段都有相同的基地址0,界限为4GB(32位)。我们所使用的操作系统,无论是 windows 还是 Linux 都运行在这种模式下。

2)多段模式:不同的段,如代码段、数据段、堆栈段等位于不同的段中。实际上Windwos 和 Linux 都使用了平坦模式。

一页是4KB。

CPU将线性地址转换成物理地址。

高2G的内存不能读写的主要原因是页的限制。

当给定一个线性地址、有效地址时,计算机需要将当前地址转换成物理地址后去查找值,将线性地址、有效地址转化成物理地址时,需要去查找一张表,如果这张表的地址是线性地址、有效地址时,会陷入死循环(即查找表的基地址也需要查表),所以当前表的地址不能是线性或有效地址必须要是实际的物理地址,所以CR3寄存器中存放这张表的基地址,这个地址是一个真实的物理地址。在所有寄存器中只有CR3存放的是实际的物理地址。

每个进程都有一个CR3,(准确的说是都是以CR3的值,CR3本身是一个寄存器,一个核,只有一套寄存器)

CR3指向一个物理页,一共4096字节:(CPU提供的,表是操作系统定义的)

CR3 ->  第一级(1024)  ->  第二级           -> 物理页
		   ↓
		   0           ->  0 - 1024(个表)   -> 物理页     
		   1           ->  0 - 1024(个表)   -> 物理页 			
	(0 - 1024)

每一级有1024个表,每个表又指向下一级的1024个表。即第一即1024个表,每个表又指向第二级每个第二级又存在 1024个。CR3指向的表一个程序一个。

线性地址(虚拟地址)、有效地址(逻辑地址)、物理地址

MOV eax, dword ptr ds:[0x12345678]

其中,0x12345678是有效地址

ds.Base + 0x12345678是线性地址,因为运行在平坦模式下基址一般都是0,所以 有效地址(逻辑地址) = 线性地址(虚拟地址)。

线性地址和有效地址都不是真正的地址,还有一个物理地址。

当物理内存用完时,会将不常用的内存缓存到文件中。

线性地址(虚拟地址):

线性地址(Linear Address)也叫虚拟地址(virtual address)是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。是一个32位无符号整数,可以用来表示高达4GB的地址,也就是,高达4294967296个内存单元。线性地址通常用十六进制数字表示,值得范围从0x00000000到0xfffffff)程序代码会产生逻辑地址,通过逻辑地址变换就可以生成一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址。如果没有启用分页机制,那么线性地址直接就是物理地址。

有效地址(逻辑地址):

逻辑地址是在有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址,也就是是机器语言指令中,用来指定一个操作数或是一条指令的地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址即物理地址。一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是个索引号,后面3位包含一些硬件细节 。

物理地址:

物理地址,CPU地址总线传来的地址,由硬件电路控制(现在这些硬件是可编程的了)其具体含义。物理地址中很大一部分是留给内存条中的内存的,但也常被映射到其他存储器上(如显存、BIOS等)。在没有使用虚拟存储器的机器上,虚拟地址被直接送到内存总线上,使具有相同地址的物理存储器被读写;而在使用了虚拟存储器的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到存储器管理单元MMU,把虚拟地址映射为物理地址。

在保护模式中,它们的含义是:

虚拟地址–(分段)-> 逻辑地址–(分页)-> 物理地址

虚拟内存查找物理内

通过第一级目录去找第二级目录,第二级目录找到物理页

每个物理页4096字节,所以存放1024个地址。

查找物理地址是CPU的执行过程。

分页模式

分页模式决定了当前CPU能够识别的物理内存的最大值。

10-10-12决定了当前CPU能够找到的最大物理内存就是4GB。

10-10-12 (PDI-PTI-物理页偏移)

10-10-12分页即将32位的地址分三份,为10位-10位-12位

10                - 10            - 12	
 ↓                  ↓               ↓
第一级目录地址   第二级目录地址    物理页目录地址

例:

000AA8A0 \\地址

0000 0000 00 - 00 1010 1010 - 1000 1010 0000 \\分完

10-10-12的来源:

1)先确定了页的大小4K = 2的12次方,所以后面的12位的功能就确定了。

2)当初的物理内存比较小,所以4个字节的PTE就足够了,加上页的尺寸是4K(PTT也是在一个页里,实际上它也是一个物理页),所以一个也最多保存1024个PTE,1024 = 2的10次方所以第2个10确定了。

3)PDT表也是1024个字节。

一个物理页是4096个字节是2的12次方,所以需要12个2进制数来寻址。

一个PDT和PTT是1024项,是2的10次方,所以需要10个2进制数来寻址。

PDE_PTE

注意:每个PDE和PTE的后三位为属性,计算地址不能使用。

第一级目录:

CR3指向的第一个目录页为PDT(页目录表,也是一个物理页),页目录表一共4096个字节,每4个字节为一项称为PDE,指向一个页表。

第二级目录:

第一级目录的每项都指向一个PTT(页表),每个页表也是4096个字节,每4个字节为一项称为PTE,指向一个物理页。

PDE一共32位,前20位代表一个物理地址指向一个PTE,低12位为属性。

PTE一共32位,前20位代表一个物理地址指向一个物理页,低12位为属性。

10-10-12一共可以有:

1024(个PTT) * 1024(个物理页) * 4096(一个物理页大小) = 4G的物理内存。

当前CPU能够找到的最大物理内存为4G。

PTE可以不指向物理页,也可以存在多个PTE同时指向一个物理页,但不允许一个PTE指向多个物理页。

0地址的PTE没有指向任何物理页。

判断两个线性地址是否在同一个物理页里,只需要判断前20位相同就可以,在CR3相同的情况下,后12位是线性地址在物理页的偏移。

操作系统给PDT和PTT提供了值。操作系统给我们分配了物理页,所以随便替换PTE的前20位是不行的,最好是自己定义一个变量获取当前变量的地址,这个物理页是一定存在的。

使0地址可以访问:有效的物理地址 + (p = 1) -> 有效的PTE

在4的线性地址中会有两个线性地址可以找到PDT和PTT两个表,我们一但申请内存操作系统或通过这两个值给我们的PDT和PTT两个表赋值,因为申请一个内存需要给该线性地址挂物理页,所以需要添加PDE和PTE的值,但是操作系统只能访问线性地址,PDT和PTT里的值为物理地址没办法直接访问,因此需要这两个线性地址。

PDE_PTE 属性

物理页的属性 = PDE属性 & PTE属性

一个物理页的属性是相同的。

例如0的地址不能读写,那么从0–fff都不能读写。(一个物理页4096个字节)

PDE属性

31         12     11     9      8     7    6     5     4     3    2    1   0

   页表基址           AVL        G    PS    0     A    PCD   PWT  U/S  R/W  P

PTE属性

31         12     11     9      8     7    6     5     4     3     2    1   0

   物理页基址          AVL       G    PAT   D     A    PCD    PWT   U/S   R/W   P

P位:有效位。0表示无效,判断地址时PDE和PTE都要有效。(判断物理页是否存在)

R/W: 0表示只读,1表示可读写。

U/S: 0表示只能0、1、2特权级可访问,1表示普通用户(3)也可以访问。

PWT:

PCD:

A: 0表示该页未被访问,1表示已被访问。

G: 全局页

D: 脏位。0表示该页未写过,1表示该页被写过。

PS: 只存在于页目录表。 0表示这是4KB页,指向一个页表。1表示PDE直接指向物理页无PTE,线性地址的低22位是页内偏移。 线性地址只能拆成2段:大小位4MB。 俗称“大页”。 10 - 22

当PTE,P位为0时:

当CPU找到PTE时,发现P位为0后,CPU就会走E号中断,判断是一下那种原因出现的E号中断。

只要P为0必须走E号中断这是CPU的逻辑,操作系统只能在E号中断处理程序中提供操作系统的业务处理函数。

a) 为于页面文件

页面文件偏移     0     0      保护      PFN*    0
31        12   11     10     9  5     4   1    0

11:转移位

10:原型位

当前PTE指向的物理页被别的地方使用了,之前PTE的物理页的值被保存到文件中了,同时CPU会修改31-12和4-1的值为文件保存位置的地址,然后将P位置为0,此时PTE32位中1-4的值加上12-31的值为保存在文件中的地址。

当CPU访问当前线性地址时,发现PTE的P位为0时,会走E号中断,当E号中断发现PTE和上面情况一样时,说明当前线性地址是有效的,但是当前线性地址对应的内容不在物理页上(或者说没有物理页),而在内存文件中。E号中断会找一个空闲的物理页挂到当前PTE上,然后查找保存的文件将之前保存的值都写上去。

b) 要求一个零页面

	0			0		0		保护		0     0
31		12		11		10		9  5   4  1   0

当前说明当前线性地址是有效的,还没有分配物理页,E号中断会中一个空白的物理页挂在上面让程序继续执行。

c)页面正在转移

当11位为1时,页面正在转移。

d)未知原因,查VAD

当CPU访问当前线性地址时,当P位为0,并且整个32位的PTE都为0时,有可能时分配了没有挂物理页,也有可能是当前地址无效,需要查找VAD。

VAD是一个二叉树,会记录当前内存中分配的所有线性地址。

当PDE/PTE的属性为只读但程序试图写入的时候也会发生缺页异常。

Windows操作系统启动后,一般会将 0 - 7FFFffff : U/S = 1; 80000000 - FFFFffff : U/S = 0;

当CPU将当前物理页给其他地方使用时,会将当前PTE的P位置为0。 PDE或PTE中有一个P位为0,CPU就会走中断报异常缺页,对应LDT表的E号中断。

PDT基址、PTT基址(线性地址连续,物理地址不连续)

如果系统要保证某个线性地址是有效的,那么必须为其填充正确的PDE与PTE,如果我们想填充PDE与PTE那么必须能够访问PDT与PTT这两个物理地址,所以要访问PDT与PTT必须要有一个有效的线性地址,要保证这个线性地址是有效的,就必须要保证这个线性地址已经被挂到了PDT与PTT中了。

这个线性地址一定是存在的不然操作系统也无法访问PDT与PTT。

所谓的PDT这个表其实不存在,PDT这个表其实也是一个PTT,其他的PTT指向的都是物理页里面存放数据,而这个PDT即PTT指向的也是一个个物理页,只不过写物理页里面存放的是地址,这些地址指向其余的PTT表,同时还有指向自己的PTE。

[PDT](PTT0) -> 物理页 -> PTT0地址
			
			-> 物理页 -> PTT1地址 

			-> 物理页 -> PTT2地址
					  .
					  .
					  .
			
[PTT1]      -> 物理页 - > 数据

[PTT2]      -> 物理页 - > 数据
			.
			.
			.

PDT基址:C0300000(线性地址)

C0300000找到的是一个物理页,同时C0300000存储也是页目录表的基址。

这个物理页是页目录表,本身也是页表,里面的每一项都指向一个PTT。

访问一个PDE:

C0300000 + N*4

PTT基址:C0000000 \ C0001000 (线性地址)

C0000000 为第一个PTT表的线性基址。

C0001000 为第二个PTT表的线性基址。

C0002000 为第三个PTT表的线性基址。
			.
			.
			.
C03FFFFF 为第1024个PTT表的线性基址。

页表被映射到了从0xC000000到0xC03FFFFF的4M地址空间(一个页表1024项,每项4个字节,一共1024个页表.)

每个PTT页表基址之间相差一个页的大小为1024个字节。

地址索引

10(PDI) - 10(PTI) - 12

PDI 页目录表索引

PTI 页表索引

访问页目录表的公式:

C0300000 + PDI * 4

访问页表的公式:

0xC000000 + PDI * 4096 + PTI * 4

每个PTT页表基址之间相差一个页的大小为1024个字节。

MmlsAddressValid函数逆向,判断当前地址是否有效,也是查找PTE和PDE。

2-9-9-12分页

2-9-9-12 = 2 + 9 + 9 + 12 = 32位的线性地址。

CR3 -> 第一级PDPT(4) -> 第二级 -> 第三级 -> 物理页 ↓ 0 (PDPTE) -> PDT(512个PDE) -> PTT(512个PTE) -> 物理页
1 (PDPTE) -> PDT(512个PDE) -> PTT(512个PTE) -> 物理页
2 (PDPTE) -> PDT(512个PDE) -> PTT(512个PTE) -> 物理页 3 (PDPTE) -> PDT(512个PDE) -> PTT(512个PTE) -> 物理页

其中一个PDPT表有4个8位的项(PDPTE),每项指向一个PDT,一个PDT里有512个8位的项(PDE),每一项指向一个PTT,一个PTT里有512个8位的项(PTE),每项指向一个4KB大小的物理页。

这个图和64G的寻址没有关系,这个是决定当前进程4G虚拟地址的,当极端情况是可以给4G的你虚拟内存全部挂上物理页。

64G的寻址意味着的是,PTE可以指向4G以外的位置,但是物理页的大小还是4K。

PTT和PDT都是一个页的大小。

PDPT表,里面有4项,因为剩下2个字节每项为PDPTE(Page-Directory-point-Table Entry)页目录指针表。每项占8个字节。每项里面保存的是一个指向PDT的指针。

2-9-9-12来源

2-9-9-12分页,又称为PAE(物理地址扩展)分页。

1)12(找物理页):页的大小是确定的,4KB不能随便改,所以12确定了。

2)9(找PTE) :如果想增大物理内存的访问范围,就需要增大PTE,考虑到对齐因素,增加到8个字节,即64位。因为一个页大小为4KB确定了,所以现在一个PTE为8个字节只能存放512项 = 2的9次方,所以PTI为9。

3)9(找PDE) :和PTE一样,增加到8个字节,即64位。因为一个页大小为4KB确定了,所以现在一个PDE为8个字节只能存放512项 = 2的9次方,所以PDI为9。

4)2(PDP):指向一个只有四个项的PDPT表

在2-9-9-12分页中,有效的物理地址只有36位,可寻址64G大小的范围。

1.PDPDE 页目录指针表项

63                        36 35            32
	 Reserved(Set to 0)          BaseAddr

31                               12 11      9 8           5    4     3   2   1    0
	Page-Directory Base Address	 	   avall    Reservad      PCD   PWT   RES    P = 1

Dbg寻址的时候要加上属性位后面的4位,将四位属性为置零使用。35-0都是地址。

2.PDE

注意:当PS = 1时是大页, 35 - 21位(15位)是大页的物理地址,这样36位的物理地址的低21位为0,这就意味着页的大小为2MB,且都是2MB对齐。

当PS = 0时, 35 - 12位时页表基址,低12位补0,共36位

大页模式(2MB):

63                          36 35            32
	Reserved(Set to 0)          BaseAddr
	
31                              21  20                     13   12   11       9   8   7    6   5   4    3   2   1   0
	Page-Directory Base Address	 	   Reserved(Set to 0)       PAT     Avall     G   PS   D   A  PCD  PWT U/S R/W  P

小页模式(4KB):

63                          36 35            32
	Reserved(Set to 0)             BaseAddr
	
31                                 12   11       9   8    7     6   5   4    3   2   1   0
	Page-Directory Base Address	           Avall     0   PS=0   0   A  PCD  PWT U/S R/W  P

PTE:

63                          36 35            32
	Reserved(Set to 0)             BaseAddr
	
31                                 12   11       9   8   7   6   5   4    3    2   1   0
	Page-Directory Base Address	           Avall     G  PAT  D   A  PCD  PWT  U/S R/W  P

PTE中35 - 12是物理页基址,24位,低12补0。

物理页基址 + 12位的页内偏移指向具体数据

一个PDPTE现在指向1G的物理地址,但是一个进程有4G的虚拟内存地址,所以4个PDPTE一共是寻址4G。

XD标志位(AMD中称为NX,即NO Excetion)

PDE/PTE结构

x   保留   35-12物理地址

段的属性有可读、可写和可执行。

页的属性有可读、可写。

当RET执行返回的时候,如果我修改堆栈里面的数据指向一个我提前准备好的数据(把数据当作代码来执行,漏洞都是依赖这点,比如SQL注入)。

所以,Intel就做了硬件保护,做了一个不可执行位,XD = 1时,那么你的软件溢出了也没关系,即使你的EIP蹦到了危险的“数据区”,也是不可以执行的!

再PAE分页模式下,PDE与PTE的最高位为XD/NX位。

TLB

1)通过一个线性地址访问一个物理页。比如:DWORD,其实未必是真正读的是4个字节,我们先读的PDE在读PTE最后读的4个字节的页。

2)在2-9-9-12会读24个字节如果跨页可能更多,

为了提高效率,只能做记录。

CPU内部做了一个表,来记录这些东西,这个表格是CPU内部的,和寄存器一样快,这表格:TLB(Translation Lookaside Buffer)。

TLB结构

LA(线性地址)    PA(物理地址)   ATTR(属性)    LRU(统计)
0x123456789    ...

说明:

1)ATTR(属性): 属性是PDPE、PDE、PTE三个属性AND起来的。如果是10-10-12就是PDE and PTE。

2)不同的CPU这个表的大小不一样。

3)只要CR3变了,TLB立马刷新,一个核一套TLB。

4)如果两个线性地址在同一个物理一中则只存一个。

5)TLB能存多少条和CPU型号有关系,如64条的TLB能存64个页。

6)invlpg清空TLB的数据。

PDE、PTE的G位:

操作系统的高2G映射基本不变,如果CR3改了,TLB刷新重建高2G以上很浪费。所以PDE和PTE中有个G标志位,如果G位为1刷新TLB时将不会刷新PDE/PTE的G位为1的页,当TLB满了,根据统计信息将不常用的地址废弃,最近常用的保留。

TLB种类:

第一组:缓存一般页表(4K字节页面)的指令页表缓存(Instructon-TLB)
第二组:缓存一般页表(4K字节页面)的数据页表缓存(Data-TLB)
第三组:缓存大尺寸页表(2M/4M字节页面)的指令页表缓存(Instruction-TLB)
第四组:缓存大尺寸页表(2M/4M字节页面)的数据页表缓存(Data-TLB)

刷新CR3:

_asm
{
	mov eax, CR3
	mov CR3, eax
}

CR0寄存器

CR0结构:

 31   30   29   30     19  18  17  16  15    6  5   4    3    2    1    0            
 PG   CD   NW              AM      WP           NE  ET   TS   EM   MP   PE

说明:

1.PE: CR0的位0是启用保护标志。PE=1保护模式、PE=0实地址模式 这个标志仅开启段保护,而没有启用分页机制。若要启用分页机制,那么PE和PG标志都要置位。

2.PG: 当设置该位时即开启了分页机制。在开启这个标志之前必须已经或者同时开启PE标志。

PG = 0 且 PE = 0 处理器工作在实地址模式下

PG = 0 且 PE = 1 处理器工作在没有开启分页机制的保护模式下

PG = 1 且 PE = 0 在PE没有开启的情况下无法开启PG (不会存在)

PG = 1 且 PE = 1 处理器工作在开启了分页机制的保护模式下

3.WP: 对于Intel 80486或以上的CPU,CR0的位16是写保护标志位,当设置该标志时,处理器会禁止超级用户程序(例如特权级0的程序)向用户级只读页面执行写操作:

当CPL < 3的时候:

如果 WP = 0 可以读写任意用户级物理页,只要线性地址有效。

如果 WP = 1 可以读写任意用户级物理页,但对于只能读的物理页,则不能写。

CR2寄存器

31                    0
	  缺页线性地址

说明:

当CPU访问某个无效页面时,会产生缺页异常,此时,CPU会将引起异常的线性地址存放在CR2中。

CR4寄存器

31                           10  9  8     7     6     5     4     3     2     1     0
		保留位(置为0)              PCE   PGE   MCE   PAE   PSE   DE    TSD   PVI   VME

PAE/PSE说明:

PAE = 1 时,为2-9-9-12分页 PAE = 0 是 10-10-12分页。

PSE为1是PS位才有效。 PSE = 1:

10-10-12 PS = 1    4M页     2-9-9-12   PS = 1  2M页
		 PS = 0    4K页                PS = 0  4K页

PSE = 0:

10-10-12 PS = 1    4K页     2-9-9-12   PS = 1  4K页
		 PS = 0    4K页                PS = 0  4K页	

PWT_PCD属性

CPU缓存:

1)CPU缓存是位于CPU与物理内存之间的轮式存储器,它的容量比内存小的多但是交换速度却比内存要快得多。

2)CPU缓存可以做的很大,有几K、 几十K、 几百K甚至上M的也有。

CPU缓存与TLB的区别:

TLB:

线性地址 <–> 物理地址

CPU缓存:

物理地址 <–> 内容

PWT: PWT = 1时, 写Cache的时候也要将数据写入内存中。PWT = 0时, 写Cache(缓存)的时候不写入内存中。

PCD: PCD = 1时, 禁止某个页写入缓存,直接写内存。

比如,做页表用的页,已经存储在TLB中了,可能不需要再缓存了。

总结

实模式访问的地址是真实的物理地址。

2G以上是内核才能访问的原因是U/S位的设置问题,如果将内核的某个页设置为1就可以在R3访问了。

0 1 2是系统环,可以访问系统页和用户页。0环是特权环 1、2环虽然不是特权级环,但是是系统环。3环是用户环可以访问用户页。

低2G(0-7FFFffff)物理内存几乎不同。

高2G(80000000-FFFFffff)物理内存几乎相同。

0-7FFFffff的前64K和后64K都时没有映射物理内存的。

10-10-12能识别的物理地址是4G,当一个进程挂满全部的物理页后需要4G(每个进程默认4GB的虚拟内存)后,再不切换页的情况下,4G的物理内存以全部占用,所以只能启动一个进程。

2-9-9-12能识别的物理地址是64G,当一个进程挂满全部的页后需要4G(每个进程默认4GB的虚拟内存)后,再不切换页的情况下,64G的物理内存还能识别剩下的60G,所以还能再支持7个满全部物理页的进程启动。

只有2个函数才会申请内存:

virtualAlloc底层ntvirtualAlloc:申请独享物理页,只有当前进程可以使用。

CreateFileMapping:申请共享物理页,所有进程可以使用。dll的本质都是通过CreateFileMapping创建出来的。可以防止注入。

malloc底层调用HeapAlloc(windows下),他没有申请内存,只是从申请号的内存中划分一块来用。

内存对齐是编译器做的操作,因为以4字节对齐效率高。

蓝色妖姬 – 相守