中断

什么是中断?

中断是能够打断CPU指令序列的事件,它是在CPU内外,由硬件产生的电信号。CPU接收到中断后,就会向OS反映这个信号,从而由OS就会对新到来的数据进行处理。不同的事件,其对应的中断不同,而OS则是通过中断号(也即IRQ线)来找到对应的处理方法。不同体系中,中断可能是固定好的,也可能是动态分配的。

中断产生后,首先会告诉中断控制器。中断控制器负责收集所有中断源的中断,它能够控制中断源的优先级、中断的类型,指定中断发给哪一个CPU处理。

中断控制器通知CPU后,对于一个中断,会有一个CPU来响应这个中断请求。CPU会暂停正在执行的程序,转而去执行相应的处理程序,也即OS当中的中断处理程序。这里,中断处理程序是和特定的中断相关联的。

中断描述符表

那么CPU是如何找到中断服务程序的呢?为了让CPU由中断号去查找到对应的中断程序入口,就需要在内存中建立一张查询表,也即中断描述符(IDT)。在CPU当中,有专门的寄存器IDTR来保存IDT在内存中的位置。这里需要注意的是,常说的中断向量表,是在实模式下的,中断向量是直接指出处理过程的入口,而中断描述符表除了入口地址还有别的信息。

IDTR有48位,前32位保存了IDT在内存中的线性地址,后16位则是保存IDT的大小。而IDT自身,则是一个最大为256项的表(对应了8位的中断码),表中的每个向量,是一个入口。这里IDT表项的异常类型可以分为三种,其表项的格式也不同:

  • 任务门:利用新的任务方式去处理,需要切换TSS。它包含有一个进程的TSS段选择符,其偏移量部分没有用,linux没有采用它来进行任务切换。
  • 中断门:适宜处理中断,在进入中断处理时,处理器会清IF标志,避免嵌套中断发生。中断门中的DPL(Descriptor privilege Level)为0,因此用户态不能访问中断门,中断处理程序都是用中断门来激活的,并且限制在内核态。
  • 陷阱门:适宜处理异常,和中断门类似,但它不会屏蔽中断。

以下是32bit中的IDT表项。

IDT门

值得注意的是,CPU还提供一种门,调用门,它是linux内核特别设置的,通常通过CALL和JMP指令来使用,能够转移特权级。

实模式和保护模式

在了解CPU是如何通过中断向量表调用具体的服务程序之前,首先需要了解CPU的工作方式。

对于IA-32架构,它支持实模式、保护模式和系统管理模式。

实模式以拓展对方式实现了8086CPU的程序运行环境,处理器在刚刚上电和重启后时,处于实模式,其寻址空间最大为1M(2^20)。实模式的主要意义,在于提供更好的兼容性,开发者能够直接使用BIOS中断,从而在boot阶段不必关注硬件的具体实现。实模式主要还是为进入保护模式进行准备。

8086处理器有16-bit寄存器和16-bit的外部数据总线,但能够访问20-bit的地址,因为它引入了“分段机制”,一个16bit的段寄存器包含了一个64KB的段的基址。而段寄存器+16bit的指针,就能够提供20bits的地址空间。其计算方式为:16位基地址左移4位+16位偏移量=20位。

保护模式是处理器的根本模式。保护模式可以直接为实模式程序提供保护的,多任务的环境,这种特性被称为虚拟8086模式,它实际上是保护模式的一种属性。保护模式能够为任何任务提供这种属性。在保护模式中,地址依然通过“段+偏移量”的形式来实现,但此时段寄存器中保存的不再是一个段的基址,而是一个索引。通过这个索引可以找到一个表项,里面存放了段基址等许多属性,这个表项也就是段描述符,而这个表也就是GDT表。

保护模式的最大寻址是2^32次方,也即4G,并且可以通过PAE模式访问超过4G的部分。它有4个安全级别,内存操作时,有安全检查。其分页功能带来了虚拟地址和物理地址的区别。

系统管理模式为操作系统或者执行程序提供透明的机制去实现平台相关的特性,例如电源管理、系统安全。

对于Intel 64架构,它增加了两种子模式。

