羽夏看Linux内核——中断与分页相关入门知识
- 2022 年 8 月 9 日
- 笔记
- Linux 系统内核, 羽夏看Linux内核
写在前面
此系列是本人一个字一个字码出来的,包括示例和实验截图。如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 羽夏看Linux系统内核——简述 ,方便学习本教程。
中断
中断通常是由CPU
外部的输入输出设备(硬件)所触发的,供外部设备通知CPU
有事情需要处理,因此又叫中断请求,英文为Interrupt Request
。中断请求的目的是希望CPU
暂时停止执行当前正在执行的程序,转去执行中断请求所对应的中断处理例程,中断处理程序由哪有IDT表决定。
80x86
有两条中断请求线:非屏蔽中断线,NMI
,全称NonMaskable Interrupt
和可屏蔽中断线,INTR
,全称Interrupt Require
。
不可屏蔽中断
什么是不可屏蔽中断
?CPU
的EFLAG
之中有一个位,它是IF
位。如果它被置0。如果有可屏蔽中断告诉CPU
有中断来了,你能先执行我的代码呢?可是IF
位是0,对不起,我听不见。左耳朵进,右耳朵出。反之,我会处理。常见的不可屏蔽中断有电脑长按关机、键盘输入等等。当非可屏蔽中断产生时,CPU在执行完当前指令后会里面进入中断处理程序,非可屏蔽中断不受那个位的影响,一旦发生,CPU
必须处理。为了方便观看,给个EFLAG
图解:
那么CPU
是如何处理我们的不可屏蔽中断呢?我们先来看如下表格:
(IDT表)中断号 | NMI | 说明 |
---|---|---|
0x2 | 不可屏蔽中断 | 80×86 中固定为 0x2 |
如果处理不可屏蔽中断,CPU
会调用2号中断。涉及的IDT
表和中断门的知识如果忘却请查看前面的教程。
可屏蔽中断
什么是可屏蔽中断
,我就不赘述了。在硬件级,可屏蔽中断是由一块专门的芯片来管理的,通常称为中断控制器。它负责分配中断资源和管理各个中断源发出的中断请求.为了便于标识各个中断请求,中断管理器通常用IRQ
,全称为Interrupt Request
,后面加上数字来表示不同的中断。
那么CPU
是如何处理我们的可屏蔽中断呢?我们先来看如下表格:
(IDT表)中断号 | IRQ | 说明 |
---|---|---|
0x30 | IRQ0 | 时钟中断 |
0x31-0x3F | IRQ1-IRQ15 | 其他硬件设备的中断 |
如果自己的程序执行时不希望CPU去处理这些中断,可以用CLI
指令清空EFLAG
寄存器中的IF
位,用STI
指令设置EFLAG
寄存器中的IF
位。
硬件中断与IDT
表中的对应关系并非固定不变的,可以参考白皮书的Chapter 10 Advanced Programmable Interrupt Controller(APIC)
进行了解。
异常
异常通常是CPU
在执行指令时检测到的某些错误,比如除0、访问无效页等。中断与异常之间有一些相似之处,但它们是不一样的:中断来自于外部设备,是中断源(比如键盘)发起的,CPU
是被动的;而异常来自于CPU
本身,是CPU
主动产生的。INT N
虽然被称为“软件中断”,但其本质是异常,EFLAG
的IF
位对INT N
是无效。
异常处理
无论是由硬件设备触发的中断请求还是由CPU
产生的异常,处理程序都在IDT
表。常见的异常处理程序如下表所示:
错误类型 | (IDT表)中断号 |
---|---|
页错误 | 0xE |
段错误 | 0xD |
除零错误 | 0x0 |
双重错误 | 0x8 |
控制寄存器
控制寄存器用于控制和确定CPU的操作模式。控制寄存器有Cr0
、Cr1
、Cr2
、Cr3
、Cr4
。Cr1
被保留了,Cr3
用于页目录表基址,其他的将继续详细讲解。
Cr0
Cr0
是一个十分重要的寄存器,可以说它是总开关的集合体。如下图所示:
PE
位是启用保护模式(Protection Enable)标志。若PE = 1
是开启保护模式,反之为实地址模式。这个标志仅开启段级保护,而并没有启用分页机制。若要启用分页机制,那么PE
和PG
标志都要置位。
PG
位是启用分页机制。在开启这个标志之前必须已经或者同时开启PE
标志。PG = 0
且PE = 0
,处理器工作在实地址模式下。PG = 0
且PE = 1
,处理器工作在没有开启分页机制的保护模式下。PG = 1
且PE = 0
,在PE
没有开启的情况下无法开启PG
。PG = 1
且PE = 1
,处理器工作在开启了分页机制的保护模式下。
WP
位对于Intel 80486
或以上的CPU
,是写保护(Write Proctect)标志。当设置该标志时,处理器会禁止超级用户程序(例如特权级0的程序)向用户级只读页面执行写操作;当CPL < 3
的时候,如果WP = 0
可以读写任意用户级物理页,只要线性地址有效。如果WP = 1
可以读取任意用户级物理页,但对于只读的物理页,则不能写。
Cr2
当CPU访问某个无效页面时,会产生缺页异常,此时,CPU会将引起异常的线性地址存放在CR2中,如下图所示:
Cr4
Cr4
的结构如下图所示:
VME
用于虚拟8086模式。PAE
用于确认是哪个分页,PAE = 1
,是2-9-9-12
分页,PAE = 0
是10-10-12
分页。PSE
是大页是否开启的总开关,如果置0,就算PDE
中设置了大页你也得是普通的页。至于分页到底是什么,将会在下一篇进行讲解。
中断小节
有些结构的位我并没有详细介绍,详情请查看白皮书的控制寄存器的篇章,如下图所示:
如果对分页不了解看不懂没关系,下一篇将会介绍相关知识。
分页
在讲解分页基础之前,我们先大体了解CPU
是如何在保护模式下访问数据的,如下图所示:
比如我们执行mov eax,ds:[0x12345678]
这句汇编指令的时候,0x12345678
这个线性地址会传递给CPU
,先查询TLB
和缓存
有没有,有的话直接取出来返回;如果没有,经过MMU(内存管理单元)
处理得到物理地址,通过固定的分页模式直接找到,取出数据返回。
前面的教程讲解了段的机制,接下来将介绍页的机制。CPU为了方便管理物理内存,按照页的方式进行管理内存。可用的所有内存可以类比为一本书,而所有的内存被分为这本书的一个页。对于32位来说,它有10-10-12
分页和2-9-9-12
分页。其中10-10-12
分页最为简单,故拿其作为详细讲解,作为分页讲解的基础。
我们都了解一个进程都有4GB的虚拟地址空间,它们并不是真正的地址,而是个索引。它通过某种方式进行转换,从而指向真正的物理地址,示意图如下所示:
而虚拟地址也被称作线性地址。举个例子,比如某个进程里面我想读取一个0x12345678
,它就是线性地址,通过一些转换,找到了对应的物理地址0x10101010
,如下图所示:
每个进程都有一个CR3
,准确的说是都一个CR3
的值。CR3
本身是个寄存器,一核一套。CR3
里面放的是一个真正的物理地址,指向一个物理页,一共4096字节
,如下图所示:
对于10-10-12
分页来说,线性地址对应的物理地址是有对应关系的,它被分成了三个部分,每个部分都有它具体的含义。线性地址分配的结构如下图所示:
第一个部分指的是PDE
在PDT
的索引,第二部分是PTE
在PTT
的索引,第三个部分是在PTE指向的物理页的偏移。PDT
被称为页目录表,PTT
被称为页表。PDE
和PTE
分别是它们的成员,大小为4个字节。我们接下来将详细介绍每一个部分是咋用的。
10-10-12 分页整体结构
通过实验我们了解了它们的结构,接下来将详细介绍了。根据实验结果的体验,可以给出如下图:
分页并不是由操作系统决定的,而是由CPU
决定的。只是操作系统遵守了CPU
的约定来实现的。物理页是什么?物理页是操作系统对可用的物理内存的抽象,按照4KB
的大小进行管理(Intel
是按照这个值做的,别的CPU
就不清楚了),和真实硬件层面上的内存有一层的映射关系,这个不是保护模式的范畴,故不介绍。
PDE 与 PTE
前面我们简单了解PDE
和PTE
,接下来将学习它们的属性结构,结构如下:
P 位
表示PDE
或者PTE
是否有效,如果有效为1
,反之为0
。
R/W 位
如果R/W = 0
,表示是只读的,反之为可读可写。
U/S 位
如果U/S = 0
,则为特权用户(super user),即非3环权限。反之,则为普通用户,即为3环权限。
PS位
这个位只对PDE
有意义。如果PS == 1
,则PDE
直接指向物理页,不再指向PTE
,低22位是页内偏移。它的大小为4MB
,俗称“大页”。
A 位
是否被访问,即是否被读或者写过,如果被访问过则置1
。
D 位
脏位,指示是否被写过。若没有被写过为0
,被写过为1
。
注意,下面的三个位的讲解将涉及 TLB 和控制寄存器相关知识,为了保证文章的完整性,故先介绍。之后将会详细讲解。
G 位
表示是否为全局页。它的作用是什么呢?举个例子,操作系统的进程的高2G
映射基本不变,如果Cr3
改了,TLB
刷新重建高2G
以上很浪费。所以PDE
和PTE
中有个G
位,如果为1,刷新TLB
时将不会刷新它指向的页。
PWT 位
当PWT = 1
,写缓存的时候也要将数据写入内存中。
PCD 位
当PCD = 1
时,禁止某个页写入缓存,直接写内存。比如,做页表用的页,已经存储在TLB中了,可能不需要再缓存了。
注意事项
PTE
可以没有物理页,且只能对应一个物理页。- 多个
PTE
也可以指向同一个物理页。 PDE
和PTE
重合的属性共同决定着最终物理页的属性。比如P位
,如果有一个是0,那么最终的物理页就是无效的。但是PDE
和PTE
它们的属性的影响范围是不一样的。数值上:物理页的属性 = PDE属性 & PTE属性。
PAE 分页
PAE
分页是啥,其实他就是2-9-9-12
分页的英文缩写。为什么要有2-9-9-12
分页,其实还是物理页不够用了,需要扩展。但想要足够的物理页,位数在那里,你想大也大不了。那么我就需要扩展物理页地址的位数,于是乎2-9-9-12
分页诞生了,它整体分页的结构如下:
与10-10-12
分页不同的地方就是,多了一层名为页目录指针表
的东西,英文缩写为PDPTT
。每个PDE
和PTE
被扩展为8个字节,物理地址描述的位数扩展为24位
,故可以描述更多的物理页,但个数减半,变成了512个。下面详细查看它们的结构。
首先看PDPTT
的结构。由2-9-9-12
的2
可知第一部分由两位二进制组成,那么最多有4种结果。也就是为什么有五个成员,它的结构如下图所示:
然后是PDE
,既然学过了10-10-12
分页,直接看下面的结构示意图吧:
非大页
大页
然后是PTE
,同理不多说了:
我们之前做一道作业题目知道,我写的shellcode
写到一个页上,它并没有执行权限,但它不是代码,仍然可以被我执行。为了弥补这个漏洞,Intel
给我们补了一个硬件层面上的漏洞,它是一个位,处于PDE
和PTE
的最高位,如下图所示:
如果最高位是1
,说明被保护。如果这个是数据,且这个X
位被置为1
,则会被报出异常不能执行。反之,和正常的10-10-12
分页没什么两样。
一个进程的线性地址仍是4GB
的线性空间,有再多的物理页有啥用呢?在10-10-12
分页下,假设进程一启动,就把所有的物理页都挂上,且没有任何交换。那么只能启动一个;如果在2-9-9-12
分页下,同样的情况,它可以启动4个进程。这个就是2-9-9-12
分页的意义。
TLB
CPU
通过页的方式对物理内存进行了,需要通过某种运算方式才能访问真正的内存。但是,如果频繁访问某一个线性地址,每次都得通过推算到真正的物理地址然后进行读写操作,是不是挺浪费效率的?Intel
就考虑到性能的问题,提供了TLB
这一个机制,提供缓存提高读写效率。
TLB
的全称为Translation Lookaside Buffer
,它的结构如下:
LA(线性地址) | PA(物理地址) | ATTR(属性) | LRU(统计) |
---|
对于TLB
,给出如下说明:
1. ATTR(属性):如果是2-9-9-12
分页,属性是PDPE
、PDE
、PTE
三个属性共同决定的。如果是10-10-12
分页就是PDE
和PTE
共同决定。
2. 不同的CPU
这个表的大小不一样。
3. 只要Cr3
变了,TLB
立马刷新,一核一套TLB
。
如果Cr3
改了,TLB
刷新重建高2G以上很浪费。所以PDE
和PTE
中有个G
标志位,如果G
位为1刷新TLB
时将不会刷新PDE/PTE
的G
位为1的页,当TLB
满了,根据统计信息将不常用的地址废弃,最近最常用的保留。
TLB
有不同的种类,用于不同的缓存目的,它在X86
体系里的实际应用最早是从Intel
的486CPU
开始的,在X86
体系的CPU
里边,一般都设有如下4组TLB
:
第一组:缓存一般页表(4K字节页面)的指令页表缓存:Instruction-TLB
第二组:缓存一般页表(4K字节页面)的数据页表缓存:Data-TLB
第三组:缓存大尺寸页表(2M/4M字节页面)的指令页表缓存:Instruction-TLB
第四组:缓存大尺寸页表(2M/4M字节页面)的数据页表缓存:Data-TLB
CPU缓存
CPU缓存是位于CPU
与物理内存之间的临时存储器,它的容量比内存小的多但是交换速度却比内存要快得多。它可以做的很大,但不是TLB
,它们有很大的不同。TLB
存的是线性地址与物理地址的对应关系,CPU缓存存的是物理地址与内容对应关系。
更多的细节请参考白皮书的Chapter 11 Memory Cache Control
,本篇教程主要是针对内核安全层面,就不再赘述了。
PWT 与 PCD
PWT
全称为Page Write Through
,PWT = 1
时,写Cache
的时候也要将数据写入内存中。
PCD
全称为Page Cache Disable
,PCD = 1
时,禁止某个页写入缓存,直接写内存。比如,做页表用的页,已经存储在TLB
中了,可能不需要再缓存了。
下一篇
羽夏看Linux内核——内核加载