zoukankan      html  css  js  c++  java
  • 操作系统-中断(2)IA-32/Linux的向量中断方式

    一、Intel定义下的异常和中断
    不同体系和教材往往对异常和中断有不同的定义。
    Intel定义:中断是一种典型的由I/O设备触发的、与当前正在执行的指令无关的异步事件;而异常是处理器执行一条指令时,由处理器在其内部检测到的、与正在执行的指令相关的同步事件。
    Intel将异常分为故障(fault)、陷阱(trap)和终止(abort),不能被屏蔽,一旦出现立刻响应处理。
    • 故障:引起故障的指令被启动后但未执行结束时,CPU检测到的一类与指令执行相关的意外事件。这些事件有些可以恢复,有些不可以(如溢出、非法操作码)。
    以页故障举例:执行每条指令都要访存(取指令、取操作数、存结果)。在保护模式下,每次访存都要进行逻辑地址向物理地址转换(由硬件MMU实现),在地址转换过程中会发现是否发生了页故障(由硬件发现)。
            缺页:页表项有效位为0,可通过读磁盘恢复故障
            段故障:地址越界:地址大于最大界限 
                          访问越级或越权(保护违例)
                                   •    越级:用户进程访问内核数据(CPL=3 ,DPL=0)
                                   •    越权:读写权限不相符(如对只读段进行了写操作)
    • 陷阱/自陷/陷入:当执行到陷阱指令(也称为自陷指令)时,CPU就调出特定的程序进行相应的处理,处理结束后返回到陷阱指令的下一条指令执行。
    陷阱指令异常称为编程异常(programmed exception),这些指令包括 INT n(软中断指令,n与操作系统有关)、int 3(断点)、into(溢出检查,OF=1?)、bound(地址越界检查)等。
    在IA-32/Linux中可以使用快速系统调用指令sysenter或者软中断指令int $0x80进行系统调用。
    利用陷阱机制可以实现程序调试功能:
    –    IA-32中,当CPU处于单步跟踪状态(TF=1且IF=1)时,每条指令都被设置成了陷阱指令,执行每条指令后,都会发生中断类型号为1的“调试”异常,从而转去执行单步跟踪处理程序,该程序将当前指令执行的结果(通用寄存器、内存内容)显示在屏幕上。
    单步跟踪处理过程中, CPU会自动把标志寄存器压栈,然后将TF和IF清0。这样,在单步跟踪处理程序执行过程中,CPU能以正常方式工作。
    单步处理结束、返回到断点处执行之前,再从栈中取出标志,以恢复TF和IF的值,使CPU回到单步跟踪状态。
    这样,下条指令又是陷阱指令,将被跟踪执行。如此下去,每条指令都被跟踪执行、直到将TF或IF清0为止。
    –    IA-32中,用于程序调试的“断点设置”陷阱指令为int 3,对应机器码为CCH。若调试程序在被调试程序某处设置了断点,则调试程序就在该处加一条int 3指令(该指令首字节改为CCH)。执行到该指令时,会暂停被调试程序的运行,并发出“EXCEPTION_BREAKPOINT”异常,以调出调试程序执行,执行结束后回到被调试程序执行。
    • 终止
    如果在执行指令过程中发生了严重错误, 例如, 控制器出现问题、 访问DRAM或SRAM时发生校验错等, 则程序将无法继续执行, 只好终止发生问题的进程,在有些严重的情况下,甚至要重启系统。 显然, 这种异常是随机发生的, 无法确定发生异常的是哪条指令。
     
    Intel将外中断分成可屏蔽中断(maskable interrupt)和不可屏蔽中断(nonmaskable interrupt,NMI)。
    • 可屏蔽中断:通过INTR引脚向CPU请求,可通过设置屏蔽字来屏蔽请求,若中断请求被屏蔽,则不会被送到CPU。 
    • 不可屏蔽中断:非常紧急的硬件故障,如:电源掉电,硬件线路故障等。通过NMI向CPU请求。一旦产生,就被立即送CPU,以便快速处理。这种情况下,中断服务程序会尽快保存系统重要信息,然后在屏幕上显示相应的消息或直接重启系统。
    每个中断具有不同处理优先级,表示事件的紧急程度,处理高一级中断时往往会部分或全部屏蔽低级中断。
     
    二、中断识别方式
    检测到异常或中断时,CPU须进行以下基本处理:
    ① 关中断(中断允许位清0):使CPU处于“禁止中断”状态,以防止新中断破坏断点(PC)、程序状态(PSW)和现场(通用寄存器)。 
    IA-32中的中断允许位就是EFLAG寄存器中的中断标志位IF。CPU在异常或中断响应过程中,大部分情况下(除into、bound和int $0x80指令引起的异常外)都会将标志寄存器EFLAGS中的IF清0,以禁止响应新的可屏蔽中断或者异常。
    CPU也可以在异常或中断处理程序中,通过执行指令sti或cli,将标志寄存器EFLAGS中的IF位置1或清0。
    ② 保护断点和程序状态:将断点和程序状态保存到栈(为支持嵌套处理)或特殊寄存器中
        PC→栈 或 EPC(专门存放断点的寄存器)
        PSWR →栈 或 EPSWR (专门保存程序状态的寄存器) 
                PSW(Program Status Word):程序状态字
                PSWR(PSW寄存器):如IA-32中的的EFLAGS寄存器
    ③ 识别中断事件:
    (1)软件识别(MIPS采用)
    设置一个异常状态寄存器,用于记录异常原因。操作系统使用一个统一的异常处理程序,该程序按优先级顺序查询异常状态寄存器,识别出异常事件。先查询到的先被处理。
    (例如:MIPS中位于内核地址0x80000180处有一个专门的异常/中断查询程序,它通过查询Cause寄存器来检测异常和中断类型,然后转到内核中相应的异常处理程序或中断服务程序进行具体的处理。
    (2)硬件识别(向量中断)(IA-32采用)
    向量中断是一种中断识别方式,所有事件都被分配一个中断类型号,每个中断都有相应的“中断服务程序”。用专门的硬件查询电路按优先级顺序识别异常,得到中断类型号,根据此号作为索引,到中断向量表中读取对应的中断服务程序的入口地址,送到EIP。

    三、IA-32中的异常和中断处理

    IA-32有256种不同类型的异常和中断,每个异常和中断都有唯一编号,称之为中断类型号/向量号。
    PS:如类型0为“除法错”(除数为0或结果溢出),类型1为“单步跟踪调试陷阱”(执行每条指令时若发现TF=IF=1),类型2为“NMI中断”(不可屏蔽外部中断),类型3为“断点陷阱”(执行int 3指令),类型14为“缺页”。
    每个异常和中断有与其对应的异常处理程序或中断服务程序,其入口地址放在一个专门的中断向量表或中断描述符表中。
    前32个类型(0~31)保留给CPU使用;剩余的由用户(机器硬件的用户,即操作系统)自行定义,部分用于可屏蔽中断,部分用于软中断。
    通过执行INT n(指令第二字节给出中断类型号n,n=32~255)使CPU自动转到OS给出的中断服务程序执行。
    软中断指令INT n被设定为一种陷阱异常,例如 ,Linux通过int $0x80指令将128号设定为系统调用,而Windows通过int $0x2e指令将46号设定为系统调用。

    Image(53)

    实地址模式(Real Mode)是Intel为80286及其之后的处理器提供的一种8086兼容模式。

    寻址空间1MB(2^20),存储管理采用分段方式,每段最大地址空间64KB(2^16)。

    物理地址=段地址*16+偏移地址,其中段地址在段寄存器中,即:

    指令地址=CS<<4+IP(当时CS只有16位)。中断向量表位于0000H~03FFH。共256组,每组占四个字节,共1KB。

    采用的是8086/8088的中断类型和中断方式。

    中断向量表中每一项是对应中断服务程序或异常处理程序的入口地址,被称为中断向量(Interrupt Vector)。

    实地址模式下没有分页管理机制!根据中断类型号查地址即可。

    IA-32的保护模式并不像实地址模式那样将异常处理程序或中断服务程序的入口地址直接填入00000H -003FFH存储区,而是借助于中断描述符表来获得异常处理程序或中断服务程序的入口地址。

    保护模式下,通过中断描述符表(Interrupt Descriptor Table,IDT)获得异常处理或中断服务程序入口地址,它是OS内核中的一个表,共有256个表项,每个表项占8个字节,共占用2KB。每一个表项是一个中断门描述符、陷阱门描述符或任务门描述符。

    IDTR寄存器中存放 IDT在内存的首地址。

    中断门描述符格式如图所示:

    • 段选择符用来指示异常处理程序或中断服务程序所在段的段描述符在GDT中的位置,其RPL=0;

    • 偏移地址则给出异常处理程序或中断服务程序第一条指令所在偏移量。

    • P:Linux总把P置1,表示段存在。(因为它从来不会把一个段交换到磁盘上, 而是以页面为单位交换。 )

    • DPL:访问本段要求的最低特权级。主要用于防止恶意应用程序通过 INT n 指令模拟非法异常而进入内核态执行破坏性操作。Linux把除了3、4、5、128外的绝大多数中断类型包括自定义中断类型,都设置为了0,因此漏洞较小。

    • DPL后一位为0,再后4位是TYPE。

    • TYPE:标识门的类型。TYPE=1110B为中断门;TYPE=1111B为陷阱门;TYPE=0101B为任务门。

    IA-32中,每条指令执行后,下条指令的48位逻辑地址(虚拟地址)由16位段描述符CS和32位段偏移量EIP指示 。

    每条指令执行过程中,CPU会根据执行情况判定是否发生了某种内部异常事件,并在每条指令执行结束时判定是否发生了外部中断请求。

    (由此可见,异常事件和中断请求的检测都是在某一条指令执行过程中进行的,显然由硬件完成)

    在CPU根据CS和EIP取下条指令之前,会根据检测的结果判断是否进入中断响应阶段

    (异常和中断的响应也都是在某一条指令执行过程中或执行结束时进行的,显然也由硬件完成)

    IA-32中中断检测过程 

    x86体系架构包含一个可编程中断控制器PIC(Programmable Interrupt Controller),用于收集外部中断并将其发送给CPU。外部设备不能直接和CPU链接。
    现在的多核心CPU其实都是用的Advanced PIC,包括两部分:一是集成到CPU内部的Local APIC,主要负责传递中断信号到各自的处理器,每个Local APIC都有一个寄存器、一个内部时钟,一个本地定时设备以及为本地中断保留的两条额外的IRQ线LINT0和LINT1;二是位于南桥的I/O APIC,主要是收集来自CPU外部的中断信号且发送信号到Local APIC。所有Local APIC都连接到I/O APIC,形成一个多级APIC系统。Local APIC除了接收来自I/O APIC的中断信号,还可以接收其他来源的中断,比如接在CPU LINT0和LINT1管脚上的中断、IPI中断(核间中断)、APIC定时器中断、性能监视计数器中断、热传感器中断、APIC内部错误中断等。LINT0、LINT1引脚一般和INTR引脚、NMI引脚共享,禁用Local APIC时作为INTR引脚、NMI引脚使用。
    I/O PIC包括中断控制逻辑线路和中断请求寄存器,CPU可以通过指令对其进行编程,如通过out指令对中断屏蔽字寄存器进行设置、送来中断查询信号。
    中断屏蔽字用来动态的设置处理优先级,不同中断服务程序设置不同的中断屏蔽字。有多少个中断源,中断屏蔽字就有多少位。
    当前的中断服务程序会设置当前中断控制器里的屏蔽字,决定当前中断服务例程能被哪些中断源中断。
    每个能够发出中断请求的外部设备控制器都有一条名为IRQ线的输出线,所有外设的IRQ线都会连接到PIC的一个引脚。
    它们发出的外部中断请求号都有一个编号,如IRQ0、IRQ1、…、IRQi,将与IRQi关联的中断类型号设定为32+i(外部中断的中断向量号均在32~255之间)。
    PIC需对所有外设来的IRQ请求按优先级排队,若至少有一个IRQ线有未被屏蔽的请求,则PIC向CPU的INTR引脚发中断请求。
    CPU会在每条指令最后一个控制信号启动采样查询INTR,若发现中断请求寄存器中记录有不被屏蔽的中断产生且CPU处在开中断状态,CPU采样到的INT信号有效。
    中断类型通过中断优先权编码器获得:CPU发出中断查询信号(INT有效的情况下才会查询),中断查询信号发出后固定时间内读取数据线得到中断类型32+i。随后进入中断响应过程(关中断、保护断点和现场、发中断查询信号),再调出中断服务程序执行。

    IA-32中异常和中断响应过程:

    (1)确定中断类型号 i(硬件发现、中断控制器送CPU),从 IDTR寄存器指向的 IDT中取出第 i 个表项 IDTi。

    (2)根据 IDTi 中段选择符,从 GDTR 指向的 GDT 中取出相应段描述符,得到对应异常或中断处理程序所在段的 DPL、基地址等信息。Linux下中断门和陷阱门对应的即为内核代码段,所以DPL为0,基地址为0。

    (3)若CS寄存器低2位的当前特权级CPL<段描述符中的DPL或编程异常 IDTi 的 DPL<CPL,则发生13号异常。Linux下,前者不会发生。后者用于防止恶意程序模拟 INT n 陷入内核进行破坏性操作。

    (4)若CPL≠DPL,则从用户态换至内核态,以使用内核栈。切换栈的步骤:

        ①读TR 寄存器,以访问正在运行的用户进程的TSS段;

        ②将TSS段中保存的内核栈的段选择符和栈指针分别装入寄存器 SS 和 ESP,然后在内核栈中保存原来用户栈的 SS 和 ESP。

    (5)若是故障,则将发生故障的指令的逻辑地址写入 CS 和 EIP,以使处理后回到故障指令执行。其他情况下,CS 和 EIP 不变,使处理后回到下条指令执行。

    (6)在当前栈中保存 EFLAGS、CS 和 EIP 寄存器的内容(断点和程序状态)。

    (7)若异常产生了一个硬件出错码,则将其保存在内核栈中。

    (8)将IDTi中的段选择符装入CS,IDTi中的偏移地址装入EIP,它们是异常处理程序或中断服务程序第一条指令的逻辑地址(Linux中段基址=0)。

    下个时钟周期开始,从CS:EIP所指处开始执行异常或中断处理程序!

    内核中的TSS段记录了每个进程的状态信息,例如,每个进程对应的页表、 task和mm等结构信息、内核栈的栈顶信息:

    SS:ESP 等

    IA-32中异常和中断返回过程:

    中断或异常处理程序最后一条指令是从内核态返回用户态的IRET。CPU在执行IRET指令过程中完成以下工作:

    (1)从栈中弹出硬件出错码(保存过的话)、EIP、CS和EFLAGS

    (2)检查当前异常或中断处理程序的CPL是否等于CS中最低两位,若是则说明异常或中断响应前、后都处于同一个特权级,此时,IRET指令完成操作;否则,再继续完成下一步工作。

    (3)从内核栈中弹出SS和ESP,以恢复到异常或中断响应前的特权级进程所使用的栈。

    (4)检查DS、ES、FS和GS段寄存器的内容,若其中有某个寄存器的段选择符指向一个段描述符且其DPL小于CPL,则将该段寄存器清0。这是为了防止恶意应用程序(CPL=3)利用内核以前使用过的段寄存器(DPL=0)来访问内核地址空间。

    执行完IRET指令后,CPU回到原来发生异常或中断的进程继续执行

    四、Linux中的异常和中断处理

    Linux利用陷阱门来处理异常,利用中断门来处理中断。

    异常和中断对应处理程序都属于内核代码段,所以,所有中断门和陷阱门的段选择符(0x60)都指向 GDT 中的“内核代码段”描述符。

    通过中断门进入到一个中断服务程序时,CPU 会清除 EFLAGS 寄存器中的 IF 标志,即关中断;通过陷阱门进入一个异常处理程序时,CPU 不会修改 IF 标志。也就是说,外部中断不支持嵌套处理,而内部异常则支持嵌套处理。

    任务门描述符中不包含偏移地址,只包含 TSS 段选择符,这个段选择符指向 GDT 中的一个 TSS 段描述符,CPU 根据 TSS 段中的相关信息装载SS 和 ESP 等寄存器,从而执行相应的异常处理程序。

    Linux中,将类型号为8的双重故障(#DF)用任务门实现,而且是唯一通过任务门实现的异常。

    双重故障 TSS 段描述符在 GDT 中位于索引值为 0x1f 的表项处,即13位索引为0 0000 0001 1111,且其TI=0(指向 GDT),RPL=00(内核级代码),即任务门描述符中的段选择符为00F8H。

    Image(56)

    CPU负责对异常和中断的检测与响应,而操作系统则负责初始化 IDT 以及编制好异常处理程序或中断服务程序。Linux运用提供的三种门描述符格式,构造了以下5种类型的门描述符:

    • 中断门:

    DPL=0,TYPE=1110B。激活所有中断

    • 系统门:

    DPL=3,TYPE=1111B。激活4、5和128三个陷阱异常,分别对应指令into、bound和int $0x80三条指令。因DPL为3,CPL≤DPL ,故在用户态下可使用这三条指令

    • 系统中断门:

    DPL=3,TYPE=1110B。激活3号中断(即调试断点),对应指令int 3。因DPL为3,CPL≤DPL,故用户态下可使用int 3指令。

    • 陷阱门:

    DPL=0,TYPE=1111B。激活所有内部异常,并阻止用户程序使用INT n(n≠128或3)指令模拟非法异常来陷入内核态运行。

    • 任务门:

    DPL=0,TYPE=0101B。激活8号中断(双重故障)。

    Linux内核在启用异常和中断机制之前,先设置好 IDT 的每个表项,并把IDT 首址存入 IDTR。系统初始化时,Linux完成对 GDT、GDTR、IDT和 IDTR 等的设置,以后一旦发生异常或中断,CPU就可通过异常和中断响应机制调出异常或中断处理程序执行。

    Linux对异常的处理

    异常处理程序发送相应的信号给发生异常的当前进程,或者进行故障恢复,然后返回到断点处执行。

    例如,若执行了非法操作,CPU就产生6号异常(#UD),在对应的异常处理程序中,向当前进程发送一个SIGILL信号,以通知当前进程中止运行。

    采用向发生异常的进程发送信号的机制实现异常处理,可尽快完成在内核态的异常处理过程,因为异常处理过程越长,嵌套执行异常的可能性越大,而异常嵌套执行会付出较大的代价。

    并不是所有异常处理都只是发送一个信号到发生异常的进程。

    例如,对于14号页故障异常(#PF),需要判断是否访问越级、越权或越界等,若发生了这些无法恢复的故障,则页故障处理程序发送SIGSEGV信号给发生页故障异常的进程;若只是缺页,则页故障处理程序负责把所缺失页面从磁盘装入主存,然后返回到发生缺页故障的指令继续执行。

    所有异常处理程序的结构是一致的,都可划分成以下三个部分:

    (1)准备阶段:在内核栈保存通用寄存器内容(称为现场信息),这部分大多用汇编语言程序实现。

    (2)处理阶段:采用C函数进行具体处理。函数名由do_前缀和处理程序名组成,如 do_overflow 为溢出处理函数。

    大部分函数的处理方式:保存硬件出错码(如果有的话)和异常类型号,然后,向当前进程发送一个信号。

    当前进程接受到信号后,若有对应信号处理程序,则转信号处理程序执行;若没有,则调用内核abort例程执行,以终止当前进程。

    (3)恢复阶段:恢复保存在内核栈中的各个寄存器的内容,切换到用户态并返回到当前进程的断点处继续执行。

    Linux中异常对应的信号名和处理程序名如图所示:

    Image(58)

    异常处理在内核态信号处理在用户态

    举例:

    Image(59)

    加上注释后,会出现如图结果,出现除法出错。如果是浮点数除法错,异常处理程序可以将结果用特殊值表示,而整数除法错则向当前进程发送一个SIGFPE信号,进程接受到信号后没有对应信号处理程序,调用内核abort例程执行,以终止当前进程。运行结果为“Floating point exception”。

    Image(60)

    去掉注释后会转信号处理程序执行,执行后可以继续运行完成。

    Image(61)

    Linux对中断的处理:

    Linux中将处理的中断有分为三种类型:I/O中断:由I/O外设发出的中断请求;时钟中断:由某个时钟产生的中断请求,告知一个固定的时间间隔到;处理器中断:多处理器系统中其他处理器发出的中断请求。
    ①准备阶段:在内核栈中保存各通用寄存器的内容(称为现场信息)以及所请求IRQi的值等,并给PIC回送应答信息,允许其发送新的中断请求信号。
    ② 处理阶段:执行IRQi对应的中断类型号为32+i的中断服务例程ISR(InterruptServer Routine)。
    ③ 恢复阶段:恢复保存在内核栈中的各个寄存器的内容,切换到用户态并返回到当前进程的逻辑控制流的断点处继续执行。
     
    五、总结:
    典型的多重中断处理(中断服务程序)分为三个阶段: 
    • 准备阶段(先行阶段)
    此前中断响应,硬件关中断使EFLAGS中的IF=0
    在内核栈中保存现场信息(各通用寄存器的内容)、旧屏蔽字、
    通过异常/中断处理程序查明原因(软件中断时),IA-32无此步骤
    设置新屏蔽字
    通过STI指令开中断(执行中断服务例程过程中如果有新请求会进入中断的嵌套,不过Linux通常会屏蔽外部中断)
    • 处理阶段(具体的中断处理阶段)
    执行IRQi对应的中断类型号为32+i的中断服务例程
    • 恢复阶段
    通过CTI指令关中断 
    恢复现场及旧屏蔽字
    清“中断请求” 
    开中断
    中断返回
  • 相关阅读:
    Centos7安装Zabbix4.0步骤
    linux异常处理:selinux配置错误导致无法重启
    自学Python5.4-类 _init_方法
    自学Python5.3-类和对象的简单操作
    自学Linux Shell9.3-基于Red Hat系统工具包:RPM属性依赖的解决方式-YUM在线升级
    自学Linux Shell9.4-基于Red Hat系统工具包存在两种方式之二:源码包
    自学Linux Shell9.2-基于Red Hat系统工具包存在两种方式之一:RPM包
    JS 对象API之修改、删除对象的属性
    JS 对象API之判断自有属性、共有属性
    JS 对象API之判断父对象是否在子对象的原型链上
  • 原文地址:https://www.cnblogs.com/yangyuliufeng/p/9214381.html
Copyright © 2011-2022 走看看