精华内容
下载资源
问答
  • 我们知道,中断在发生时,处理器根据收到的中断向量号在中断描述符表中找到相应的中断门描述符。 处理器从该描述符中加载目标代码段选择子到代码段寄存器 cs 及偏移量到指令指针寄存器 EIP。 注意,由于 cs 加载了新...

    我们知道,中断在发生时,处理器根据收到的中断向量号在中断描述符表中找到相应的中断门描述符。

    处理器从该描述符中加载目标代码段选择子到代码段寄存器 cs 及偏移量到指令指针寄存器 EIP。

    在这里插入图片描述
    注意,由于 cs 加载了新的目标代码段选择子,处理器只要发现段寄存器被加载,段描述符缓冲寄存器就会被刷新,因为处理器认为是换了一个段,属于段间转移,也就是远转移。

    所以,当前进程被中断打断后,为了从中断返回后能继续运行该进程,处理器自动把 cs 和 EIP 的当前值保存到中断处理程序使用的栈中。

    除了要保存 cs、 EIP 外,还需要保存标志寄存器 EFLAGS,

    因为中断是可以在任何特权级别下发生的,不同特权级别下处理器使用不同的栈,如果涉及到特权级变化,还要压入 SS 和 ESP 寄存器。

    那么我们就来看看到底中断的时候是如何来压栈的吧~

    1。当中断发生时,低特权级向高特权级转化时的压栈现象
    在这里插入图片描述
    当处理器根据中断向量号找到中断描述符后,根据中断描述符中的段描述符选择子+GDTR的值,我们可以找到目标段描述符。

    我们是否能访问这个目标段描述符,要做的就是将找到中断描述符时当前的CPL与目标段描述符的DPL进行对比。

    这里我们讨论的是CPL特权级比DPL低的情况,即数值上CPL > DPL.
    这表示我们要往高特权级栈上转移,也意味着我们最后需要恢复旧栈,所以

    (1) 处理器先临时保存一下旧栈的SS和ESP(SS是堆栈段寄存器,因为换了一个栈,所以其也要变,ESP相当于在栈上的索引),然后加载新的特权级和DPL相同的段,将其加载到SS和ESP中,然后将之前保存的旧栈的SS和ESP压到新栈中
    在这里插入图片描述

    由于SS堆段寄存器是16位寄存器,所以为了对齐,将其0拓展后压栈。
    然后在新栈中压入EFLAGS标志寄存器,得到图如下:
    在这里插入图片描述

    2.因为是要切换栈,属于段间转移,所以我们还要将旧栈SS和EIP备份,以便中断程序执行后还可以恢复到被中断的进程。因为CS代码段寄存器也是16位寄存器,所以也要在压入栈前进行0扩展,此时如下图
    在这里插入图片描述

    3.某些异常会有错误码,此错误码用于报告异常是在哪个段上发生的,也就是异常发生的位置,所以错误码中包含选择子等信息。错误码会紧跟在 EIP 之后入栈,记作 ERROR CODE
    在这里插入图片描述

    2。当中断发生时,无特权级转化时的压栈现象
    此时由于不会切换栈,就不用保存SS和ESP,但是由于在处理完中断程序后还是要返回源程序中继续执行的,所以,我们的CS EIP寄存器还是要保存的。

    在这里插入图片描述
    这种中断返 回是用 iret 指令实现的。 Iret,即 interrupt ret。注意在返回的时候,其错误码不会自动跳过,所以需要我们手动跳过。

    ????????????????????????

    好了,我们的压栈过程就到此讲完了,下面我们来了解一下,中断程序返回即出栈的过程,假设此时ESP已经跳过错误码指向eip_old

    当处理器执行到 iret指令时,它首先需要从栈中返回CS_old 及EIP_old,由于处理器在中断返回的时候,并不记得自己来的时候也曾经做过特权级检查,所以这时候它还要再进行特权级检查。

    特权级检查如下:
    (1)将cs_old对应的代码段的DPL及cs_old中的RPL做特权级检查, 如果检查通过,随即需要更新寄存器cs和EIP

    由于cs_old在入栈时已经将高16位扩充为 0,现在是 32 位数据,段寄存器 cs 是 16 位,故处理器丢弃 cs_old 高 16 位,将低16 位加载到 cs.
    将 EIP_old加载到 EIP 寄存器,之后栈指针指向 EFLAGS。

    如果进入中断时未涉及特权级转移,此时栈指针是 ESP_old (说明在之前进入中断后,是继续使用旧栈)。否则栈指针是 ESP_new (说明在之前进入中断后用的是 TSS 中记录的新栈)。

    (2)将栈中保存的EFLAGS 弹出到标志寄存器 EFLAGS。如果在第 1 步中判断返回后要改变特权级, 此时栈指针是 ESP_new,它指向栈中的 ESP_old。否则进入中断时属于平级转移,用的是旧栈,此时栈指针是 ESP_old,栈中已无因此次中断发生而入栈的数据,栈指针指向中断发生前的栈顶。

    (3)如果在第 1 步中判断出返回时需要改变特权级,也就是说需要恢复旧栈,此时便需要将 ESP_old 和 SS_old 分别加载到寄存器 ESP 及 SS,丢弃寄存器 SS 和 ESP 中原有的 SS_new 和 ESP_new,同时进行特权级检查。

    由于 SS_old 在入栈时已经由处理器将高 16 位填充为 0,现在是 32 位数据,所以在 重新加载到栈段寄存器 SS 之前,需要将 SS_old 高 16 位剥离丢弃,只用其低 16 位加载 SS。

    至此,处理器回到了被中断的那个进程。

    参考书籍:《操作系统真相还原》第七章7.4.2

    作者:五月的天气
    来源:CSDN
    原文:https://blog.csdn.net/qq_37414405/article/details/84986968
    版权声明:本文为博主原创文章,转载请附上博文链接!

    展开全文
  • 中断系统

    中断:

    由于CPU获知了计算机中发生的某些事,CPU暂停正在执行的程序,转而去执行处理该事件的程序,当这段程序执行完毕后,CPU继续执行刚才的程序,整个过程称为中断处理,也称为中断。没有中断,操作系统几乎什么都做不了,操作系统就是中断驱动的。首先,操作系统就是个死循环,这个死循环什么都做不了,仅仅是保持操作系统能够周而复始的运行下去,运行的目的就是为了等待中断发生。
    中断按事件来源分类,来自CPU外部的中断就称为外部中断,来自CPU内部的中断称为内部中断。

    外部中断:

    CPU提供了两条信号线。外部硬件的中断是通过两根信号线通知CPU的,这两根信号是INTR(可屏蔽中断)和NMI(不可屏蔽中断)
    可屏蔽中断可以1,通过lags寄存器的IF位将所有这些外部设备的中断屏蔽。2,通过中断代理8259A来屏蔽单个设备的中断。

    内部中断:

    内部中断分为软中断和异常。
    软中断:就是软件主动发起的中断,具有主观性,它来自于软件运行中指令int 8位立即数(系统调用)、 int3等
    异常:指令执行期间CPU内部产生的错误引起的(除以0等)。不受eflags的IF影响,无法向用户隐瞒
    总结:外部中断的NMI和内部中断都可以无视IF位。

    中断向量号:

    中断的本质就是来一个中断信号后,调用相应的中断处理函数。范围0-255。为每一个中断信号分配一个中断号,然后用此中断号作为中断描述符表的索引,找到对应的表项,进而在表项找到对应的地址,从而转去相应的处理程序。
    异常和不可屏蔽中断的中断向量号是由CPU自动提供的,来自外部设备(时钟、键盘、硬盘)的可屏蔽中断号是由中断代理设置的,软中断是由软件提供的(int 8位立即数)。

    中断描述符表

    中断描述符表是保护模式下用于储存中断处理程序入口地址的表。中断描述符表中的中断描述符称为---门。Linux系统只用了中断门

    中断门包含了中断处理程序所在段的段选择子和段内偏移地址。当通过此方式进入中断后,eflags中的 IF 位自动置0,即进入中断后,自动把中断关闭,避免嵌套。Linux就是利用中断门实现了系统调用:int 0x80。中断门只能在IDT中。

    16位的表界限可以容纳中断描述符的个数为:64KB/8=8192,但是处理器只支持0-255。

    中断处理过程及保护

    完整的中断过程分为CPU外和CPU内两部分:
    CPU外:外部设备的中断由中断代理芯片接受,处理后将该中断向量号发送到CPU
    CPU内:CPU执行该中断向量号对应的中断处理程序

    处理器内部的过程:

    1,处理器根据中断向量号定位中断门描述符
    处理器用中断向量号乘以8后与IDTR中的中断描述符表地址相加,所求的地址之和便是该中断向量号对应的中断描述符的线性地址,然后经过页部件翻译得到真实地址。
    2,处理器进行特权级检查
    若中断由软中断int n,int3(系统调用等)引起,则要进行当前的CPL和门槛DPL_Gate、门框:目标代码段DPL的检查。
    若中断是外部设备和异常引起的,只检查当前CPL和目标代码段的DPL,数值上CPL > 目标代码段DPL。
    3,执行中断处理程序
    特权级检查后,将门描述符目标代码段选择子加载到代码段寄存器CS中,把门描述符中中断处理程序的偏移地址加载到EIP,开始执行中断处理程序。

    中断发生后eflags中的 NT 位和 TF 位会被置 0 。用的是中断门的话eflags 的 IF 位会被自动置0,防止中断嵌套,即处理器会将这个中断完全执行完毕后才能出来下一个中断。从中断返回的指令是 iret ,它从栈中弹出数据到寄存器的 cs、eip eflags 等,根据特权级是否改变,判断是否恢复旧栈,即将SS_old 和 ESP_old 位置的值弹出到寄存器 ss和 esp。

    中断发生时的压栈

    特权级发生转移时:用户进程正在执行时候发生了中断

    1,当前进程被中断打断后,处理器根据中断向量号找到对应的中断描述符,拿CPL和中断门描述符中选择子对应的目标代码段的DPL对比,若CPL权限比DPL低,即数值上CPL >DPL,表示要想高特权级转移,需要切换高特权级的栈。即:从用户进程中断去执行内核中断程序。中断执行完返回时也要回到之前的旧栈,所以处理器临时将SS_old 和 ESP_old 保存到一个内存中,然后CPU取出TSS中目标代码段的DPL 级别的栈加载到寄存器 SS 和 ESP 中,记作 SS_new 和 ESP_new,CPU在找到之前的旧栈,将其压入新栈中:将 SS_old 用0 扩展高16位,成为32位数据。

    2,在新栈中压入EFLAGS寄存器

    3,将要CS_old 和 EIP_old 压入新栈:CS_old 需要用0 来填充高16位为0。

    4,某些异常有错误码,错误码用于报告哪个段上发生了异常,所以ERROR_CODE也压入栈。

    特权级未发生转移时发生的中断:正在执行内核代码发生了中断

    1,在新栈中压入EFLAGS寄存器

    2,将要CS_old 和 EIP_old 压入新栈:CS_old 需要用0 来填充高16位为0。

    3,某些异常有错误码,错误码用于报告哪个段上发生了异常,所以ERROR_CODE也压入栈。

    中断返回的出栈:

    用 iret 指令实现,iret 指令并不知道栈内容的正确性,一次弹出4字节,所以在使用iret 之前,一定要调整好esp的位置,使其依次弹出:EIP、CS、EFLAGS、ESP、SS。因此我们要手动跳过 ERROR_CODE。

    1,当处理器执行到 iret 时,需要从栈中返回。CPU首先检查CS_old 的选择子的 RPL 位,判断在返回过程中是否要改变特权级。弹出 EIP_old 和 CS_old 。

    2,弹出 EFLAGS。若需要改变特权级,则弹出旧栈。若不需要改变特权级,则是平级转移。

    如果需要改变特权级,CPU还会检查数据段寄存器DS、ES、FS、GS的内容,如果它们之中某个寄存器的选择子所指向的数据段描述符的DPL权限比返回后的CPL高,则处理器将该段寄存器置为 0 。

    可编程中断控制器8259A

    8259A的作用是负责所有来自外设的中断,如时钟的中断、键盘的中断等等。8259A它用于管理和控制可屏蔽中断,表现在可以屏蔽想要屏蔽的外设中断,对外设中断实行优先级判决,向CPU提供中断向量号等功能。每个独立运行的外部设备就是一个中断源,可以发中断信号给8259A。
    个人电脑中只有两片 8259A 芯片,共16 个 IRQ 接口。
    8259A芯片的内部结构:


    8259A的编程

    8259A内部有两组寄存器,一组是初始化命令寄存器,用来保存初始化命令字,ICW1-ICW4。另一组寄存器是操作命令寄存器组,用来保存操作命令字,OCW1-OCW3。我们对8259A的编程,也分为初始化和操作两部分。

    1,用ICW作初始化:确定是否需要级联、设置起始的中断向量号、设置中断结束模式等等

    ICW1用来初始化8259A的连接方式:单片还是级联       中断信号触发方式:电平触发还是边沿触发

    ICW2用来设置起始中断向量号。指定了 IRQ0 映射到的中断向量号,其他 IRQ 接口对应的中断向量号会顺着自动排列下去。

    ICW3用来设置级联方式下主片和从片用哪个IRQ接口互连。

    ICW4用来设置中断结束模式:自动还是手动。

    2,用OCW来操作控制:中断屏蔽、中断结束

    OCW1用来屏蔽连接在8259A上的外部设备中的中断信号。这些没有屏蔽的中断最终还是要受到eflags的IF位管束,IF=0,不管8259A怎么样设置全部屏蔽。

    OCW2用来设置中断结束方式:发EOI来终止某一个中断      优先级模式:循环还是固定

    OCW3用来设置特殊屏蔽方式和查询方式。

    ICW1和OCW2、OCW3是用偶地端口 0x20(主片)或 0xA0(从片)写入。

    ICW2-ICW4是用奇地址端口 0x21(主片) 或0xA1(从片)写入。

    4个ICW要保证一定的次序写入,8259A就知道写入端口的数据是什么意思了。

    OCW的写入顺序无关,各控制字有唯一标识可以辨别。

    所以8259A 的编程步骤为:

    1,首先必须进行初始化,必须依次写入 ICW1、ICW2 、ICW3 、ICW4

    2,初始化后,可以进行OCW的操作。

    中断处理程序

    Intel的8259A芯片位于主板的南桥芯片中。主片IR0引脚上就是时钟中断,这已经由内部电路实现了。我们只打开时钟中断,屏蔽其他外部设备的中断。中断向量号的前32个(0-31)都被系统的内部中断给占用了,所以我们的时钟中断向量号只能从0x20开始。我们设置0x20为时钟中断的向量号。所以我们要在中断描述符中的第33个中断描述符中加载时钟中断处理程序的选择子和偏移地址。因此我们要定义33个描述符的中断描述符表。由于我们可以屏蔽的中断信号只能是外部设备的可屏蔽中断信号。所以当处理器内部发生内部中断时也会产生0-31的向量号,到时候也会到中断描述符中去找中断处理程序的地址。这些程序都需要我们来写。所以我们写的程序就有中断向量号为0-31这32个内部中断的处理程序和中断向量号为32的时钟中断处理程序。

    宏属于预处理指令。预处理指令被编译器的预处理器支持,属于伪指令。这类指令是在编译前,编译器需要预先处理的指令,预处理器会将这些伪指令展开替换成具体的编译器可以识别的语言符号,在预处理后,其中的预处理指令(伪指令)全部都会不见的。比如#include<>、#define等都是预处理指令,都会被预处理器在预处理阶段都替换成具体所代表的代码或数字。

    宏:Macro。用来代替重复性输入,是一段代码的模板。

    定义单行的宏,汇编中可以用%define 指令实现。

    定义多行的宏,汇编中可以用%macro来实现。

    %macro 宏名字+参数个数
    宏代码体 		;要引用某个参数,需要用“% 数字” 的方式来引用。参数序号从 1 开始
    %endmacro

    总结我们的中断建立的步骤为:

    1,编写中断处理程序,记录每个中断程序的地址

    2,构造出中断描述符表,将中断程序的地址加载进去

    3,设置完成8259A

    4,加载IDTR

    5,将eflags的IF位置1

    我们采取汇编语言和C语言相结合的方式来编写中断处理程序。因为C语言编写程序更方便,我们用C语言来写中断处理程序。发生中断后,首先执行汇编语言写的中断处理程序,然后在调用C语言写的中断处理程序执行。即汇编中的中断程序只是提供了中断处理程序的入口,进入后在转去执行C语言的中断处理程序,C语言的程序才是真正的处理程序。

    在内部中断的前32个中,有些中断类型有错误码,系统就自动压入了错误码,有些中断类型没有错误码,系统没有错误码,为了保持一致性,所以我们要统一压入错误码。

    中断程序的汇编中断处理代码:只是提供一个入口功能,然后转去C中断处理程序。我们要编写33个汇编中断处理程序:0-32是内部中断的处理程序,33是时钟的中断程序。

    [bits 32]
    %define ERROR_CODE nop		 ; 若在相关的异常中cpu已经自动压入了错误码,为保持栈中格式统一,nop是不做操作.
    %define ZERO push 0		 ; 若在相关的异常中cpu没有压入错误码,就手工压入一个0
    
    extern idt_table		 ;idt_table是C中注册的中断处理程序数组,数组元素是C语言的中断处理程序的地址。
    
    section .data
    global intr_entry_table		;定义intr_entry_table为全局变量数组名,每个数组元素为程序的地址
    intr_entry_table:		;因为这个标号是在 .data段内的,所以这个标号指的是 .data里面的地址
    
    %macro VECTOR 2
    section .text
    intr%1entry:		 ; 每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少
    
       %2				 ; 中断若有错误码会压在eip后面 
    ; 以下是保存上下文环境
       push ds
       push es
       push fs
       push gs
       pushad			 ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI
    
       ; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI 
       mov al,0x20                   ; 中断结束命令EOI
       out 0xa0,al                   ; 向从片发送
       out 0x20,al                   ; 向主片发送
    
       push %1			 ; 不管idt_table中的目标程序是否需要参数,都一律压入中断向量号,调试时很方便
       call [idt_table + %1*4]       ; 调用idt_table中的C版本中断处理函数
       jmp intr_exit
    
    section .data
       dd    intr%1entry	 ; 存储各个中断入口程序的地址,形成intr_entry_table数组
    %endmacro
    
    section .text
    global intr_exit
    intr_exit:	     
    ; 以下是恢复上下文环境
       add esp, 4			   ; 跳过中断号
       popad
       pop gs
       pop fs
       pop es
       pop ds
       add esp, 4			   ; 跳过error_code
       iretd
    ;0-31个中断向量号是内部中断,有些有错误码,有些没有。32中断向量号为时钟中断
    VECTOR 0x00,ZERO
    VECTOR 0x01,ZERO
    VECTOR 0x02,ZERO
    VECTOR 0x03,ZERO 
    VECTOR 0x04,ZERO
    VECTOR 0x05,ZERO
    VECTOR 0x06,ZERO
    VECTOR 0x07,ZERO 
    VECTOR 0x08,ERROR_CODE
    VECTOR 0x09,ZERO
    VECTOR 0x0a,ERROR_CODE
    VECTOR 0x0b,ERROR_CODE 
    VECTOR 0x0c,ZERO
    VECTOR 0x0d,ERROR_CODE
    VECTOR 0x0e,ERROR_CODE
    VECTOR 0x0f,ZERO 
    VECTOR 0x10,ZERO
    VECTOR 0x11,ERROR_CODE
    VECTOR 0x12,ZERO
    VECTOR 0x13,ZERO 
    VECTOR 0x14,ZERO
    VECTOR 0x15,ZERO
    VECTOR 0x16,ZERO
    VECTOR 0x17,ZERO 
    VECTOR 0x18,ERROR_CODE
    VECTOR 0x19,ZERO
    VECTOR 0x1a,ERROR_CODE
    VECTOR 0x1b,ERROR_CODE 
    VECTOR 0x1c,ZERO
    VECTOR 0x1d,ERROR_CODE
    VECTOR 0x1e,ERROR_CODE
    VECTOR 0x1f,ZERO 
    VECTOR 0x20,ZERO
    

    编译器编译程序时会按照每个节的属性,将相同属性的节放在一起的。所以我们编译器会分别将代码段和数据段放在一起的。因为预处理器会在编译前提前将代码展开,所以最后 .data放在一起,可以构成一个数组,数组元素是代码段的地址。 idt_table的数组元素是每一个C处理程序的地址。所以 call  [idt_table+4*num]==call 处理函数地址。

    eflags、cs、eip、error_code是CPU自己压入栈的,我们不需要管,我们要负责的是其他入栈参数。注意 iret 时候要跳过error_code 使esp 指向eip。


    中断的C语言中断处理程序:里面是真正的中断处理程序。这里我们只设置一个通用的,33个中断都是这一个中断函数。将中断函数的地址循环33编放入数组中即可。

    intr_handler idt_table[IDT_DESC_CNT];  //定义C语言中断处理程序数组
    /* 通用的中断处理函数,一般用在异常出现时的处理 */
    //功能是:出现异常时显示中断向量号;
    static void general_intr_handler(uint8_t vec_nr) 
    {
    if (vec_nr == 0x27 || vec_nr == 0x2f) {	// 0x2f是从片8259A上的最后一个irq引脚,保留
    return;		//IRQ7和IRQ15会产生伪中断(spurious interrupt),无须处理。
    }
    put_str("int vector: 0x");
    put_int(vec_nr);
    put_char('\n');
    }
    static void exception_init(void)
    {       // 完成一般中断处理函数注册注册
    int i;
    for (i = 0; i < IDT_DESC_CNT; i++)
    {
    idt_table[i] = general_intr_handler;      // 默认为general_intr_handler。
    }
    }
    

    中断描述符表IDT的建立

    中断门描述符结构体,要按照中断门描述符中的顺序来,低地址的先定义,则先储存在内存中

    struct gate_desc
    {
       uint16_t    func_offset_low_word; //要先定义,则会先储存在内存中
       uint16_t    selector;
       uint8_t     dcount;   //此项为双字计数字段,是门描述符中的第4字节。此项固定值,不用考虑
       uint8_t     attribute;
       uint16_t    func_offset_high_word;  
    };
    
    //定义中断描述符表,有33个门描述符,0-31中断向量号被系统占用,我们从32(0x20)开始定义时钟中断
    static struct gate_desc idt[IDT_DESC_CNT];
    
    //声明出中断程序入口地址数组,有33个地址,0-31的地址属于内部中断程序地址,0x20的地址属于时钟中断程序地址
    //这些地址在汇编的中断程序中已经得出
    extern intr_handler intr_entry_table[IDT_DESC_CNT];  //汇编中断处理地址的中的全局数组,元素是每个中断程序的入口地址
    
    //声明中断描述符构造函数
    static void make_idt_desc(struct gate_desc* p_gdecs,uint8_t attr,intr_handler function);
    //门描述符构造函数
    static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) 
    { 
       p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000FFFF;
       p_gdesc->selector = SELECTOR_K_CODE;
       p_gdesc->dcount = 0;
       p_gdesc->attribute = attr;
       p_gdesc->func_offset_high_word = ((uint32_t)function & 0xFFFF0000) >> 16;
    }
    
    //构造出33个门描述符
    static void idt_desc_init(void) 
    {
       int i;
       for (i = 0; i < IDT_DESC_CNT; i++) 
    	{
          make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]); 
       }
       put_str("   idt_desc_init done\n");
    }
    

    8259A设置程序

    设置头文件io.h。通过这个头文件来读写端口。

    static函数的作用:普通的函数名是全局变量,在一个文件中定义后,在所有的文件中都可以使用。static函数,在一个文件中定义后,只能在那个文件中使用,其他文件不能用。

    inline函数的作用:当主调函数调用被调函数时候,主调函数要压栈,call,然后csip转过去执行等等,被调函数也要备份等等,速度很慢。inline函数是当主调函数调用被调函数时,直接在调用出原封不动的将被调函数的函数体展开,这样就不用call了,这就不属于函数调用了,csip的值也不用变,依次执行即可。

    我们将static函数放在头文件中的目的在于:因为这是对端口进行操作,所以需要的条件就是一定要快。static函数放在头文件中,那么所有包含该头文件的文件都有这个static函数,static函数执行起来比普通函数要快很多,因为静态函数会被自动分配在一个一直使用的静态存储区,直到退出应用程序才清空这个区域,而普通的函数还生成了一些其他信息来保证安全,所以static函数速度快很多。

    内存循环复制三剑客:cld、(ES:ESIES:EDI)、repmovs[dw]

    端口循环读写三剑客:cld、(es:edi)、rep ins[dw]      cld、(es:esi)、rep outs[dw]

    两个指令都是在执行 rep前先读取ecx的值与0比较,循环执行后ecx的值减1,然后esiedi自动加上相应的字节。

    1,首先我们编写端口读写函数,不管什么端口读写,调用即可。

    #ifndef _LIB_IO_H
    #define _LIB_IO_H
    //向端口port写入一个字节.N为立即数约束:表示0-255个端口号
    static inline void outb(unit16_t port, unit_t data)
    {
    	asm volatile("outb %b0,&w1"::"a"(data),"Nd"(port));//b和w来限制寄存器大小
    }
    
    //将addr处起始的 word_cnt 个字写入端口port。
    //执行rep指令前先拿ecx与0比较,不相等则可以执行,执行完后edi加一个字,ecx减1。在执行rep之前再比较
    static inline void outsw(uint16_t port,const void* addr,unit32_t word_cnt)
    {
    	asm volatile("cld;rep outsw": "+S"(addr),"+c"(word_cnt): "d"(port));
    }
    
    //将从端口port读入一个字节返回
    static inline uint8_t inb(uint16_t port)
    {
    	uint8_t data;
    	asm volatile("inb %w1,%b0":"=a"(data):"Nd"(port));
    	return data;
    }
    
    //将从端口port读入的word_cnt个字写入addr
    //
    static inline void insw(uint16_t port,void* addr,uint32_t word_cnt)
    {
    	asm volatile("cld;rep insw":"+D"(addr),"+c"(word_cnt):"d"(port):"memory");
    }
    #endif
    

    2,当我们设置8259A时,我们调用端口读写函数即可

    /* 初始化可编程中断控制器8259A */
    static void pic_init(void) {
    
       /* 初始化主片 */
       outb (PIC_M_CTRL, 0x11);   // ICW1: 边沿触发,级联8259, 需要ICW4.
       outb (PIC_M_DATA, 0x20);   // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
       outb (PIC_M_DATA, 0x04);   // ICW3: IR2接从片. 
       outb (PIC_M_DATA, 0x01);   // ICW4: 8086模式, 正常EOI
    
       /* 初始化从片 */
       outb (PIC_S_CTRL, 0x11);	// ICW1: 边沿触发,级联8259, 需要ICW4.
       outb (PIC_S_DATA, 0x28);	// ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
       outb (PIC_S_DATA, 0x02);	// ICW3: 设置从片连接到主片的IR2引脚
       outb (PIC_S_DATA, 0x01);	// ICW4: 8086模式, 正常EOI
    
       /* 只打开主片上IR0,也就是目前只接受时钟产生的中断 */
       outb (PIC_M_DATA, 0xfe);
       outb (PIC_S_DATA, 0xff);
    
       put_str("pic_init done\n");
    }


    加载IDTR

    根据IDTR的结构,采用内联汇编的形式加载进去。

    数组名便是指针,所以,

     uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16));//将idt的基地址左移16位,
     asm volatile("lidt %0" : : "m" (idt_operand));
    

    将eflags的IF位置1

    asm volatile("sti");

    到此为止,时钟中断设置完成,在主函数中依次初始化idt、pic(8259A)、idtr,然后开中断即可。


    可编程计数器/定时器8253

    计算机时钟分为两类:内部时钟和外部时钟

    内部时钟:是内部元件,位于主板上,由晶体振荡器产生,其频率经过分频之后就是主板的外频,外频乘以某个倍数就是主频。内部定时无法改变。

    外部时钟:是处理器与外部设备之间通信采用的一种时序

    内部时钟和外部时钟是两套独立运行的定时体系。对于外部定时,我们有两种实现方式:一种软件实现,一种硬件实现为定时器。

    计时器的功能就是定时发出信号,当到达所计数的时间,计数器可以发出一个中断信号,处理器转去执行相应的中断处理程序。计数器内部有计算脉冲信号,每一个时钟脉冲信号到来时都会修改计数值。

    8253用的是 倒计时 的方式。比如为了放在DRAM(内存卡)数据丢失,每隔一段时间就要进行充电刷新,就是利用这个向CPU发出中断去刷新的。我们设置8253来调整时钟中断发生的频率。

     每个计数器有3个引脚

    CLK:时钟输入信号,即计数器自己的时钟频率,连接到8253的CLK的脉冲频率为 2MHZ。

    GATE:门控输入信号

    OUT:计数器输出信号,当计数值为0时,用于通知处理器或者某个设备:定时完成

    计数开始之前的计数初值保存在计数初值寄存器中,减法计数器将此值载入后,CLK 收到一个脉冲信号,减法计数器就将计数值减1,同时将当前值保存到输出锁存器中。当计数值为0时,OUT引脚输出中断信号或者启动某个设备工作。

    因此我们使用计数器0来产生时钟信号,这个时钟是连接饿到 IRQ0 引脚上的那个时钟,即计数器0决定了定时中断信号的发生频率。因此计数器0的计时到期后就会发出中断时钟中断信号,中断代理8259A就会感知引脚 IRQ0 有中断信号到来。

    我们通过控制字寄存器来设置计数器0的工作方式。我们通过控制字寄存器选择用哪个计数器,指定该计数器的控制模式,再用该计数器写入计数初值就行。

    默认情况下,三个计数器的工作频率位 1.19318MHz,即一秒内有 1193180 次脉冲信号,一秒内会减1193189次1,计数器0的默认值是0,即65536,那么一秒内的输出信号次数大约为 : 1193180/65536=18.206,所以时钟中断信号的频率为18.206Hz,大约55毫秒发生一次中断。这就是我们默认情况下的时钟中断频率了。

    我们的目的是为了使中断发生的快一点。默认频率为18.206Hz,我们调整为100Hz,即一秒钟100次中断。我们通过8253,通过控制字指定使用计数器0,工作方式选择方式2(比率发生器),并为附上合适的初值 1193180/100=11932。

    static void frequency_set(uint8_t counter_port,  uint8_t counter_no, uint8_t rwl, uint8_t counter_mode,  uint16_t counter_value) 
    {
       /* 往控制字寄存器端口0x43中写入控制字 */
       outb(PIT_CONTROL_PORT, (uint8_t)(counter_no << 6 | rwl << 4 | counter_mode << 1));
       /* 先写入counter_value的低8位 */
       outb(counter_port, (uint8_t)counter_value);
       /* 再写入counter_value的高8位 */
       outb(counter_port, (uint8_t)counter_value >> 8);
    }

    获取当前中断状态并开关中断

    可以定义枚举来表示中断状态

    enum intr_status
    {
       INTR_OFF;
       INTR_ON;
    }

    获取当前eflags的中断状态状态

    我们定义宏函数,首先先看一下宏函数:

    宏函数实现:

    #define MAX( a, b) ( (a) > (b) (a) : (b) )
    

    普通函数来实现:

    int max( int a, int b)
    {
    return (a > b? a : b);
    }
    

    很显然,我们不会选择用函数来完成这个任务,原因有两个:

    1,普通的函数会带来额外的开销:将实参压栈、压入地址、弹出地址、释放堆栈等等。这种开销不仅会降低代码效率,而且代码量也会大大增加,而使用宏定义则在代码调用处直接展开,省去了很多工作;

    2,普通函数的参数必须被声明为一种特定的类型,所以它只能在类型合适的表达式上使用,我们若要改变类型,只能另外写一个函数。反之,宏定义可以用于整形、长整形、单浮点型、双浮点型以及其他任何可以用“>”操作符比较值大小的类型,也就是说,宏是与类型无关的。

    不过和使用函数相比,使用宏的不利之处在于每次使用宏时,一份宏定义代码的拷贝都会插入到程序中。除非宏非常短,否则使用宏会大幅度增加程序的长度。

    //返回eflags寄存器到变量中
    #define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl;popl %0":"=g"(EFLAG_VAR))
    //获取当前状态
    enum intr_status intr_get_status()
    {
     int eflags=0;
     GET_EFLAGS(eflags);
     return (EFLAGS_IF & eflags) ? INTR_ON:INTR_OFF;
    }
    //开中断并返回开中断前的状态
    enum intr_status intr_enable()
    {
    	enum intr_status old_status=INTR_ON;
    	if(old_status==intr_get_status())
    		return old_status;
    	old_status=INTR_OFF;
    	asm volatile("sti");
    	return old_status;
    }
    
    //关闭中断并返回中断前的状态
    enum intr_status intr_enable()
    {
    	enum intr_status old_status=INTR_OFF;
    	if(old_status==intr_get_status())
    		return old_status;
    	old_status=INTR_ON;
    	asm volatile("cti");
    	return old_status;
    }



    展开全文
  • 关于中断的碎碎念

    2017-08-31 20:55:47
    看了一下中断压栈和调用函数压栈的内容, 中断压栈时,需要对所有的寄存器进行压栈,因为进入内核态时,不知道具体会使用那些寄存器,因此只能一次把所有的都保存了;调用函数时,因为编译器实现已经知道了调用函数...

    看了一下中断压栈和调用函数压栈的内容,

    中断压栈时,需要对所有的寄存器进行压栈,因为进入内核态时,不知道具体会使用那些寄存器,因此只能一次把所有的都保存了;调用函数时,因为编译器实现已经知道了调用函数会需要使用那些寄存器,因此只需要调用函数对需要调用的函数进行压栈即可。


    arm在中断时,除了要保存通用寄存器,还要保存LR,和CPSR,LR用于保存原来下一条指令的地址,而CPSR用于保存之前系统的工作状态。


    理解这些根本上还是为了硬件实现时,中断单元如何实现。

    展开全文
  • linux 的压栈操作

    2015-01-11 10:19:58
    中断处理压栈 参考 深入理解linux内核
    中断处理压栈  参考 深入理解linux内核
    
    展开全文
  • 学会使用PendSV中断进行压栈和出栈操作,是实现任务调度的关键。今天我们就来学习一下如何使用不超过20行的汇编实现压栈和出栈操作。 我们现在来实现这么一个例子:先把R4-R11通用寄存器的值保存到一个缓冲区里面,...
  • 一般口头说“系统跑起来”,啥叫“跑起来”呢,很费解,也不形象,最近看了ucos一段话才感觉有点意思:系统的运行不是跑起来,而是像陀螺一样被鞭打才能被动地转动起来,鞭打源很简单,就是中断:定时中断、systick...
  • 中断和硬件中断

    千次阅读 2012-12-31 11:21:52
     软中断调用时将返回地址和CPU状态寄存器内容压栈,修改特权级,根据中断号查找中断向量表,找到ISR中断服务例程地址,跳转执行。  综上,函数调用和软中断调用的区别是,软中断多了修改特权级和查找中断向量表的...
  • 优点:无需CPU控制 或 中断压栈-出栈过程,让RAM与IO设备间可快速传输数据,减少CPU负载 stm32f4资源 双AHB总线,一个用于存储器访问,一个用于外设访问 编程接口仅支持32位访问的AHB使用DMA 最多2个DMA控制器...
  • 中断

    2016-11-21 10:29:00
    3. 非叶子函数lr需要压栈,怕后面的异常覆盖。 4. c语言子函数调用会自动操作bx,lr。 5. cp15协处理器,用于系统存储管理。 6. MMU负责虚拟地址到物理地址的映射,并提供硬件机制的内存访问检查。 7. 虚拟...
  • ARM中断

    2013-07-21 20:39:41
    2.CPU保存当前程序的运行环境,调用中断服务程序ISR来处理中断(如数据的压栈) 3. 在ISR中通过读取中断控制寄存器、外设的相关寄存器来识别是哪个中断,并进行相应的处理 4.清除中断:通过读写相关中断控制寄存器和...
  • STM32的中断处理2

    2013-10-27 14:26:10
    1、中断嵌套:第一个中断使用PSP或MSP压栈,嵌套中断使用MSP压栈。 2、抢占场景,替换原来的中断流程。 咬尾的判决处理:
  • 如果某些代码段不允许被中断打断,那么这段代码就必须用关中断的方式给保护起来,在UCOS中可以看到,一般保护方式有3种: (1)关闭中断(总中断或者指定的几个中断),执行临界段,开启中断...(2)把中断状态压栈...
  • 中断发生如何执行到中断服务程序

    千次阅读 2017-08-14 22:39:28
    首先,PIC(可编程中断控制器)将...执行中断服务程序前,首先进行当前进程寄存器的压栈处理,软后向cs和eip装载中断服务程序的基址和偏移,执行中断服务程序。 IDT表是可编辑的,一部分由内核初始化使用,一部分留给用户
  • 中断是个很重要的一个概念,在计算机组成原理中曾经讲过这一概念,对于...第一步:保护现场,将当前位置的PC地址压栈; 第二步:跳转到中断服务程序,执行中断服务程序; 第三步:恢复现场,将栈顶的值回送给PC; 第
  • 1.中断触发过程对主程序压栈——把中断服务函数的地址写入到程序计数器(PC)——执行中断服务函数2.中断向量表中断服务函数的地址在STM32的手册上的中断向量表中(如下是一部分):如上表所示,EXTI0中断服务函数的地址...
  • (13)中断

    2020-09-29 23:24:12
    一、中断门描述符 可以看到,中断门的结构和调用门非常像,主要是TYPE域,0x6表示16位中断门,0xE表示32位中断门。和调用门不同,中断门不能传参。 中断门存储在IDT表中,IDT表除了存储中断门描述符,还有任务门和...
  • 中断分析

    2017-02-20 15:04:16
    ARM CPU 在上电启动之后会自动进入SVC模式,也是ARM上电后的默认工作模式,如果发生了中断,ARM会自动切换到外部中断模式(IRQ为例),如果是FIQ那么就切换到FIQ模式下面进行处理。  在每一个模式下面都有一组可以...
  • 对于Cortex-M处理器,若中断时零等待的,而且系统设计允许取向量和压栈同时执行,则中断等待为12个周期,其中包括寄存器压栈、取向量以及取中断处理指令(这是最理想的情况)。 多周期指令执行期间产生中断 若...
  • 中断发生时的压栈 1根据CPL和DPL判断是否发生特权级的变换,将SS_old(用0扩张为16位)和ESP_old压入栈 2压入EFLAGS寄存器 3将CS_old 和 EIP_old 压入栈,中断程序执行结束后才能退回原来...
  • 1.过程调用指令 ...INT n 之后n4就是存放中断服务子程序入口地址的单元偏移地址 执行过程 FLAGS压栈 INT 指令的下一条指令的CS\IP压栈 n4得到存放中断向量的地址 将中断向量送给CS IP 转入中断服务程序 3.二者区
  • [转](13)中断

    2020-11-15 09:42:50
    一、中断门描述符 可以看到,中断门的结构和调用门非常像,主要是TYPE域,0x6表示16位中断门,0xE表示32位中断门。和调用门不同,中断门不能传参。 中断门存储在IDT表中,IDT表除了存储中断门描述符,还有...
  • 中断和异常

    2016-07-01 09:21:23
    def:由CPU外部引发的使CPU停下当前的任务,转而去执行其他任务的情况称为中断...1.错误:通常是指令的操作数的错误,CPU在执行完异常处理的代码后,再回到产生错误的这条指令(既这条指令会被压栈),在执行一次若还是
  • Cortex-M系列中断和异常(四)

    千次阅读 2019-11-06 16:39:53
    文章目录1 中断及异常的注意事项1.1 中断及异常的优先级配置1.2 中断与栈空间的关系...RETURN3 进入异常及返回异常流程3.1 异常进入及压栈3.2 异常返回及出栈4 中断等待和异常处理优化4.1 中断等待4.2 多周期指令执...
  • 此图选自《linux内核完全注释》 由上图可以看出,当特权级没有转换时,中断处理程序和被...然后切换堆栈,然后处理器把被中断进程的ss,esp,EFLAGS,CS,EIP,压栈,如果有错误码的话,错误码将最后被压栈(iretd指令并不...
  • stm32的中断等待

    2021-01-09 19:33:59
    而这个延迟就是压栈,跳转等等一系列动作造成的。 如果优先级不够高,或有其它的关中断操作什么的,那这个延迟时间还会更长。 具体有哪些操作会导致延迟时间变长,书中都有详细的描述。 2. 中断延迟中的12周期是...
  • 一般中断问题分析  ARM CPU 在上电启动之后会自动进入SVC... 在每一个模式下面都有一组可以访问的寄存器,SVC和IRQ模式下的R0~R12是共用的,这就涉及到寄存器Rx内容的保存:压栈,恢复:出栈操作。在IRQ处理中用到
  • 调试记录显示当时堆栈溢出,回推发生异常前PC将要执行的下一条指令,发现位于CAN_IntReceive_0位置,具体见附图,我个人认为是CAN接收中断优先级过低被1ms为单位的单位时间定时器中断频繁打断压栈造成的。...
  • 文中用一个例子解释了惰性压栈的原理。在发生中断嵌套时,Cortex-M处理器将使用出栈抢占、末端连锁、延迟到达等机制来优化响应速度,同时降低了功耗[6]。理解这部分原理,一方面有利于处理在中断中出现的BUG,另一...

空空如也

空空如也

1 2 3 4 5 ... 8
收藏数 143
精华内容 57
关键字:

中断压栈