保护模式下的80386及其编程04:中断及异常

目录

1 概述

1.1 什么是中断和异常

1.2 中断和异常处理时机

1.3 中断向量

1.4 中断和异常的屏蔽简介

2 中断

2.1 INTR中断

2.2 NMI中断

3 异常

3.1 异常分类

3.1.1 故障(Fault)

3.1.2 陷阱(Trap)

3.1.3 终止(Abort)

3.2 异常小结

3.2.1 异常0:除法出错

3.2.2 异常1:排错异常

3.2.3 异常3:单字节INT3

3.2.4 异常4:溢出

3.2.5 异常5:边界检查

3.2.6 异常6:无效操作码

3.2.7 异常7:设备不可用

3.2.8 异常8:双重故障

3.2.9 异常10:无效TSS

3.2.10 异常11:段不存在

3.2.11 异常12:栈段异常

3.2.12 异常13:通用保护

3.2.13 异常14:页异常

4 中断和异常的优先级

5 屏蔽中断和异常

6 中断和异常的转移方法

6.1 中断描述符表(IDT)

6.2 中断描述符

6.2.1 Selector & Offset

6.2.2 P位

6.2.3 DPL

6.2.4 Type

6.3 通过中断门和陷阱门转移

6.3.1 选择中断描述符

6.3.2 门级检查

6.3.3 段级检查

6.3.4 特权级与栈切换

6.3.5 EFLAGS寄存器压栈与设置

6.3.6 保存返回地址

6.3.7 控制流转移

6.3.8 出错码压栈

6.3.9 IRET返回

6.4 通过任务门转移

6.5 通过中断/陷阱门和任务门处理中断/异常的比较


1 概述

1.1 什么是中断和异常

1. 中断和异常是特殊的控制转移方法,当中断或异常发生时,处理器在结束当前指令的执行后,将控制转移到一个处理中断或异常的程序,在处理完成后通过一条IRET指令返回被中断的程序

2. 中断是异步的

① 中断由一个外部事件引起,与正在执行的指令没有关系

② 中断通常用于指示IO设备的一次操作已经完成

3. 异常是同步的

① 异常是在指令执行期间检测到的不正常或非法的条件

② 异常与所执行的指令有直接的联系

1.2 中断和异常处理时机

1. 中断和异常通常在两条指令之间进行处理

2. 但是80386中有些指令执行时间相对较长,而且执行时间不固定(e.g. 串操作指令,movs等)。为了保证系统有良好的中断响应,在串操作指令中,80386允许在每次重复操作后响应中断

1.3 中断向量

1. 每种中断和异常,都有一个与之相联系的中断向量,用8位二进制数表示。因此,中断向量的范围为0 ~ 255

2. 处理器通过中断向量从中断描述符表(IDT)中为给定的中断或异常选择处理程序

3. 分配给异常的中断向量为0 ~ 31,80386处理器体系结构确定的

4. 中断可以使用的中断向量号其实是0 ~ 255范围内的任何值(通过设置中断控制器实现),但是为了避免与异常使用的中断向量冲突,最好分配在32 ~ 255范围内

1.4 中断和异常的屏蔽简介

1. 在正常情况下,中断和异常可以在任意两条指令之间发生,但是处理器提供了相应机制屏蔽中断和某些异常

2. 只有调试相关的异常可以屏蔽,其他异常(e.g. 段异常、页异常)不能被屏蔽

2 中断

80386支持可屏蔽中断INTR和不可屏蔽中断NMI这两种类型的中断,处理器有2个引脚分别对应这两种类型的中断

说明:此处的屏蔽与不可屏蔽,是指是否可以被EFLAGS寄存器中的IF位屏蔽

2.1 INTR中断

1. INTR中断是可屏蔽中断,可以有任意的中断向量号。中断控制器在发出INTR中断信号的同时,还会向处理器发送中断向量号

以8059A中断控制器为例,会在CPU向8259A发送第2个INTA(Acknowledge)信号期间,将一个代表中断向量号的8位数据送到数据总线上供CPU读取

关于8059A中断控制器的详细内容,可参考X86汇编语言从实模式到保护模式08:中断和动态时钟显示chapter 1.3及chapter 2.5.5

2. INTR中断可以用EFLAGS寄存器中的IF位进行屏蔽

IF(Interrupt-enable Flag)