兼容模式允许绝大部分16bit-32bit应用无需编译就能在64bit下运行,它类似于保护模式,有4G的地址空间限制。

64bit模式在64bit线性地址空间上运行应用程序,通用寄存器被增加到64bits。它取消了分段机制,其默认地址长度为64bits。

x64寻址

在保护模式下(32bit),物理地址的翻译分为两步:逻辑地址翻译(段)和线性地址翻译(页)。逻辑地址利用16bit segment selector和32bit offset来表示。处理器首先要将逻辑地址翻译为线性地址(32bit)。这个翻译过程如下:

  1. 通过segment selector,在对应的GDT或LDT中去找到段描述符;
  2. 检查段描述符,访问是否合法,段是否能够访问,偏移量是否在范围之内;
  3. 将段基地址和偏移量相加来获取线性地址的值。

在IA-32e模式下(64bit),逻辑地址的翻译步骤和上述过程类似,唯一不同的是,其段基地址和偏移量,都是64bit,而不是32bit的。线性地址同理也是32bit的。

段寻址,也即将内存分成不同的段,利用段寄存器能够找到其对应的段描述符,从而获得相关的段基址、大小、权限等信息。

段寻址

段选择子Segment selector的示意图如下:

段选择子

段选择子会被存在段寄存器当中,其中最低两位为RPL(cs寄存器不同,最低位位CPL)。而第三位Table Indicator则是表示该从GDT还是LDT寻找对应的段描述符,后面的bits就是对应的index了。

为了减少地址翻译的开销,处理器提供了6个段寄存器,CS,SS,DS,ES,FS,GS。通常来说一个程序至少有CS、DS、SS三个selector。假设程序要使用段来访问地址,那么必须将segment selector载入段寄存器当中。对此,Intel是提供了特殊的指令的,直接载入的指令包括MOV,POP,LDS,LES等。而隐含的载入则包括CALL,JMP,RET,SYSENTER等等。它们会改变CS寄存器(有时也会改变其它段寄存器)的内容。

而在IA-32e模式下(64bit mode),ES,DS,SS段寄存器都不会使用了,因此它们的域会被忽视掉,而且某些load指令也被视为违法的,例如LDS。与ES,DS,SS段有关的地址计算,会被视为segment base为0。为了保证兼容性,在64bit mode当中,段load指令会正常执行,从GDT、LDT中读取时,也会读取寄存器的隐藏部分,并且值都会正常的载入。但是data、stack的segment selector和描述符都会被忽略掉。
而FS和GS段在64bit mode能够手动使用,它们的计算方式为(FS/GS).base+index+displacement。用这种方式去进行内存访问时,是不会进行检查的。载入的时候不会载入Visible Part,也即Segment Selector,也就是把段机制给忽略了。

IDT,LDT和GDT

中断向量表提供了一个入口,但这个入口还需要进一步的计算。这个入口的计算,是通过段寻址来实现的。而段的信息,则是保存在LDT和GDT当中。

段描述符的结构如下图:

段描述符

段描述符最重要的部分是DPL位,它会在权限检查的时候使用。在进程需要装载一个新的段选择子时,会判断当前的CPL和RPL是否都比相应的DPL权限高,如果是则允许加载新的段选择子,否则产生GP。

在操作系统中,全局描述符只有一张,也即一个CPU对应一个GDT。GDT可以存放在内存中的任何地址,但CPU必须知道GDT的入口,因此有一个寄存器GDTR用来存放GDT的入口地址,它存放了GDT在内存中的基址和表长。

GDT&LDT

但是在64位系统当中,段机制就被取代了,而页表项也能够达到数据访问的保护目的。但是对于不同特权级之间的控制流转移,还是和原来的机制一样。在64-bit模式中,GDT依然存在,但不会改变,而其寄存器被拓展到了80bit。

而GDT中会包含一个LDT段的段描述符,LDT是通过它的段描述符来访问的。

在IA-32e模式下,段描述符表可以包含2^13个8-byte描述符。这里,描述符分为两种,段描述符会占据一个entry(8bit),而系统描述符会占据两个entry(16bit)。而GDTR和LDTR被拓展为能够保存64bit的基地址。其中,IDT描述符、LDT、TSS描述符和调用门描述符都被拓展称为了16bytes。

