转载自 http://sec-lbx.tk/2017/02/15/中断相关/#TSS 感谢原文作者
中断是能够打断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表项。
值得注意的是,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。
在保护模式下(32bit),物理地址的翻译分为两步:逻辑地址翻译(段)和线性地址翻译(页)。逻辑地址利用16bit segment selector和32bit offset来表示。处理器首先要将逻辑地址翻译为线性地址(32bit)。这个翻译过程如下:
通过segment selector,在对应的GDT或LDT中去找到段描述符;
检查段描述符,访问是否合法,段是否能够访问,偏移量是否在范围之内;
将段基地址和偏移量相加来获取线性地址的值。
在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,也就是把段机制给忽略了。
中断向量表提供了一个入口,但这个入口还需要进一步的计算。这个入口的计算,是通过段寻址来实现的。而段的信息,则是保存在LDT和GDT当中。
段描述符的结构如下图:
段描述符最重要的部分是DPL位,它会在权限检查的时候使用。在进程需要装载一个新的段选择子时,会判断当前的CPL和RPL是否都比相应的DPL权限高,如果是则允许加载新的段选择子,否则产生GP。
在操作系统中,全局描述符只有一张,也即一个CPU对应一个GDT。GDT可以存放在内存中的任何地址,但CPU必须知道GDT的入口,因此有一个寄存器GDTR用来存放GDT的入口地址,它存放了GDT在内存中的基址和表长。
但是在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 LDT描述符的格式如下
64 bit LDT
在intel 手册上看到的大图,很详细的解释了IA-32模式和IA-32e模式下的系统架构,它也就包含了中断处理和线性地址的翻译过程。
在中断产生之后,处理器会将中断向量号作为索引,在IDT表中找到对应的处理程序。IDT表将每个中断/异常向量和一个门描述符关联起来。在保护模式下,它是一个8-byte的描述符(与GDT,LDT类似),IDT最大有256项。IDT能够保存在内存中的任何位置,处理器用IDTR寄存器来保存它的值。
在中断/陷阱门描述符中,segment selector指向了GDT或当前LDT中的代码段描述符,而offser域指向了exception/interrupt的处理过程。
在执行call这一步的时候,倘若handler过程会在一个更低的权限执行,那么就会涉及到stack switch。当stack switch发生时,segment selector和新的栈指针都需要通过TSS来获取,在这个栈上,处理器会把之前的segment selector和栈指针压入栈中。处理器还将保存当前的状态寄存器在新的栈上。
如果handler过程会在相同的权限执行,处理器会把状态寄存器的值保存在当前的栈上。
IA-32e
从中断处理程序返回时,handler必须使用IRET指令。它与RET类似,但它会将保存的标志位恢复到EFLAGS寄存器中。如果stack switch在调用过程中发生了那么IRET会切换到中断前的stack上。在中断过程中,权限级的保护与CALL调用过程类似,会对CPL进行检查。
在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代码和数据能够被放在更高权限级的段中,操作系统会保护它们不被应用程序访问。当保护机制启用时,每次内存访问都会被检查,这些检查包括:
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 field,它防止程序访问某个段之外的的内存位置,其有效值由G flag来决定,对于数据段来说,其limit还由E flag和B flag决定。在64bit模式下,处理器不会对代码段活着数据段进行limit check,但是会对描述符表的limit进行检查。
段描述符包含两个type 信息,S flag和type field。处理器会使用这个信息,来检查对段和门的不正确使用。S flag表示descriptor的类型,它包括系统/代码/数据三种类型。在处理一个段选择子时,处理器会在:
将segment selector载入段寄存器:寄存器只能包含对应的描述符类型
指令访问段时:段只能被相应的指令访问
指令包含segment selector时:指令只能对某些特定类型的段/门进行访问
进行某些具体操作时:far call、far jump,对调用门、任务门的call/jump等,会判断描述符中的类型是否符合要求。
处理器的段保护机制包含有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
处理器执行工作的单位,被称为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描述符,则和其他的段一样,是由一个段描述符来定义的,它的结构在上文中已经给出了(与LDT是一致的),它只能放在GDT当中,不能放在LDT或者IDT当中。
Task寄存器保存了当前TSS的段选择子和整个段描述符。它包含可见和不可见两个部分(能否被软件修改)。段选择子位于可见部分,指向GDT当中的TSS描述符。不可见部分则是用来保存TSS的段描述符(能够提高执行效率)。