中断允许标志位

  1. 用于控制处理器响应INTR引脚发来的外部中断信号
  1. 当IF = 1,响应外部中断;当IF = 0,忽略外部中断

3. 当EFLAGS寄存器中的IF位为0时,如果有INTR中断产生,信号会被送到80386处理器,但是处理器不会响应该中断请求。此时,该中断信号被挂起(pending,直到IF位被置为1或外部硬件撤销该中断请求

这里的中断挂起,存在于3个层面,

① 中断控制器层面,中断控制器中的中断请求寄存器的相关位会一直为1,表示该中断还没有被处理

② 处理器层面,INTR引脚被持续拉高,表示INTR中断请求信号一直有效

③ 产生中断的外设层面,该外设的中断请求寄存器的相关位也会一直为1,表示该中断还没有被处理(该位一般在中断处理程序中被清零)

2.2 NMI中断

1. NMI中断是不可屏蔽中断,即EFLAGS寄存器中的IF位对NMI中断不起屏蔽作用

2. 处理器在响应NMI中断时,不从外部硬件获取中断向量号,而是由处理器固定分配中断向量号。在80386中,NMI的中断向量号为2

3. 由于NMI不可屏蔽,所以仅限于系统出现某种紧急事件时使用。例如系统中出现总线超时、电源故障等

产生不可屏蔽中断的中断源一般通过与非门连接到处理器的NMI引脚,一旦有一个中断源生效,则可以触发处理器的NMI中断

4. NMI处理程序屏蔽另一个NMI

当处理器正在处理一个NMI中断请求的过程中,如果又发生了另一个紧急事件;或者两种紧急事件同时在两条指令之间发生。仅在这两种情况下,一种事件的NMI处理程序可以屏蔽另一个NMI中断请求,从而避免发生中断嵌套。当然,这种情况是极少发生的

3 异常

3.1 异常分类

根据引起异常的程序是否可被恢复,将异常分为故障(Fault)、陷阱(Trap)和终止(Abort)

3.1.1 故障(Fault

1. 故障是在引起异常的指令之前,将异常情况通知给系统的一种异常

2. 当处理器跳转到故障处理程序时,所保存的断点(CS : EIP)指向引起故障的指令。也就是说,在80386中故障是可以重新执行的

一旦在故障的异常处理程序中将导致故障的原因排除,通过iret指令就可以再次执行引起异常的指令

3. 典型的故障示例是缺页异常(Page Fault),操作系统依赖该机制实现虚拟内存的分配

① 当程序访问尚未建立映射的虚拟地址时,会触发缺页异常

② 在缺页异常的处理程序中,会分配物理页,并填充页表建立虚拟地址到物理页的映射(这其中还包括权限检查,以及可能的读取磁盘的操作)

③ 当缺页异常处理程序返回时,会再次执行引起缺页异常的指令,由于此时已经建立映射,指令可以成功执行

4. 故障的检测时机与状态恢复

① 故障的检测可以在一条指令开始之前,也可以在一条指令的执行期间进行

② 如果在指令执行的中间检测到故障,则中止故障指令,并将指令的源操作数恢复为指令执行之前的值。这样在故障恢复后,重新执行故障指令时,就可以有与前一次执行完全相同的输入条件

3.1.2 陷阱(Trap

1. 陷阱是在引起异常指令之后,将异常情况通知给系统的一种异常

2. 当处理器跳转到故障处理程序时,所保存的断点(CS : EIP)指向引起陷阱的指令的下一条应该执行的指令

当陷阱的异常处理程序通过iret指令返回时,则继续执行后面的指令

3. 在一个陷阱通知系统之前,引起陷阱的指令会被执行完成,因为该指令可能修改了寄存器或内存

4. 典型的陷阱示例是调试指令(INT3 / INTO / BOUND)及软中断指令(INT n)

说明:如何理解"下一条应该执行的指令"

① "下一条应该执行的指令"不一定是汇编程序中的下一条指令,例如使用loop前缀的循环指令。在循环条件仍满足的情况下,如果循环指令触发陷阱,则返回后仍然执行循环中的指令

② 之所以提出这点区别,是要说明不能总是能够根据陷阱保存的断点,反推确定产生异常的指令

3.1.3 终止(Abort

1. 终止是系统在出现严重情况(e.g. 硬件故障或系统表中出现非法值)时,通知给系统的一种异常

2. 引起中止的指令是无法确定的,产生终止时,正在执行的程序不能被恢复执行

3. 系统接收到终止异常后,处理程序需要重建各种系统表,并可能需要重新启动操作系统

3.2 异常小结

说明1:某些异常会提供出错码,向异常处理程序提供附加信息

说明2:通过INT n指令触发的软中断属于陷阱类型异常,因此无法通过EFLAGS中的IF位屏蔽

3.2.1 异常0:除法出错

1. 由DIV和IDIV指令触发

2. 产生该故障的场景,

① 除数为0

② 相除后得到的商太大,结果操作数无法容纳

3.2.2 异常1:排错异常

1. 排错异常有故障类型的,也有陷阱类型的。可以通过访问DR6寄存器(Debug Register),确定排错异常的原因及类型

2. 在一条指令中,可以检测不止一个排错异常,并使得DR6中同时有多位被置为1

3. EFLAGS寄存器中的RF位,用于控制是否产生调试异常

RF(Restart Flag)

重启标志位

  1. 用于控制处理器对调试异常(指令断点)的响应
  1. 当RF = 0,则指令断点产生调试异常

当RF = 1,则禁用指令断点产生调试异常

说明:80386调试寄存器简介

80386共有DR0 ~ DR7共8个调试寄存器,其中,

① DR0 ~ DR3可以分别设置4个断点的线性地址

② DR4 ~ DR5保留未使用

③ DR6是断点状态寄存器

④ DR7是断点控制寄存器(包括断点类型、断点长度、断点使能)

3.2.3 异常3:单字节INT3

1. INT3是一条特别的单字节INT n指令,用来支持程序断点

2. INT3指令被看成是一种陷阱,而不是一个中断

3.2.4 异常4:溢出

INTO指令提供的是一个条件陷阱,

1. 如果执行INTO指令时,EFLAGS寄存器中的OF位为1,则产生陷阱

2. 如果执行INTO指令时,EFLAGS寄存器中的OF为为0,则不产生陷阱,继续执行后面的指令

3.2.5 异常5:边界检查

1. 如果BOUND指令发现被测试的值超出了规定的范围,则发生边界检查故障

2. BOUND指令格式如下,

;目的操作数是寄存器,包含数组的索引

;源操作数指向内存位置,那里存储了两个成对出现的字或双字

;分别是数组索引的下限和上限

bound r16, m16

bound r32, m32

3.2.6 异常6:无效操作码

1. 从[CS : EIP]指定的内存位置开始,如果连续一个或多个字节所包含的内容不表示80386指令集中的任何一条指令,则发生无效操作码故障

2. 发生无效操作码故障的场景

① 操作码字段的内容不是合法的80386指令代码

② 指令要求使用存储器操作数,但是使用寄存器操作数

③ 不能被加锁的指令使用了LOCK前缀

3.2.7 异常7:设备不可用

1. 在不包括80387协处理器的系统中,可以通过异常7来软件模拟浮点运算

2. 发生设备不可用故障的场景

① 在执行浮点指令时,CR0中的EM位或TS位为1

② 在执行WAIT指令时,CR0中的TS位及MP位均为1

3.2.8 异常8:双重故障

1. 双重故障是指处理器在检测到一个异常时,已有另一个异常正通知给系统,此时会向系统通知一个双重故障

2. 双重故障属于终止异常,因此断点(CS : EIP)保存的地址不可能指向引起双重故障的指令

3. 双重故障通常指示系统中出现的严重问题,例如段描述符表、页表或中断描述符表出现问题

3.2.9 异常10:无效TSS

1. 当正从TSS中装入选择子时,如果发生除了不存在异常以外的段异常时,则产生无效TSS故障

2. 无效TSS故障会提供一个出错码,包含有引起异常的段选择子

3.2.10 异常11:段不存在

1. 当处理器加载除SS之外的段寄存器时,如果发现要访问的段描述符中P位 =0,则产生段不存在故障

2. 段不存在故障会提供一个出错码,包含有引起异常的段选择子

说明:段异常出错码

① 当出错码中包含段选择子时,直接将引起异常的段选择子的Index字段和TI字段保存在出错码中。如果出错码中不包含段选择子,则Index部分为0

② 当处理某一异常或外部中断时,如果又发生了异常,此时会将EXT位置为1

③ 当从IDT中读取表项并产生异常时,会将IDT位置为1

3.2.11 异常12:栈段异常

当处理器检测到用SS段寄存器寻址发生问题时,产生段故障,分为如下3类,

1. 由SS段寄存器寻址栈段时界限超出范围,将引起段故障,且提供的出错码为0

2. 跨特权级调用(e.g. 调用门)或中断处理中,高特权级栈的界限超出范围,将引起段故障,且出错码包含有高特权级栈的栈选择子

3. 加载SS段寄存器时,如果对应的段描述符P位 = 0,将引起段故障,且出错码包含有引起异常的段选择子

说明1:区分高特权级栈故障与栈段不存在故障

① 高特权级栈故障与栈段不存在故障的出错码均为非零,不能通过出错码直接区分这两种情况

② 通过检查出错码中段选择子对应的段描述符的P位,可以区分上述两种情况

说明2:栈段扩展操作

① 在支持栈段扩展的系统中,如果栈段故障的出错码为0,则表示需要对栈段进行扩展

② 需要注意的是,这里是指分段机制控制的线性地址空间中的栈段扩展

3.2.12 异常13:通用保护

1. 通用保护异常属于故障类型

2. 通用保护异常会提供一个依赖于被检测条件的出错码,以便异常处理程序判断异常类型

3.2.13 异常14:页异常

1. 同时满足如下2个条件时产生页故障,

① 分页机制被启用,即CR0中的PG位为1

② 指令在进行内存访问时,被访问的线性地址所在的页未驻留在内存,或用不适当的访问类型访问内存页(e.g. 对只读内存进行写入操作)

2. 处理器将引起页故障的线性地址保存在CR2,并提供一个出错码,指示引起页故障的存储器访问类型,页异常处理程序中会根据该出错码进行不同的处理

说明1:页故障出错码

① P位表示页异常是否由页不存在导致

  • P = 0,表示页异常由于访问不存在的页导致
  • P = 1,表示页异常由于访问类型违反保护权限导致

② W位表示引起页异常的访问类型

  • W = 0,表示进行读访问
  • W = 1,表示进行写访问

③ U位表示引起页异常的访问特权级

  • U = 0,表示访问来自特权级0、1、2(系统级)执行的程序
  • U = 1,表示访问来自特权级3(用户级)指向的程序

说明2:Linux 0.11页异常处理程序示例

① 进入页异常处理程序时的栈状态

此时栈顶指向页异常错误码

② 页异常处理程序入口

文件:mm/page.s

在调用do_no_page和do_wp_page函数之前,向栈中压入了EDX和EAX寄存器,其中分别保存了引发页异常的线性地址和页异常错误码,他们将作为传递给do_no_page和do_wp_page函数的参数

需要注意的是,按照约定,函数参数从右向左压栈

③ 处理缺页异常

文件:mm/memory.c

④ 处理页保护异常

文件:mm/memory.c

4 中断和异常的优先级

1. 80386所识别的中断及异常优先级如上图所示

2. 当检测到多于一个中断或异常时,将优先级最高的中断或异常通知给系统,其他优先级较低的异常被废弃,而优先级较低的中断则保持pending

5 屏蔽中断和异常

某些条件及处理器标志将引起中断及排错故障被忽略或屏蔽,在这种情况下,中断保持pending,而排错异常被废弃。具体场景如下,

1. 如果EFLAGS寄存器中的IF位为0,则INTR中断被屏蔽

2. 当EFLAGS寄存器中的IF位为0时执行STI指令(也就是开中断),则在STI指令及下一条指令执行期间屏蔽INTR中断

3. 如果EFLAGS寄存器中的RF位为1,则屏蔽排错异常

4. 当系统正在处理一个NMI中断时,屏蔽任何新的NMI中断

5. 以SS寄存器为目的寄存器的MOV或POP指令,将在该指令及下面一条指令的执行期间屏蔽各种中断及排错异常

说明1:设置栈时屏蔽中断和异常

① 80386中设置栈时,需要将栈指针加载到SS和ESP寄存器。如果使用2条指令完成,则需要确保这2条指令的执行不被打断,以确保栈设置的正确性

② 80386中也提供了LSS指令,该指令可以在一条指令中,将存储器中的一个48位全指针分别加载到SS和ESP寄存器

说明2:Linux 0.11使用LSS指令设置栈指针示例

文件:boot/head.s

6 中断和异常的转移方法

6.1 中断描述符表(IDT

在保护模式下,处理器使用的是中断描述符表IDT(Interrupt Descriptor Table)管理中断

1. 中断描述符表共256个表项(与256个异常向量对应),每个描述符8B,因此需要2KB内存

2. 当中断或异常发生时,处理器以中断向量作为索引,从中断描述符表中获取一个8B的门描述符

3. 每个表项是一个门描述符,可以是中断门、陷阱门或任务门

4. 可以存储在线性地址空间中的任何位置,通过IDTR寄存器保存其线性基地址与表界限(与GDT类似)

说明1:中断门和陷阱门描述符只允许安装在IDT内,任务门描述符可以安装在GDT、LDT和IDT中

说明2:中断描述符表寄存器IDTR

① IDTR中存储的是IDT的线性基地址和表界限,16位表界限最大为64KB,但实际最大只使用2KB

② 系统复位时,IDTR的基地址部分为0,表界限部分为0xFFFF

③ 与GDT类似,系统中只有一个IDT

④ 为了提高高速缓存的工作性能,建议IDT的基地址8B对齐

6.2 中断描述符

如上文所述,中断描述符表的每个表项中可以部署中断门、陷阱门或任务门,因此中断描述符的格式就是系统段或门的描述符格式(其中DT位 = 1)

6.2.1 Selector & Offset

1. 包含中断 / 异常处理程序的48位全指针

2. 如果是任务门,则不使用Offset字段,只使用Selector字段记录任务的TSS段选择子

关于任务门描述符,可参考X86汇编语言从实模式到保护模式17:协同式任务切换 chapter 3

6.2.2 P

1. 标识门描述符是否有效,P = 1时,门描述符有效;P = 0时,门描述符无效

2. 使用无效的门描述符将引起异常

6.2.3 DPL

1. 标识门描述符的特权级,用于进行门级检查,此时要求

CPL <= 门描述符DPL

2. 只有使用INT nINTO指令触发异常时,才会进行门级检查,以避免应用程序通过INT n指令使用外部设备的中断向量触发异常

对于其他异常或中断,不进行门级检查,忽略DPL

6.2.4 Type

1. Type字段标识中断描述符类型

2. 如上文所述,部署在中断描述符表中的,只能是下面5种门描述符

说明:在中断门  / 陷阱门  / 任务门描述符中,不使用DwordCount字段(该字段仅在调用门中使用)

6.3 通过中断门和陷阱门转移

转移目的地:当前任务全局部分中的中断 / 异常处理程序

6.3.1 选择中断描述符

1. 当中断或异常发生时,处理器以中断向量作为索引,从中断描述符表中获取一个8B的门描述符

2. 如果索引的中断描述符是一个中断门或陷阱门,则表示将控制转移到当前任务的一个处理程序(此时不会发生任务的切换)

3. 中断门或陷阱门中的Selector字段用于访问GDT或LDT中的段描述符,该描述符必须指向一个可执行的存储器段

门描述符中的Offset字段则标识处理程序入口点在段内的偏移量

6.3.2 门级检查

1. 门级检查仅在使用INT n或INTO指令触发异常时进行,以避免应用程序使用分配给外部设备的中断向量

2. 对于其他的异常或中断,不进行门级检查

6.3.3 段级检查

1. 中断门或陷阱门描述符中的Selector必须指向一个存在的、可执行的存储器段,处理器需要检查该存储器段的描述符中的DPL字段,以确定是否可以跳转

2. 段级检查中,要求只能向同级或更高优先级跳转,即

CPL >= 跳转目标段描述符DPL

6.3.4 特权级与栈切换

1. 如果中断 / 异常处理需要跳转到更高优先级处理,则根据保护模式的要求,所使用的栈段也需要随之切换

2. 新的栈段初值[SS : ESP]从当前TSS段中获取,而旧的栈段值[SS : ESP]被压入新栈

6.3.5 EFLAGS寄存器压栈与设置

1. 当完成特权级与栈的切换后,将当前EFLAGS寄存器值压栈

2. 将当前EFLAGS寄存器中的NT位清零,表示处理器通过IRET指令返回时,是返回到同一任务而不是一个嵌套任务

3. 将当前EFLAGS寄存器中的TF位清零,表示处理器不允许单步执行。这样做是为了避免进入中断 / 异常处理程序后,每执行一条指令都反复触发异常,重新进入异常处理程序

4. 如果是通过中断门进行转移,将当前EFLAGS寄存器的IF位清零,表示处理器屏蔽INTR中断,不允许中断嵌套

如果是通过陷阱门进行转移,则IF位保持不变,从而在异常处理程序中也可以响应中断

说明:除了对IF位的处理不同,中断门和陷阱门是完全相同的。根据该特性,中断门更适合处理中断,陷阱门更适合处理异常

6.3.6 保存返回地址

1. 通过将适当的[CS : EIP]寄存器值压栈,保存中断 / 异常处理程序的返回地址

2. 这里的"适当"是指根据中断 / 异常的特性,压栈当前指令还是下一条应执行指令

6.3.7 控制流转移

1. 将门描述符中的Selector装入CS寄存器

2. 将门描述符中的偏移量装入EIP寄存器

说明:关于门描述符中Selector的RPL字段

① 由于门描述符中的Selector字段是引用一个可执行存储器段的选择子,因此他的最后2位是RPL字段

② 此处的RPL字段的特权级可以低于跳转目标代码段的特权级(e.g. 目标跳转代码段的DPL = 0,但是此处RPL = 3),这样就可以实现对一致性代码段的支持(即在跳转到高特权级代码段时,CPL仍然保持低特权级)

6.3.8 出错码压栈

1. 处理器异常处理的最后一部分,是根据需要将出错码压栈

2. 出错码为16位,但是为了确保栈边界对齐,实际压栈的出错码具有32位的值,其中高16位的值未做定义。出错码压栈后的状态如下图所示,最终的栈顶指向出错码(如果有的话)

3. 出错码由异常处理程序负责出栈

由于出错码并非所有异常都给出,因此在调用IRET指令进行中断返回之前,对于有出错码的异常,异常处理程序需要先从栈中弹出出错码

4. 如果通过任务门处理异常,且对应的异常有出错码,那么该出错码会被压入新任务的栈中

6.3.9 IRET返回

1. 由于在通过中断门或陷阱门进行转移时,EFLAGS寄存器的NT位被清零,所以在中断 / 异常处理程序中调用IRET指令时,将从当前栈的栈顶获取返回信息

因此,为了IRET指令可以正确返回,在进行中断 / 异常处理程序中需要保持栈平衡,并且将错误码弹出

2. IRET指令从栈顶弹出返回地址[CS : EIP],然后弹出EFLAGS寄存器。根据弹出的CS的RPL字段和CPL,判断中断 / 异常返回是否切换了特权级

如果切换了特权级,则再从栈中取出保存的低特权级栈段[SS : ESP],并进行栈段的切换

6.4 通过任务门转移

转移目的地:其他任务

1. 如果通过中断向量索引到的中断描述符是一个任务门,则表示要转移到不同任务的处理程序

2. 通过任务门的转移,会将EFLAGS寄存器的NT位置为1,从而在调用IRET指令时,可以返回嵌套的任务

6.5 通过中断/陷阱门和任务门处理中断/异常的比较

1. 使用中断/陷阱门和使用任务门是两种不同的解题思路,

① 使用中断/陷阱门,是由当前任务之内的一个过程来处理中断/异常

② 使用任务门,是由另外一个任务来处理中断/异常

2. 由当前任务内的过程来处理中断/异常,可以很快地转移到处理程序,但是处理程序需要负责保存和恢复寄存器的内容(即保存和恢复中断现场)

此时在中断/异常处理程序中访问被打断的任务状态比较容易

3. 由另外一个任务来处理中断/异常,要花费较长的时间,但是保存和恢复寄存器内容的开销已作为任务切换的一部分处理

此时在中断/异常处理程序中访问被打断的任务状态比较负责,但是隔离效果较好

说明:中断/异常处理程序的部署

① 一般将中断/异常处理程序部署在任务的全局空间,以便被所有任务共享

② 如果各个任务要求有不同的处理程序,则全局中断/异常处理程序中保存一个各任务处理程序的入口表,并为引起异常的任务调用相应的处理程序

猜你喜欢

转载自blog.csdn.net/chenchengwudi/article/details/124843379
今日推荐