64bit IDT描述符的格式如下

64bit IDT

64bit LDT描述符的格式如下

64 bit LDT

中断的处理过程

在intel 手册上看到的大图,很详细的解释了IA-32模式和IA-32e模式下的系统架构,它也就包含了中断处理和线性地址的翻译过程。

IA-32

IA-32e

在中断产生之后,处理器会将中断向量号作为索引,在IDT表中找到对应的处理程序。IDT表将每个中断/异常向量和一个门描述符关联起来。在保护模式下,它是一个8-byte的描述符(与GDT,LDT类似),IDT最大有256项。IDT能够保存在内存中的任何位置,处理器用IDTR寄存器来保存它的值。

在中断/陷阱门描述符中,segment selector指向了GDT或当前LDT中的代码段描述符,而offser域指向了exception/interrupt的处理过程。

IA-32e

在执行call这一步的时候,倘若handler过程会在一个更低的权限执行,那么就会涉及到stack switch。当stack switch发生时,segment selector和新的栈指针都需要通过TSS来获取,在这个栈上,处理器会把之前的segment selector和栈指针压入栈中。处理器还将保存当前的状态寄存器在新的栈上。

如果handler过程会在相同的权限执行,处理器会把状态寄存器的值保存在当前的栈上。

IA-32e

从中断处理程序返回时,handler必须使用IRET指令。它与RET类似,但它会将保存的标志位恢复到EFLAGS寄存器中。如果stack switch在调用过程中发生了那么IRET会切换到中断前的stack上。在中断过程中,权限级的保护与CALL调用过程类似,会对CPL进行检查。

64-bit模式下的中断处理

在64bit模式下,中断和异常的处理与非64bit模式下几本一致,但也存在一些不同的地方。包括有:

  • IDT所指向的代码是64bit代码
  • 中断栈push的大小是64bit
  • 栈指针(SS:RSP)在中断时,无条件的被push(保护模式下是由CPL来决定的)
  • 当CPL有变化时,新的SS会被设置为NULL
  • IRET的过程不同
  • stack-switch的机制不同
  • 中断stack的对齐不同

其中,64bit的IDT门描述符在前面已经介绍了。IST(interrupt Stack Table)用于stack-switch。通过中断门来调用目标代码段时,它必须为一个64bit的代码段(CS.L=1,CS.D=0)。如果不是也会触发#GP。在IA-32e模式下,只有64bit的中断和陷阱门能够被调用,遗留的32bit中断/陷阱都被重新定义为64bit的。

在遗留模式中,IDT entry的大小是16/32bit,它决定了interrupt-stack-frame push时的大小。并且SS:ESP只在CPL发生改变时被压入stack中。在64bit模式下,interrupt-stack-frame push的大小被固定为8bytes(因为只有64bit模式的门能够被调用),而且SS:RSP是无条件压入栈中的。遗留模式下,Stack pointer能够在任何地址进行push,但是IA-32e模式之下,RSP必须是16-byte边界对齐的,而stack frame在中断处理程序被调用时也会对齐。而在中断服务结束时,IRET也会无条件的POP出SS:RSP,即使CPL=0。

IA-32e模式下,stack-switching机制被替代了,它被称为interrupt stack table(IST)。

遗留模式下,在64bit中,中断如果造成了权限级的改变,那么stack就会switch,但是这时不会载入新的SS描述符,而只会从TSS中载入一个inner-level的RSP。新的SS selector被强制设置为NULL,这样就能够处理内嵌的far transfers。而旧的SS和RSP会被保存在新的栈上。也就是说stack-switch机制除了SS selector不会从TSS加载之外,其余都一样。

而新的IST模式,则是无条件的进行stack switch。它是基于IDT表项中的一块区域实现的,它的设计目的,是为特殊的中断(NMI、double-fault、machine-check)等提供方法。在IA-32e模式下,一部分中断向量能够使用IST,另一部分能够使用遗留的方法。

