羽夏看Linux内核——中断与分页相关入门知识

写在前面

  此系列是本人一个字一个字码出来的,包括示例和实验截图。如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我

你如果是从中间插过来看的,请仔细阅读 羽夏看Linux系统内核——简述 ,方便学习本教程。

中断

  中断通常是由CPU外部的输入输出设备(硬件)所触发的,供外部设备通知CPU有事情需要处理,因此又叫中断请求,英文为Interrupt Request。中断请求的目的是希望CPU暂时停止执行当前正在执行的程序,转去执行中断请求所对应的中断处理例程,中断处理程序由哪有IDT表决定。
  80x86有两条中断请求线:非屏蔽中断线,NMI,全称NonMaskable Interrupt和可屏蔽中断线,INTR,全称Interrupt Require

不可屏蔽中断

  什么是不可屏蔽中断CPUEFLAG之中有一个位,它是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虽然被称为“软件中断”,但其本质是异常,EFLAGIF位对INT N是无效。

异常处理

  无论是由硬件设备触发的中断请求还是由CPU产生的异常,处理程序都在IDT表。常见的异常处理程序如下表所示:

错误类型 (IDT表)中断号
页错误 0xE
段错误 0xD
除零错误 0x0
双重错误 0x8

控制寄存器

  控制寄存器用于控制和确定CPU的操作模式。控制寄存器有Cr0Cr1Cr2Cr3Cr4Cr1被保留了,Cr3用于页目录表基址,其他的将继续详细讲解。

Cr0

  Cr0是一个十分重要的寄存器,可以说它是总开关的集合体。如下图所示:

  PE位是启用保护模式(Protection Enable)标志。若PE = 1是开启保护模式,反之为实地址模式。这个标志仅开启段级保护,而并没有启用分页机制。若要启用分页机制,那么PEPG标志都要置位。
  PG位是启用分页机制。在开启这个标志之前必须已经或者同时开启PE标志。PG = 0PE = 0,处理器工作在实地址模式下。PG = 0PE = 1,处理器工作在没有开启分页机制的保护模式下。PG = 1PE = 0,在PE没有开启的情况下无法开启PGPG = 1PE = 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 = 010-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分页来说,线性地址对应的物理地址是有对应关系的,它被分成了三个部分,每个部分都有它具体的含义。线性地址分配的结构如下图所示:

  第一个部分指的是PDEPDT的索引,第二部分是PTEPTT的索引,第三个部分是在PTE指向的物理页的偏移。PDT被称为页目录表,PTT被称为页表。PDEPTE分别是它们的成员,大小为4个字节。我们接下来将详细介绍每一个部分是咋用的。

10-10-12 分页整体结构

  通过实验我们了解了它们的结构,接下来将详细介绍了。根据实验结果的体验,可以给出如下图:

  分页并不是由操作系统决定的,而是由CPU决定的。只是操作系统遵守了CPU的约定来实现的。物理页是什么?物理页是操作系统对可用的物理内存的抽象,按照4KB的大小进行管理(Intel是按照这个值做的,别的CPU就不清楚了),和真实硬件层面上的内存有一层的映射关系,这个不是保护模式的范畴,故不介绍。

PDE 与 PTE

  前面我们简单了解PDEPTE,接下来将学习它们的属性结构,结构如下:

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以上很浪费。所以PDEPTE中有个G位,如果为1,刷新TLB时将不会刷新它指向的页。

PWT 位

  当PWT = 1,写缓存的时候也要将数据写入内存中。

PCD 位

  当PCD = 1时,禁止某个页写入缓存,直接写内存。比如,做页表用的页,已经存储在TLB中了,可能不需要再缓存了。

注意事项

  • PTE可以没有物理页,且只能对应一个物理页。
  • 多个PTE也可以指向同一个物理页。
  • PDEPTE重合的属性共同决定着最终物理页的属性。比如P位,如果有一个是0,那么最终的物理页就是无效的。但是PDEPTE它们的属性的影响范围是不一样的。数值上:物理页的属性 = PDE属性 & PTE属性。

PAE 分页

  PAE分页是啥,其实他就是2-9-9-12分页的英文缩写。为什么要有2-9-9-12分页,其实还是物理页不够用了,需要扩展。但想要足够的物理页,位数在那里,你想大也大不了。那么我就需要扩展物理页地址的位数,于是乎2-9-9-12分页诞生了,它整体分页的结构如下:

  与10-10-12分页不同的地方就是,多了一层名为页目录指针表的东西,英文缩写为PDPTT。每个PDEPTE被扩展为8个字节,物理地址描述的位数扩展为24位,故可以描述更多的物理页,但个数减半,变成了512个。下面详细查看它们的结构。
  首先看PDPTT的结构。由2-9-9-122可知第一部分由两位二进制组成,那么最多有4种结果。也就是为什么有五个成员,它的结构如下图所示:

  然后是PDE,既然学过了10-10-12分页,直接看下面的结构示意图吧:

非大页

大页

  然后是PTE,同理不多说了:

  我们之前做一道作业题目知道,我写的shellcode写到一个页上,它并没有执行权限,但它不是代码,仍然可以被我执行。为了弥补这个漏洞,Intel给我们补了一个硬件层面上的漏洞,它是一个位,处于PDEPTE的最高位,如下图所示:

  如果最高位是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分页,属性是PDPEPDEPTE三个属性共同决定的。如果是10-10-12分页就是PDEPTE共同决定。
  2. 不同的CPU这个表的大小不一样。
  3. 只要Cr3变了,TLB立马刷新,一核一套TLB
  如果Cr3改了,TLB刷新重建高2G以上很浪费。所以PDEPTE中有个G标志位,如果G位为1刷新TLB时将不会刷新PDE/PTEG位为1的页,当TLB满了,根据统计信息将不常用的地址废弃,最近最常用的保留。
  TLB有不同的种类,用于不同的缓存目的,它在X86体系里的实际应用最早是从Intel486CPU开始的,在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 ThroughPWT = 1时,写Cache的时候也要将数据写入内存中。
  PCD全称为Page Cache DisablePCD = 1时,禁止某个页写入缓存,直接写内存。比如,做页表用的页,已经存储在TLB中了,可能不需要再缓存了。

下一篇

  羽夏看Linux内核——内核加载