- 由于 CPU 获知了计算机中发生的某些事,CPU 暂停正在执行的程序,转而去执行处理该事件的程序,当这段程序执行完毕后,CPU 继续执行刚才的程序。整个过程称为中断处理,也称为中断。
把中断按事件来源分类,来自CPU外部的中断就称为外部中断,来自CPU内部的中断就称为内部中断。
- 外部中断按是否导致宕机来划分,可分为可屏蔽中断和不可屏蔽中断。
- 内部中断按中断是否正常来划分,可分为软中断和异常。
外部中断
外部中断可分为:可屏蔽中断和不可屏蔽中断
- 外部中断是指来自CPU外部的中断,而外部的中断源必须是某个硬件,所以外部中断又称为硬件中断。CPU提供了两条信号线,外部硬件的中断是通过两根信号线通知CPU的,这两根信号线就是INTR(INTeRrupt)和 NMI(Non Maskable Interrupt)
- 可屏蔽中断:可屏蔽中断是通过INTR引脚进入CPU的,外部设备如硬盘、网卡等发出的中断都是可屏蔽中断。可屏蔽的意思是此外部设备发出的中断,CPU可以不理会,因为它不会让系统宕机,所以可以通过eflags寄存器的IF位将所有这些外部设备的中断屏蔽。另外,这些设备都是接在某个中断代理设备的,通过该中断代理也可以单独屏蔽某个设备的中断。对于这类可屏蔽中断,CPU 可以选择不用理会,甚至,即使在理会后,也可以像 Linux 那样,把中断分为上半部和下半部分开处理。
- 不可屏蔽中断:不可屏蔽中断是通过 NMI 引脚进入 CPU 的,它表示系统中发生了致命的错误,它等同于宣布:计算机的运行到此结束了。 eflags 寄存器中的 IF 位对其无效
内部中断
内部中断可分为:软中断和异常
- 软中断:软中断是由软件主动发起的中断。由于该中断是软件运行中主动发起的,所以它是主观上的,并不是客观上的内部错误
以下是可以发起中断的指令:
-
“int 8 位立即数”。这是我们以后常用的指令,我们要通过它进行系统调用,8 位立即数可表示 256种中断,这与处理器所支持的中断数是相吻合的。
-
“int3”。这可不是 int 空格 3,它们之间无间隙。int3 是调试断点指令,其所触发的中断向量号是 3,int3的机器码是0xcc。
-
into。这是中断溢出指令,它所触发的中断向量号是 4。不过,能否引发 4 号中断是要看 eflags 标志寄存器中的 OF 位是否为 1,如果是 1 才会引发中断,否则该指令悄悄地什么都不做,低调得很。
-
bound。这是检查数组索引越界指令,它可以触发 5 号中断,用于检查数组的索引下标是否在上下边界之内。该指令格式是
bound 16/32 位寄存器, 16/32 位内存
。目的操作数是用寄存器来存储的,其内容是待检测的数组下标值。。源操作数是内存,其内容是数组下标的下边界和上边界。当执行 bound 指令时,若下标处于数组索引的范围之外,则会触发 5 号中断。 -
ud2。未定义指令,这会触发第 6 号中断。该指令表示指令无效,CPU 无法识别。主动使用它发起中断,常用于软件测试中,无实际用途。
以上几种软中断指令,除第一种的“int 8 位立即数”之外,其他的几种又可以称为异常。
- 异常:异常是指令执行期间CPU内部产生的错误引起的,由于是运行时错误,所以它不受标志寄存器 eflags 中的 IF 位影响,无法向用户隐瞒(因为运行不下去了,错误兜不住了)。
- 并不是所有的异常都很致命,按照轻重程度,可以分为以下三种。:
(1) Fault,也称为故障。这种错误是可以被修复的一种类型,属于最轻的一种异常,它给软件一次“改过自新”的机会。当发生此类异常时 CPU 将机器状态恢复到异常之前的状态,之后调用中断处理程序时,CPU 将返回地址依然指向导致 fault 异常的那条指令。通常中断处理程序中会将此问题修复,待中断处理程序返回后便能重试。最典型的例子就是操作系统课程中所说的缺页异常 page fault,话说 Linux 的虚拟内存就是基于 page fault 的,这充分说明这种异常是极易被修复的,甚至是有益的。
(2) Trap,也称为陷阱,这一名称很形象地说明软件掉进了 CPU 设下的陷阱,导致停了下来。此异常通常用在调试中,比如 int3 指令便引发此类异常,为了让中断处理程序返回后能够继续向下执行,CPU将中断处理程序的返回地址指向导致异常指令的下一个指令地址。
(3) Abort,也称为终止,从名字上看,这是最严重的异常类型,一旦出现,由于错误无法修复,程序将无法继续运行,操作系统为了自保,只能将此程序从进程表中去掉。导致此异常的错误通常是硬件错误,或者某些系统数据结构出错。
向量号 | 助记符 | 说明 | 类型 | 错误号 | 产生源 |
---|---|---|---|---|---|
0 | #DE | 除 出错 | 故障 | 无 | DIV或IDIV指令 |
1 | #DB | 调试 | 故障/陷阱 | 无 | 任何代码或数据引用,或是INT 1指令 |
2 | - - | NMI中断 | 中断 | 无 | 非屏蔽外部中断 |
3 | #BP | 断点 | 陷阱 | 无 | INT 3指令 |
4 | #OF | 溢出 | 陷阱 | 无 | INTO指令 |
5 | #BR | 边界范围超出 | 故障 | 无 | BOUND指令 |
6 | #UD | 无效操作码(未定义操作码) | 故障 | 无 | UD2指令或保留的操作码(奔腾Pro中加入的新指令) |
7 | #NM | 设备不存在(无数学协处理器) | 故障 | 无 | 浮点或WAIT/FWAIT指令 |
8 | #DF | 双重错误 | 异常终止 | 有(0) | 任何可产生异常、NMI或INTR的指令 |
9 | - - | 协处理器段超越(保留) | 故障 | 无 | 浮点指令(386以后的CPU不产生该异常) |
10 | #TS | 无效的任务状态段TSS | 故障 | 有 | 任务交换或访问TSS |
11 | #NP | 段不存在 | 故障 | 有 | 加载段寄存器或访问系统段 |
12 | #SS | 堆栈错误 | 故障 | 有 | 堆栈操作和SS寄存器加载 |
13 | #GP | 一般保护错误 | 故障 | 有 | 任何内存引用和其他保护检查 |
14 | #PF | 页面保护 | 故障 | 有 | 任何内存引用 |
15 | (Intel保留,请勿使用) | 无 | |||
16 | #MF | x87FPU浮点错误(数学错误) | 故障 | 无 | x87FPU浮点或WAIT/FWAIT指令 |
17 | #AC | 对齐检查 | 故障 | 有(0) | 对内存中任何数据的引用 |
18 | #MC | 机器检查 | 异常终止 | 无 | 错误码(若有)和产生源与CPU类型有关(奔腾处理器引进) |
19 | #XM | SIMD浮点异常 | 故障 | 无 | SSE和SSE2浮点指令(PIII处理器引进) |
20~31 | - - | (Intel保留,请勿使用) | |||
32~255 | - - | 用户定义(非保留)中断 | 中断 | 外部中断或者INT n指令 |
中断描述符表
- 中断描述符表((Interrupt Descriptor Table,IDT)是保护模式下用于存储中断处理程序入口的表,当CPU接收一个中断时,需要用中断向量在此表中检索对应的描述符,在该描述符中找到中断处理程序的起始地址,然后执行中断程序
- 中断描述符表中不仅有中断描述符,还有任务门描述符和陷阱门描述符。由于表中所有描述符都是记录一段程序的起始地址,相当于某段程序的“大门”,所以,中断描述符表中的描述符有自己的名称-------门
任务门
- 任务门和任务状态段(Task Status Segment,TSS)是Intel处理器在硬件一级提供的任务切换机制,所以任务门需要和TSS配合在一起使用,在任务门中记录的是TSS选择子,偏移量未使用。任务门可以存在于全局描述符表GDT、局部描述符表LDT、中断描述符表IDT中。描述符中任务门的type值为二进制0101。大多数操作系统(包括 Linux)都未用 TSS 实现任务切换。
中断门
- 中断门包括了中断处理程序所在段的段选择子和段内偏移地址。当通过此方式进入中断后,标志位寄存器eflags中的IF位自动置0,也就是在进入中断后,自动把中断关闭,避免中断嵌套,Linux就是利用中断门实现的系统调用,就是那个著名的int 0x80。中断门只允许存在于IDT中。描述符中中断门的type值为二进制1110.
陷阱门
- 陷阱门和中断门非常相似,区别是由陷阱门进入中断后,标志寄存器eflags中的IF位不会自动置0。陷阱门只允许存在于IDT中。描述符中陷阱门的type值为二进制1111.
调用门
- 调用门是提供给用户进程进入特权0级的方式。其DPL为3。调用门中记录例程的地址,它不能用int指令调用,只能用call和jmp指令。调用门可以安装在GDT和LDT中。描述符中调用门的type值为二进制1100。
CPU 内部有个中断描述符表寄存器(Interrupt Descriptor Table Register,IDTR),该寄存器分为两部分:第0~15位是表界限, IDT 大小减 1,第 16~47 位是 IDT 的基地址。16 位的表界限,表示最大范围是 0xffff,即 64KB。可容纳的描述符个数是 64KB/8=8K=8192 个。特别注意的是 GDT 中的第 0个段描述符是不可用的,但 IDT 却无此限制,第 0 个门描述符也是可用的,中断向量号为 0 的中断是除法错。但处理器只支持 256个中断,即 0~254,中断描述符中其余的描述符不可用。在门描述符中有个 P 位,所以,咱们将来在构建 IDT 时,记得把 P 位置 0,这样就表示门描述符中的中断处理程序不在内存中。
同加载 GDTR 一样,加载 IDTR 也有个专门的指令—lidt,其用法是:lidt 48 位内存数据
中断处理过程及保护
完整的中断过程分为 CPU 外和 CPU 内两部分:
- CPU 外:外部设备的中断由中断代理芯片接收,处理后将该中断的中断向量号发送到 CPU。
- CPU 内:CPU 执行该中断向量号对应的中断处理程序。
CPU 内的过程:
- 处理器根据中断向量号定位中断门描述符。
中断向量号是中断描述符的索引,当处理器收到一个外部中断向量号后,它用此向量号在中断描述符表中查询对应的中断描述符,然后再去执行该中断描述符中的中断处理程序。由于中断描述符是8个字节,所以处理器用中断向量号乘以8后,再与IDTR中的中断描述符表地址相加,所求的地址之和便是该中断向量号对应的中断描述符 - 处理器进行特权级检查
由于中断是通过中断向量号通知处理器的,中断向量号只是个整数,并没有RPL,所以在对由中断引起的特权级转移做特权级检查中,并不涉及RPL。中断门的特权检查同调用门类似,对于软件主动发起的软中断,当前特权级CPL必须在门描述符DPL和门中目标代码段DPL之间。这是为了防止位于3特权级下的用户程序主动调用某些只为内核服务的例程。
(a) 如果是由软中断int n、int3和into引发的中断,这些是用户进程中主动发起的中断,由于用户代码控制,处理器要检查当前特权级CPL和门描述符DPL,这是检查进门的特权下限,如果CPL权限大于等于DPL,特权级“门槛”检查通过,进入下一步的“门框”检查,否则,处理器抛出异常
(b) 这一步检查特权级的上限(门框):处理器要检查当前特权级CPL和门描述符中所记录的选择子对应的目标代码段 DPL,如果 CPL 权限小于目标代码段 DPL,检查通过,否则 CPL 若大于等于目标代码段 DPL,处理器将引发异常,也就是说,除了用返回指令从高特权级返回,特权转移只能发生在由低向高。
若中断是由外部设备和异常引起的,只直接检查 CPL 和目标代码段的 DPL,和上面的步骤 b)是一样的,要求 CPL 权限小于目标代码段 DPL,否则处理器引发异常。 - 执行中断处理程序。
特权级检查通过后,将门描述符目标代码段选择子加载到代码段寄存器 CS 中,把门描述符中中断处理程序的偏移地址加载到 EIP,开始执行中断处理程序。
-
中断发生后,eflags中的NT位和TF位会被置0.如果中断对应的门描述符是中断门,标志寄存器eflags的IF位会被自动置0,避免中断嵌套,即中断处理过程中又来了个新的中断,这是为防止处理某个中断的过程中又来了个相同的中断。这会导致一般保护性(GP)异常。这表示默认情况下,处理器会在无人打扰的方式下执行中断门描述符中的中断处理例程。
-
若中断发生时对应的是任务门或陷阱门,CPU 是不会将 IF 位清 0 的。因为陷阱门主要用于调试,它允许 CPU 响应更高级别的中断,所以允许中断嵌套。而对任务门来说,这是执行一个新任务,任务都应该在开中断的情况下进行,否则就独占 CPU 资源,操作系统也会由多任务退化成单任务了。
-
从中断返回的指令是 iret,它从栈中弹出数据到寄存器 cs、eip、eflags 等,根据特权级是否改变,判断是否要恢复旧栈,也就是说是否将栈中位于 SS_old 和 ESP_old 位置的值弹出到寄存 ss 和 esp。当中断处理程序执行完成返回后,通过 iret 指令从栈中恢复 eflags 的内容。
中断发生时的压栈
中断发生后,由于CS加载了新的目标代码段选择子,处理器不管新的选择子和任何段寄存器中当前的选择子是否相同,也不管这两个选择子是否指向相同的段,只要段寄存器被加载,段描述符缓冲寄存器就会被刷新,处理器都会认为换了一个段,属于段间转移,也就是远转移。所以,当前进程被中断打断后,为了从中断返回后能继续运行该进程,处理器自动把CS和EIP的当前值保存到中断处理程序使用的栈中。不同特权级别下处理器使用不同的栈,至于中断处理程序使用的是哪个栈,要视它当时所在的特权级别,因为中断是可以在任何特权级别下发生的。除了要保存CS,EIP外,还需要保存标志寄存器EFLAGS,如果涉及到特权级变换,还要压入SS和ESP寄存器。
下面看看以上寄存器入栈情况及顺序,这里不再讨论有关特权检查的内容:
-
处理器根据中断向量号找到对应的中断描述符后,拿CPL和中断门描述符中选择子对应的目标代码段的DPL比对,若CPL权限比DPL低,这表示要向高特权级转移,需要切换到高特权级的栈。这意味着当执行完中断处理程序后,若要正确返回到当前被中断的进程,同样需要将栈恢复为此时的旧栈。于是处理器先临时保存旧栈SS和ESP的值,记作SS_old 和 ESP_old,然后在TSS中找到同目标代码段DPL级别相同的栈加载到寄存器SS和ESP中,记作SS_new和ESP_new,再将之前临时保存的SS_old和ESP_old压入新栈备份,以备返回时重新加载到栈段寄存器SS和栈指针ESP。由于SS_old是16位数据,32位模式下的栈操作数是32位,所以将SS_old用0扩展其16位,成为32位数据后入栈。此时新栈内容如图A所示。
-
在新栈中压入EFLAGS寄存器,新栈内容如图B所示
-
由于要切换到目标代码段,对于这种段间转移,要将CS和EIP保存到当前栈中备份,记作CS_old和EIP_old,以便中断程序执行结束后能恢复到被中断的进程。同样 CS_old 是 16 位数据,需要用 0 填充其高 16 位,扩展为 32 位数据后入栈。此时新栈内容如图C所示。
-
某些异常会有错误码,此错误码用于报告异常是在哪个段上发生的,也就是异常发生的位置,所以错误码中包含选择子等信息,错误码会紧跟在 EIP 之后入栈,记作 ERROR_CODE。此时新栈内容如图D所示。
- 如果在第 1 步中判断未涉及到特权级转移,便不会到 TSS 中寻找新栈,而是继续使用当前旧栈,因此也谈不上恢复旧栈,此时中断发生时栈中数据不包括 SS_old 和 ESP_old。比如中断发生时当前正在运行的是内核程序,这是 0 特权级到 0 特权级,无特权级变化
- 处理器进入中断执行完中断处理程序后,还要返回到被中断的进程,这是进入中断的逆过程。中断返回是用iret指令实现的。Iret,即 interrupt ret,此指令专用于从中断处理程序返回,假设在32位模式下,它从当前栈顶处依次弹出32 位数据分别到寄存器 EIP、CS、EFLAGS。iret 指令并不清楚栈中数据的正确性,它只负责把栈顶处往上的数据,每次 4 字节,对号入座弹出到相关寄存器,所以在使用 iret 之前,一定要保证栈顶往上的数据是正确的,且从栈顶往上的顺序是 EIP、CS、EFLAGS,根据特权级是否有变化,还有 ESP、SS。
- 由于段寄存器 CS 是 16 位,故从栈中返回的 32 位数据,其高16 位被丢弃,只将低 16 位载入到 CS。若处理器发现返回后特权级会变化,还会继续将两个双字数据返回到 ESP、SS,其中 SS也是 16 位寄存器,所以同样也是弹出 32 位数据后,只将其中的低 16 位加载到 SS。iret 指令意味着从中断返回,所以,它是中断处理程序中最后一个指令。
- 同类的指令还有 iretw 和 iretd,16 位模式下用 iretw,32 位模式下用 iretd。iret 是 iretw 和 iretd的简写,无论是在 16 位模式,还是在 32 位模式下编码,都可以只用 iret 指令,它是被编译成 iretw,还是iretd,取决于伪指令 BITS 所指明的字长。
下面咱们聊聊这个从中断处理程序返回的过程:
(1)当处理器执行到 iret 指令时,它知道要执行远返回,首先需要从栈中返回被中断进程的代码段选择子CS_old 及指令指针 EIP_old。这时候它要进行特权级检查。先检查栈中 CS 选择子 CS_old,根据其RPL 位,即未来的 CPL,判断在返回过程中是否要改变特权级。
(2) 栈中 CS 选择子是 CS_old,根据 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 中记录的新栈)。
(3) 将栈中保存的 EFLAGS 弹出到标志寄存器 EFLAGS。如果在第 1 步中判断返回后要改变特权级,此时栈指针是 ESP_new,它指向栈中的 ESP_old。否则进入中断时属于平级转移,用的是旧栈,此时栈指针是 ESP_old,栈中已无因此次中断发生而入栈的数据,栈指针指向中断发生前的栈顶。
(4) 如果在第 1 步中判断出返回时需要改变特权级,也就是说需要恢复旧栈,此时便需要将 ESP_old和 SS_old 分别加载到寄存器 ESP 及 SS,丢弃寄存器 SS 和 ESP 中原有的 SS_new 和 ESP_new,同时进行特权级检查。补充,由于 SS_old 在入栈时已经由处理器将高 16 位填充为 0,现在是 32 位数据,所以在重新加载到栈段寄存器 SS 之前,需要将 SS_old 高 16 位剥离丢弃,只用其低 16 位加载 SS。
中断错误码
有些中断会在栈中压入错误码,有点“临终遗言,提供线索”的意味,用来指明中断发生在哪个段上。所以,错误码最主要的部分就是选择子,只不过此选择子可以在多种表中检索描述符。错码码由几部分组成,格式如图 所示
- EXT 表示 EXTernal event,即外部事件,用来指明中断源是否来自处理器外部,如果中断源是不可屏蔽中断 NMI 或外部设备,EXT 为 1,否则为 0。
可编程中断控制器 8259A
-
8259A 用于管理和控制可屏蔽中断,它表现在屏蔽外设中断,对它们实行优先级判决,向 CPU 提供中断向量号等功能。而它称为可编程的原因,就是可以通过编程的方式来设置以上的功能。
-
Intel 处理器共支持 256 个中断,但 8259A 只可以管理 8 个中断,所以为了多支持一些中断设备,提供了另一个解决方案,将多个 8259A 组合,官方术语就是级联。有了级联这种组合后,每一个 8259A 就被称为 1 片。若采用级联方式,即多片 8259A 芯片串连在一起,最多可级 9 个,也就是最多支持 64 个中断。n 片 8259A 通过级联可支持 7n+1 个中断源,级联时只能有一片 8259A为主片 master,其余的均为从片 slave。来自从片的中断只能传递给主片,再由主片向上传递给 CPU,也就是说只有主片才会向 CPU 发送 INT 中断信号。
-
每个独立运行的外部设备都是一个中断源,它们所发出的中断,只有接在中断请求(IRQ:InterruptReQuest)信号线上才能被 CPU 大神知晓,这也就是大家在开机时,电脑屏幕上会看到的 IRQ1…IRQn,这些都是为外部设备所分配的中断号。
8259A 内部结构逻辑示意图:
- INT:8259A 选出优先级最高的中断请求后,发信号通知 CPU。
- INTA:INT Acknowledge,中断响应信号。位于 8259A 中的 INTA 接收来自 CPU 的 INTA 接口的中断响应信号。
- IMR:Interrupt Mask Register,中断屏蔽寄存器,宽度是 8 位,用来屏蔽某个外设的中断。
- IRR:Interrupt Request Register,中断请求寄存器,宽度是 8 位。它的作用是接受经过 IMR 寄存器过滤后的中断信号并锁存,此寄存器中全是等待处理的中断,“相当于”5259A 维护的未处理中断信号队列。
- PR:Priority Resolver,优先级仲裁器。当有多个中断同时发生,或当有新的中断请求进来时,将它与当前正在处理的中断进行比较,找出优先级更高的中断。
- ISR:In-Service Register,中断服务寄存器,宽度是 8 位。当某个中断正在被处理时,保存在此寄存器中。
8259A工作流程:当某个外设发送一个中断信号时,由于主板上已经将信号通路指向了8259A芯片的某个IRQ接口,所以该中断信号最终被送入了8259A。
- 8259A首先检查IMR寄存器中是否已经屏蔽了来自该IRQ接口的中断信号。IMR寄存器中的位,为1,则表示中断屏蔽,为0则表示中断放行。如果该IRQ对应的的位已经被置1,即表示来自该IRQ接口上的中断已经被屏蔽了。则将该中断信号丢弃,否则,将其送入IRR寄存器,
- 送入IRR寄存器后,将该IRQ接口所在IRR寄存器中对应的BIT置1。IRR寄存器的作用“相当于”待处理中断队列。在某个恰当时机,优先级仲裁器PR会从IRR寄存器中挑选一个优先级最大的中断,此处的优先级决判很简单,就是IRQ接口号越低,优先级越大,所以IRQ0优先级最大。
- 之后,8259A会在控制电路中,通过INT接口向CPU发送INTR信号。信号被送入了CPU的INTR接口后,这样CPU便知道有信的中断来了,又有活干了,于是CPU将手里的指令执行完后,马上通过自己的INTA接口向8259A的INTA接口回复一个中断响应信号,表示现在CPU我已经准备好啦,8259A你可以继续后面的工作。
- 8259A在收到这个信号后,立即将刚才选出的优先级最大的中断在ISR寄存器中对应的BIT置1,此寄存器表示当前正在处理的中断,同时要将该中断从“待处理中断队列”IRR中去掉,也就是IRR中奖该中断对应1的BIT置0。
- 之后,CPU将再次发送INTA信号给8259A,这一次是想获取中断对应的中断向量号。由于大部分情况下8259A的起始中断向量号并不是 0(起始中断向量号被修改,原因后面会说),所以用起始中断向量号+IRQ 接口号便是该设备的中断向量号,由此可见,外部设备虽然会发中断信号,但它并不知道还有中断向量号这回事,不知道自己会被中断代理(如 8259A)分配一个这样的整数。
- 随后,8259A 将此中断向量号通过系统数据总线发送给 CPU。CPU 从数据总线上拿到该中断向量号后,用它做中断向量表或中断描述符表中的索引,找到相应的中断处理程序并去执行。
如果8259A的“EOI通知(End Of Interrupt)”若被设置为非自动模式(手工模式),中断处理程序结束处必须有向 8259A 发送 EOI 的代码,8259A 在收到 EOI 后,将当前正处理的中断在 ISR 寄存器中对应的 BIT 置 0。如果“EOI 通知”被设置为自动模式,在刚才 8259A 接收到第二个 INTA 信号后,也就是 CPU 向 8259A 要中断向量号的那个 INTA,8259A 会自动将此中断在 ISR 中对应的 BIT 置 0。
8259A 的编程
8259A 的编程就是对它进行初始化,设置主片与从片的级联方式,指定起始中断向量号以及设置各种工作。
中断向量号是逻辑上的东西,它在物理上是 8259A 上的 IRQ 接口号。8259A 上 IRQ 号的排列顺序是固定的,但其对应的中断向量号是不固定的,这其实是一种由硬件到软件的映射,通过设置 8259A,可以 IRQ 接口映射到不同的中断向量号。
在 8259A 内部有两组寄存器,一组是初始化命令寄存器组,用来保存初始化命令字(InitializationCommand Words,ICW),ICW 共 4 个,ICW1~ICW4。另一组寄存器是操作命令寄存器组,用来保存操作命令字(Operation Command Word,OCW),OCW 共 3 个,OCW1~OCW3。所以,我们对 8259A 的编程,也分为初始化和操作两部分。
- 一部分是用 ICW 做初始化,用来确定是否需要级联,设置起始中断向量号,设置中断结束模式。其编程就是往 8259A 的端口发送一系列 ICW。由于从一开始就要决定 8259A 的工作状态,所以要一次性写很多设置,某些设置之间是具有关联、依赖性的,也许后面的某个设置会依赖前面某个 ICW 写入的设置,所以这部分要求严格的顺序,必须依次写入 ICW1、ICW2、ICW3、ICW4。
- 另一部分是用 OCW 来操作控制 8259A,前面所说的中断屏蔽和中断结束,就是通过往 8259A 端口发送 OCW 实现的。OCW 的发送顺序不固定,3 个之中先发送哪个都可以。
ICW1 用来初始化 8259A 的连接方式和中断信号的触发方式。连接方式是指用单片工作,还是用多片级联工作,触发方式是指中断请求信号是电平触发,还是边沿触发。 - 注意,ICW1 需要写入到主片的 0x20 端口和从片的 0xA0 端口
- IC4 表示是否要写入 ICW4,这表示,并不是所有的 ICW 初始化控制字都需要用到。IC4 为 1 时表示需要在后面写入 ICW4,为 0则不需要。注意,x86 系统 IC4 必须为 1。
- SNGL 表示 single,若 SNGL 为 1,表示单片,若 SNGL 为 0,表示级联(cascade)。这里说一下,若在级联模式下,这要涉及到主片(1个)和从片(多个)用哪个 IRQ 接口互相连接的问题,所以当 SNGL 为 0 时,主片和从片也是需要 ICW3 的。
- ADI 表示 call address interval,用来设置 8085 的调用时间间隔,x86 不需要设置。
- LTIM 表示 level/edge triggered mode,用来设置中断检测方式,LTIM 为 0 表示边沿触发,LTIM 为 1表示电平触发。
- 第 4 位的 1 是固定的,这是 ICW1 的标记。
- 第 5~7 位专用于 8085 处理器,x86 不需要,直接置为 0 即可。
ICW2 用来设置起始中断向量号,就是前面所说的硬件 IRQ 接口到逻辑中断向量号的映射。由于每个8259A 芯片上的 IRQ 接口是顺序排列的,所以咱们这里的设置就是指定 IRQ0 映射到的中断向量号,其他IRQ 接口对应的中断向量号会顺着自动排下去。
- 注意,ICW2 需要写入到主片的 0x21 端口和从片的 0xA1 端口
- 由于咱们只需要设置 IRQ0 的中断向量号,IRQ1~IRQ7 的中断向量号是 IRQ0 的顺延,所以,咱们只负责填写高 5 位 T3~T7,ID0~ID2 这低 3 位不用咱们负责。由于咱们只填写高 5 位,所以任意数字都是8 的倍数,这个数字表示的便是设定的起始中断向量号。这是有意设计的,低 3 位能表示 8 个中断向量号,这由 8259A 根据 8 个 IRQ 接口的排列位次自行导入,IRQ0 的值是 000,IRQ1 的值是 001,IRQ2 的值便 010……以此类推,这样高 5 位加低 3 位,便表示了任意一个 IRQ 接口实际分配的中断向量号。
ICW3 仅在级联的方式下才需要(如果 ICW1 中的 SNGL 为 0),用来设置主片和从片用哪个 IRQ 接口互连。
由于主片和从片的级联方式不一样,对于这个 ICW3,主片和从片都有自己不同的结构
- ICW3 需要写入主片的 0x21 端口及从片的 0xA1 端口。
- 对于主片,ICW3 中置 1 的那一位对应的 IRQ 接口用于连接从片,若为 0 则表示接外部设备。比如,若主片 IRQ2 和 IRQ5 接有从片,则主片的 ICW3 为 00100100
- 对于从片,要设置与主片 8259A 的连接方式,“不需要”指定用自己的哪个 IRQ 接口与主片连接,从片上专门用于级联主片的接口并不是 IRQ。设置从片连接主片的方法是只需要在从片上指定主片用于连接自己的那个 IRQ 接口就行了
- 在中断响应时,主片会发送与从片做级联的 IRQ 接口号,所有从片用自己的 ICW3 的低 3 位和它对比,若一致则认为是发给自己的。比如主片用 IRQ2 接口连接从片 A,用 IRQ5 接口连接从片 B,从片 A 的 ICW3 的值就应该设为 00000010,从片 B 的 ICW3 的值应该设为 00000101。所以,从片 ICW3中的低 3 位 ID0~ID2 就够了,高 5 位不需要,为 0 即可
- 第 7~5 位未定义,直接置为 0 即可。
- SFNM 表示特殊全嵌套模式(Special Fully Nested Mode),若 SFNM 为 0,则表示全嵌套模式,若 SFNM 为1,则表示特殊全嵌套模式。
- BUF 表示本 8259A 芯片是否工作在缓冲模式。BUF 为 0,则工作非缓冲模式,BUF 为 1,则工作在缓冲模式。
- 当多个 8259A 级联时,如果工作在缓冲模式下,M/S 用来规定本 8259A 是主片,还是从片。若 M/S 为1,则表示则表示是主片,若 M/S 为 0,则表示是从片。若工作在非缓冲模式(BUF 为 0)下,M/S 无效。
- AEOI 表示自动结束中断(Auto End Of Interrupt),8259A 在收到中断结束信号时才能继续处理下一个中断,此项用来设置是否要让 8259A 自动把中断结束。若 AEOI 为 0,则表示非自动,即手动结束中断,咱们可以在中断处理程序中或主函数中手动向 8259A 的主、从片发送 EOI 信号。这种“操作”类命令,通过下面要介绍的 OCW 进行。若 AEOI 为 1,则表示自动结束中断。
- μPM 表示微处理器类型(microprocessor),此项是为了兼容老处理器。若 μPM 为 0,则表示 8080 或8085 处理器,若 μPM 为 1,则表示 x86 处理器。
OCW1 用来屏蔽连接在 8259A 上的外部设备的中断信号,实际上就是把 OCW1 写入了 IMR 寄存器。这里的屏蔽是说是否把来自外部设备的中断信号转发给 CPU。由于外部设备的中断都是可屏蔽中断,所以最终还是要受标志寄存器 eflags 中的 IF 位的管束,若 IF 为 0,可屏蔽中断全部被屏蔽,也就是说,在IF 为 0 的情况下,即使 8259A 把外部设备的中断向量号发过来,CPU 也置之不理。
- 注意,OCW1 要写入主片的 0x21 或从片的 0xA1 端口
- M0~M7 对应 8259A 的 IRQ0~IRQ7,某位为 1,对应的 IRQ 上的中断信号就被屏蔽了。否则某位为0 的话,对应的 IRQ 中断信号则被放行。
- OCW2 用来设置中断结束方式和优先级模式。
- 注意,OCW2 要写入到主片的 0x20 及从片的 0xA0 端口。
- OCW2 的配置比较复杂,各种属性位要配合在一起,组合出 8259A 的各种工作模式。由高 3 位 R、SL、EOI 可以定义多种中断结束方式和优先级循环方式。
- R,Rotation,表示是否按照循环方式设置中断优先级。R 为 1 表示优先级自动循环,R 为 0 表示不自动循环,采用固定优先级方式。
- SL,Specific Level,表示是否指定优先等级。等级是用低 3 位来指定的。此处的 SL 只是开启低 3 位的开关,所以 SL 也表示低 3 位的 L2~L0 是否有效。SL 为 1 表示有效,SL 为 0 表示无效。
- EOI,End Of Interrupt,为中断结束命令位。令 EOI 为 1,则会令 ISR 寄存器中的相应位清 0,也就是将当前处理的中断清掉,表示处理结束。向 8259A 主动发送 EOI 是手工结束中断的做法,所以,使用此命令有个前提,就是 ICW4 中的 AEOI 位为 0,非自动结束中断时才用。
- 第 4~3 位的 00 是 OCW2 的标识。
- L2~L0 用来确定优先级的编码,这里分两种,一种用于 EOI 时,表示被中断的优先级别,另一种用于优先级循环时,指定起始最低的优先级别。
- OCW3 用来设定特殊屏蔽方式及查询方式
- 注意,OCW3要写入主片的0x20端口或从片的0xA0端口。
- 第 7 位未用到。
- 6 位的 ESMM(Enable Special Mask Mode)和第 5 位的 SMM(Special Mask Mode)是组合在一起用的,用来启用或禁用特殊屏蔽模式。ESMM 是特殊屏蔽模式允许位,是个开关。SMM 是特殊屏蔽模式位。只有在启用特殊屏蔽模式时,特殊屏蔽模式才有效。也就是若 ESMM 为 0,则 SMM 无效。若 ESMM 为 1,SMM 为 0,表示未工作在特殊屏蔽模式。若 ESMM 和 SMM 都为 1,这才正式工作在特殊屏蔽模式下。
- 第 4~3 位的 01 是 OCW3 的标识,8259A 通过这两位判断是哪个控制字。
- P,Poll command,查询命令,当 P 为 1 时,设置 8259A 为中断查询方式,这样就可以通过读取寄存器,
- RR,Read Register,读取寄存器命令。它和 RIS 位是配合在一起使用的。当 RR 为 1 时才可以读取寄存器。
- RIS,Read Interrupt register Select,读取中断寄存器选择位,顾名思义,就是用此位选择待读取的寄存器。有点类似显卡寄存器中的索引的意思。若 RIS 为 1,表示选择 ISR 寄存器,若 RIS 为 0,表示选择 IRR 寄存器。这两个寄存器能否读取,前提是 RR 的值为 1。
对于 8259A 的初始化必须最先完成,步骤是:
- 无论 8259A 是否级联,ICW1 和 ICW2 是必须要有的,并且要顺序写入。
- 只有当 ICW1 中的 SNGL 位为 0 时,这表示级联,级联就需要设置主片和从片,这才需要在主片和从片中各写入 ICW3。注意,ICW3 的格式在主片和从片中是不同的。
- 只能当 ICW1 中的 IC4 为 1 时,才需要写入 ICW4。不过,x86 系统 IC4 必须为 1。
- x86 系统中,对于初始化级联 8259A,4 个 ICW 都需要,初始化单片 8259A,ICW3不要,其余全要。
在以上初始化 8259A 之后才可以用 OCW 对它操作。
//interrupt.c
#include "interrupt.h"
#include "print.h"
#include "io.h"
/*中断门描述符结构体*/
struct gate_desc {
unsigned short func_offset_low_word;
unsigned short selector;
unsigned char dcount; //此项为双字计数字段,是门描述符中的第4字节。此项固定值,不用考虑
unsigned char attribute;
unsigned short func_offset_high_word;
};
static struct gate_desc idt[33]; // idt是中断描述符表,本质上就是个中断门描述符数组
extern int intr_entry_table[33];
static void idt_desc_init(void)
{
int i;
for (i = 0; i < 33; i++)
{
idt[i].func_offset_low_word = ((unsigned int)intr_entry_table[i]) & 0x0000FFFF;
idt[i].selector=0b1000; //代码段选择子
idt[i].dcount = 0; //未使用
idt[i].attribute=0b10001110;
idt[i].func_offset_high_word = ((unsigned int)intr_entry_table[i] & 0xFFFF0000) >>16;
}
put_str(" idt_desc_init done\n");
}
/* 初始化可编程中断控制器8259A */
static void pic_init(void) {
/* 初始化主片 */
outb (0x20, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (0x21, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
outb (0x21, 0x04); // ICW3: IR2接从片. 对于主片,ICW3 中置 1 的那一位对应的 IRQ 接口用于连接从片
outb (0x21, 0x01); // ICW4: 8086模式, 正常EOI
/* 初始化从片 */
outb (0xa0, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (0xa1, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
outb (0xa1, 0x02); // ICW3: 设置从片连接到主片的IR2引脚
outb (0xa1, 0x01); // ICW4: 8086模式, 正常EOI
/* 打开主片上IR0,也就是目前只接受时钟产生的中断 */
outb (0x21, 0xfe); //OCW1主片
outb (0xa1, 0xff); //OCW1 从片
put_str(" pic_init done\n");
}
void idt_init()
{
put_str("idt_init start\n");
idt_desc_init(); // 初始化中断描述符表
pic_init(); // 初始化8259A
//第0~15位是表界限,即IDT减1,可容纳8192个中段描述符;第16~47位时IDT的基地址。
/* 加载idt */
unsigned long long int idt_operand = ((sizeof(idt) - 1) | (( unsigned long long int)(unsigned int)idt << 16));
asm volatile("lidt %0" : : "m" (idt_operand));
put_str("idt_init done\n");
}
//kernel.c
#include "print.h"
#include "io.h"
void intr_entry_0()
{
put_str("intr_entry_0\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_1()
{
put_str("intr_entry_1\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_2()
{
put_str("intr_entry_2\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_3()
{
put_str("intr_entry_3\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_4()
{
put_str("intr_entry_4\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_5()
{
put_str("intr_entry_5\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_6()
{
put_str("intr_entry_6\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_7()
{
put_str("intr_entry_7\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_8()
{
put_str("intr_entry_8\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("addl $4, %esp;iret");
}
void intr_entry_9()
{
put_str("intr_entry_9\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_a()
{
put_str("intr_entry_A\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("addl $4, %esp;iret");
}
void intr_entry_b()
{
put_str("intr_entry_b\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("addl $4, %esp;iret");
}
void intr_entry_c()
{
put_str("intr_entry_c\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_d()
{
put_str("intr_entry_d\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("addl $4, %esp;iret");
}
void intr_entry_e()
{
put_str("intr_entry_e\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("addl $4, %esp;iret");
}
void intr_entry_f()
{
put_str("intr_entry_f\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_10()
{
put_str("intr_entry_10\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_11()
{
put_str("intr_entry_11\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("addl $4, %esp;iret");
}
void intr_entry_12()
{
put_str("intr_entry_12\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_13()
{
put_str("intr_entry_13\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_14()
{
put_str("intr_entry_14\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_15()
{
put_str("intr_entry_15\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_16()
{
put_str("intr_entry_16\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_17()
{
put_str("intr_entry_17\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_18()
{
put_str("intr_entry_18\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("addl $4, %esp;iret");
}
void intr_entry_19()
{
put_str("intr_entry_19\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_1a()
{
put_str("intr_entry_1a\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("addl $4, %esp;iret");
}
void intr_entry_1b()
{
put_str("intr_entry_1b\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("addl $4, %esp;iret");
}
void intr_entry_1c()
{
put_str("intr_entry_1c\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_1d()
{
put_str("intr_entry_1d\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("addl $4, %esp;iret");
}
void intr_entry_1e()
{
put_str("intr_entry_1e\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("addl $4, %esp;iret");
}
void intr_entry_1f()
{
put_str("intr_entry_1f\n");
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
void intr_entry_20()
{
static int i=0;
put_str("intr_entry_20\n");
put_int(i);
put_str("\n");
i++;
// 向主片和从片发送中断结束命令(EOI)
outb(0xA0, 0x20); // 向从片发送EOI
outb(0x20, 0x20); // 向主片发送EOI
asm volatile ("leave");
asm volatile ("iret");
}
int intr_entry_table[33]={
(int)intr_entry_0,
(int)intr_entry_1,
(int)intr_entry_2,
(int)intr_entry_3,
(int)intr_entry_4,
(int)intr_entry_5,
(int)intr_entry_6,
(int)intr_entry_7,
(int)intr_entry_8,
(int)intr_entry_9,
(int)intr_entry_a,
(int)intr_entry_b,
(int)intr_entry_c,
(int)intr_entry_d,
(int)intr_entry_e,
(int)intr_entry_f,
(int)intr_entry_10,
(int)intr_entry_11,
(int)intr_entry_12,
(int)intr_entry_13,
(int)intr_entry_14,
(int)intr_entry_15,
(int)intr_entry_16,
(int)intr_entry_17,
(int)intr_entry_18,
(int)intr_entry_19,
(int)intr_entry_1a,
(int)intr_entry_1b,
(int)intr_entry_1c,
(int)intr_entry_1d,
(int)intr_entry_1e,
(int)intr_entry_1f,
(int)intr_entry_20};
为什么要加“leave”
用objdump -d kernel.o
命令查看反汇编代码可以看出,编译器在函数的开头部分,增加了保存旧的基址指针、建立新的基址指针以及为局部变量分配空间的代码。所以需要使用leave
命令相当于mov esp, ebp
pop ebp
,使堆栈平衡。