IST在TSS中,提供7个IST指针,在中断门的描述符当中,由一个3bit的IST索引位,它们用来找到TSS中IST的偏移量。通过这个机制,处理器将IST所指向的值加载到RSP当中。而当中断发生时,新的SS selector被设置为NULL,并且SS selector的RPL区域被设置为新的CPL。旧的SS、RSP、RFLAGS、CS和RIP被push入新的栈中。如果IST的索引为0,那么就会使用修改后的、旧的stack-switch机制。

保护机制

Intel 64/IA-32架构提供了段/页级别的保护机制,它们利用权限级,来限制对于的段/页的访问,例如重要的OS代码和数据能够被放在更高权限级的段中,操作系统会保护它们不被应用程序访问。当保护机制启用时,每次内存访问都会被检查,这些检查包括:

  • Limit
  • Tyep
  • Privilege level
  • Restriction of Procedure entry-points
  • Restriction of instruction set

通过CR0寄存器当中的PE flag能够开启保护模式,打开段保护机制;而页保护机制则是在分页机制启用时,自动开启的。虽然64bit中,不再使用分段机制了,但代码段依然存在。对于地址计算来说,其段地址被视为0,CS描述符当中的内容被忽略,但其余部分保持一致。代码段描述符、selector依然存在,它们在处理器的操作模式、执行权限级上依然发挥作用。其工作方式如下:

CS描述符中会使用一个保留位,Bit 53被定义为64 bit flag位(L),并且被用来在64bit/兼容模式之间切换。当CS.L = 0时,CPU处于兼容模式,CS.D则决定了数据和地址的位数为16/32bit。如果CS.L为1,那么只有CS.D = 1是合法的,并且地址和数据的位数是64bit。在IA-32e模式下,CS描述符当中的DPL位被用来做执行权限的检查(与32bit模式一样)。

代码段

Limit Checking

在段描述符当中,有一个limit field,它防止程序访问某个段之外的的内存位置,其有效值由G flag来决定,对于数据段来说,其limit还由E flag和B flag决定。在64bit模式下,处理器不会对代码段活着数据段进行limit check,但是会对描述符表的limit进行检查。

Type checking

段描述符包含两个type 信息,S flag和type field。处理器会使用这个信息,来检查对段和门的不正确使用。S flag表示descriptor的类型,它包括系统/代码/数据三种类型。在处理一个段选择子时,处理器会在:

  • 将segment selector载入段寄存器:寄存器只能包含对应的描述符类型
  • 指令访问段时:段只能被相应的指令访问
  • 指令包含segment selector时:指令只能对某些特定类型的段/门进行访问
  • 进行某些具体操作时:far call、far jump,对调用门、任务门的call/jump等,会判断描述符中的类型是否符合要求。

Privilege levels

处理器的段保护机制包含有4个privilege levels,从0到3,0最高,3最低。处理器利用这种机制,来防止一个低权限的进程,访问更高权限的部分。为了实现这个目的,处理器使用3种类型的权限级:

  • CPL:当前执行任务的权限级。它保存在CS和SS段寄存器的bit 0-1中。通常,CPL和当前代码段的权限一致,当跳转到一个有不同权限的代码段时,CPL会发生变化。如果目标是一致代码段,则会延续当前的CPL。
  • DPL:segment或者gate的权限级。它保存在段或者门的描述符当中,当当前的代码段执行,需要访问一个段或者gate的时候,这个段/门的DPL就会被拿来与CPL和RPL进行比较。在不同的环境下,DPL的意义也是不同的。
  • RPL:与segment selector有关的,能够对权限进行覆盖的权限级。它保存在segment selector的bit 0-1中。处理器会通过CPL和RPL来判断对segment的访问是否合法。即使请求访问某个段的程序,拥有比段更高的权限,如果RPL不是有效的,访问还是会被拒绝。也就是说如果RPL把CPL高,那么RPL会覆盖CPL。RPL能够保证提权的代码,不能随意访问一个segment段,除非它自身有这个权限。直观的说,必须CPL和RPL都比DPL要高,只有这种情况下,才会允许这个段的访问。其主要目的,是允许高权限为低权限提供服务的时候,能够通过较低的权限来加载段。

门调用符与权限检查:

gate

TSS

处理器执行工作的单位,被称为task。一个task分为两个部分,task的执行空间和task-state segment(TSS)。前者指的是code/stack/data segment,而后者则定义了组成前者的各个段。task是由TSS的segment selector来识别的。当一个任务被加载到处理器中执行时,segment selector、基址、limit、TSS的段描述符等都会被加载到**task register(TR)**当中去。分页启动时,页目录的基址还会载入到控制寄存器CR3当中去。

一个任务的状态,由一系列的寄存器和TSS来定义。这里,处理器定义了5个数据结构,来处理任务相关的活动。

  • TSS
  • Task-gate描述符
  • TSS描述符
  • Task寄存器
  • EFLAGS寄存器中的NT flag

为了恢复一个task,处理器所需要的信息,保存在一个系统段中,它被称为TSS。在64bit模式下,它的格式如下:

gate

而TSS描述符,则和其他的段一样,是由一个段描述符来定义的,它的结构在上文中已经给出了(与LDT是一致的),它只能放在GDT当中,不能放在LDT或者IDT当中。

Task寄存器保存了当前TSS的段选择子和整个段描述符。它包含可见和不可见两个部分(能否被软件修改)。段选择子位于可见部分,指向GDT当中的TSS描述符。不可见部分则是用来保存TSS的段描述符(能够提高执行效率)。

gate

中断的发生

中断指的是CPU在运行时,系统内发生了需要“急需处理”的事件,于是乎CPU暂停了当前正在执行对程序,转而去执行相应的时间处理程序,在处理完之后返回原来的地方执行。那么这些事件包含:(1)外部中断指的是那些CPU外的周边原件引发的中断,例如I/O中断,I/O设备异常(接下来我们把它称为“中断”);(2)内部中断指的是在CPU内部执行时,由程序自身、异常、陷阱(例如程序中的断点)产生的中断(包括硬件中断和软件中断,其中软件中断是指一系列的指令)(接下来我们把它称作“异常”)。

这两种中断类型不同,产生方式也不一样:

(1)中断因为是由外设硬件发出的,所以需要由中断控制器(APIC)参与其中。在中断发出后,首先由APIC来进行处理。这种方式解决两个问题:(a)有大量的外设,而CPU的引脚资源有限,所以不能满足所有的直连需要;(b)如果设备中断和CPU直连,那么在MP系统中,中断负载等需求就无法实现了。

可以看到,在x86_64系统下,local APIC通过I/O APIC接受链接,I/O APIC把中断处理成中断消息,并按照规则转发给local APIC。APIC决定了由哪个CPU来处理中断,为某个引脚产生特定的中断向量(中断投递协议),并把中断请求发送给对应的CPU处理。CPU之间的中断通信,也是通过APIC来完成的。

I/O设备通过IRQ线与APIC相连,APIC将信号转化为对应的向量,并把这个向量放在它的I/O端口上,允许CPU通过数据总线来读这个向量;随后它发送一个信息给CPU的INTR引脚,从而触发中断,当CPU通过把中断信号写进APIC的I/O端口时,把INTR线清除。目前,外部中断的编号是从32开始,这是由于0-31号中断是留给异常和内部中断使用的,INTEL手册上也给出了这样一个表,详细说明了中断号的对应关系(新的CPU确实用的编号也变多了,就比如#VE):

(2)异常则是由CPU自身产生的中断。那么这种中断是否需要APIC介入呢?除了I/O APIC的中断信号外,local APIC还会接收其他来源的中断,例如CPU LINT0/LINT1中断(本地连接的I/O),性能计数器中断、APIC内部错误中断等。但这不意味着所有异常都需要中断控制器的参与;软件中断的中断号是可以由指令直接给出的,因此是不需要中断控制器的参与的。

中断的屏蔽

注意,这里的中断屏蔽指的是对外部中断的屏蔽(mask)。CPU内部的中断(异常)是不能屏蔽的。在内核同步中,通常采用这种方式来屏蔽外部的中断;并结合自旋锁来保证中断不被打断。

IRQ和NMI分别是可屏蔽和不可屏蔽中断(例子:打印机中断和电源掉电)。CPU在处理NMI中断时,不从外部硬件接收中断向量号,其对应中断向量号固定为2。NMI中断通常用于故障处理(协处理器运算出错、存储器校验出错等危急事件)。NMI处理程序通常应以IRET指令来结束。IRQ则是可以屏蔽的一类中断,通过设置CPU中的IF位,可以对IRQ进行屏蔽,这个标志位可以通过软件来设置。例如在处理某个高优先级中断时,CPU收到了低优先级的中断,那么就会对它进行屏蔽。

而对于内核的同步,则是由操作系统内部来实现的。可以说,目前我们讨论的只是中断是如何被送到CPU的,而CPU把中断和异常送给操作系统,并操作系统做出反应的过程,则属于另一个范畴了(在另一片博文http://sec-lbx.tk/2017/02/15/中断相关)。

异常和中断的处理

我们可以把内核,理解成一个服务器,它不断处理着用户的各种请求。因此,它需要保证每项服务在处理时,不会互相造成影响;其并发的来源包括内核的抢占和中断处理等。在内核态,中断处理程序也可以嵌套,这种情况下中断处理程序必须永不阻塞,在它运行的期间不能发生进程切换(不过缺页异常是一个例外,它不会引起进一步的异常,所以缺页异常可以切换进程,提高效率)。中断处理程序既可以抢占其他的中断处理程序,也可以抢占异常处理程序。相反的,异常处理程序从不会抢占中断处理程序。如果已经在内核态了,就只可能发生缺页异常(当然,也包含有现在的EPT缺页),但它们不会进一步的进行导致缺页的操作。
异常处理程序通常包含三个部分:(1)在内核堆栈保存大多数寄存器的内容;(2)用高级的C函数对异常进行处理;(3)通过 ret_from_exception()函数,从异常处理程序退出。

中断处理程序与异常处理程序不同,因为当下运行的进程可能和中断完全无关。中断可以分为:I/O中断、时钟中断、处理器间中断。这里,以I/O中断为例。I/O中断必须能够为多个设备同时提供服务,而多个设备却可能会共享一个IRQ线。它往往包含四个部分:(1)在内核态堆栈中保存IRQ的值和寄存器的内容;(2)给为IRQ服务的PIC发一个应答,允许PIC进一步发出中断;(3)执行共享这个IRQ的所有设备的中断服务历程(do_IRQ()会执行与一个中断相关的所有中断服务历程,并且验证它的设备是否需要关注,这也与驱动注册相关);(4)跳转到ret_from_inrt()后终止。

IRQ的动态分配:IRQ线可能在最后时刻才和一个设备驱动相关联,这样即使几个硬件设备不共享IRQ,也能够让几个设备在不同时刻使用同一个IRQ向量。

处理器间的中断(IPI)

由某个CPU向系统中的其他CPU发送中断信号,它不由IRQ总线,而是由本地APIC的总线传递。Linux定义了这样几种处理器间中断。

  • CALL_FUNCTION:强制所有剩余CPU执行发送者传递过来的函数
  • RESCHEDULE_VECTOR:让被中断的CPU重新调度
  • INVALIDATE_TLB_VECTOR:强制CPU清洗TLB

软中断和tasklet

软中断是一种提高运行效率的手段,其核心思想是把不紧迫懂、可以延时处理的中断部分,在中断上下文外,由操作系统自行安排运行时机来运行。tasklet则是建立在软中断之上来实现的,它是I/O驱动中实现可延迟函数的主要方法。对于挂起的软中断,内核会用ksoftirqd进行检查和运行。

工作队列

工作队列是内核中,另外一种将工作推后的形式。其特点在于,它允许重新调度睡眠。其本质就是将工作交给内核线程处理。如果推后执行的任务需要睡眠、或者延时指定的时间再触发,则使用这种形式比较好;倘若推后的任务需要在一个tick内处理,那么还是选择软中断或者tasklet的形式比较好。



本文链接: http://home.meng.uno/articles/81b224e1/ 欢迎转载!

© 2018.02.08 - 2020.06.02 Mengmeng Kuang  保留所有权利!

UV : | PV :

:D 获取中...

Creative Commons License