您的位置:首页 > 其它

中断和异常

2011-11-12 14:39 302 查看
中断和异常

按发射中断信号的时机分为“中断”和“异常”

中断(又叫异步中断):由设备的硬件寄存器(定时器、I/O设备)产生,可能在任何时候发出
异常(又叫同步中断):CPU发出的,控制单元只在终止指令执行后,发出

由于程序本身的错误而产生: kernel发信号给进程
由于异常的外部情况而产生: kernel尽量恢复错误,恢复程序执行

中断信号的处理方式:

切换到中断处理时:

只需要保存和设置相关计数器(eip, cs等),而不用进程切换, 所以不用切换硬件上下文。但要保存中断处理要用的部分寄存器。

分紧急部分和不紧急部分

紧急部分:在当前进程中执行,
不紧急部分:稍候在"可延时函数"中执行

中断处理代码必须能够重入,以便能够中断嵌套

内核中有“中断禁用”的临界区,应该尽量小。

中断和异常的产生

中断的产生:

分为

可屏蔽中断 - I/O设备。
不可屏蔽中断 - 一些严重的错误,例如:硬件失败。

一个IRQ(Interrupt ReQuest)代表中断控制器上的一根中断线,和一个中断向量
单CPU:可编程中断控制器(PIC)

中断请求(中断线上的信号)优先级排序。
把中断线变成中断向量, 传给CPU(通过PIC的I/O port)
通过CPU的INTR针,与CPU通信:PIC告知有中断(set INTR),CPU发回ACK (clear INTR)
可disable某一根中断线,但上次中断向量的值会保存。这样可以在一根线上串行处理不同的设备。

多CPU:改进的可编程中断控制器(APIC):

每个CPU内有一个本地的APIC(local APIC)。local APIC内部:

32bit寄存器,
一个内部时钟、一个定时器设备 - 用来产生定时器中断。
两条附加IRQ线 LINT0 LINT1 - 给本地IRQ用

一个额外的APIC负责连接外部中断线(I/O APIC),并与每个local APIC通信。I/O APIC内部:

24跟IRQ线
中断重定位表(24个条目) - 与IRQ线对应。每个条目的内容如下:

描述某个中断向量发给哪个,或哪几个CPU,或怎么选择目标CPU

可编程寄存器
消息单元 (跟locel APIC通信)

选择目标CPU的方式:

静态:指定一次发送给哪个,哪几个,或是所有CPU
动态:由CPU的任务优先级(先考虑,找最低),本地APIC获得IRQ的仲裁优先级确定(后考虑,找最高。得到者变最低,动态变化,轮流获得机会)。

CPU当前任务的优先级,存在local APIC 的TPR寄存器中

每APIC一个ID,CPU可通过它互相发中断(在ICR(中断命令寄存器)中,指定目标APIC的ID和中断向量就可以)。
APIC在单CPU系统也可用

异常的产生

CPU探测到的:

故障(fault):可被修正,存的是当前eip, 需重新执行。如:页错误。

计算

除零
浮点数错误
SIMD浮点异常

指令和内存

操作符无效
操作数地址无效
内存对齐错误

段、页

段越界
无效的TSS
段不存在
页错误
保护异常

其他

二次故障
设备无效
机器检查

陷阱(trap):不用修正,处理完后执行下一条指令。存下一条指令地址。如:调试断点。

调试:当设置了TF位时,(地址在某个指定范围内就触发,debug寄存器存地址范围)
断点:int3指令触发。断点指令
溢出:当OF位设置了时,into指令触发

流产(abort):严重错误(硬件失败,或是系统数据出现错误); 或是CPU控制单元出错,无法保存eip。

处理程序能做的只有发信号终止进程

程序设定的异常:

程序执行引发异常的指令
用于实现系统调用,或调试。

2号异常用于不可屏蔽中断

中断向量号分配[0, 255)

物理IRQ对应中断向量[32, 238]
IBM兼容机, 某些设备的中断必须静态连接到特定的中断线上

中断号分配
0 - 19不可屏蔽的中断和异常
20 - 31intel 保留
32 - 127IRQ
128系统调用用到的软件异常
129-238IRQ
239本地APIC的timer
240本地APIC的热中断 (thermal interrupt)
241 - 250linux扩展
251 - 253CPU之间互发中断消息
254 错误中断,本地APIC探测到一个错误状况
255 本地APIC 伪造中断。告知设备发出了一个被屏蔽的中断
中断异常的硬件处理

中断描述符:

描述符格式:类型(第40-43bit表示类型),段选择符(全局段表中),偏移量, DPL,等。不同类型格式不太一样。

描述符类型如下(默认都是内核态访问):

任务入口:需要替换当前进程的。段选择符位置放的是TSS选择符,没有偏移量
中断入口:控制转移后,清除IF flag(禁止可屏蔽中断)。

用户态可以访问的叫“系统中断入口”:

指令中断号
int33(断点)
陷阱入口:控制转移后,不清除IF flag

用户态可以访问的叫“系统入口”:

指令中断号
into4(溢出)
bound5(地址边界检查))
int $0x80128(系统调用)
地址的内容:是一段汇编代码。内容大致是:

handler_name: /*保存寄存器的汇编代码*/ call do_handle_name /*真正的处理, C函数*/

中断描述符表(IDT):255个描述符,地址在idtr中,lidt指令加载idtr.

设置中断描述符的函数

set_XXX_gate (n, addr)

n:条目索引
addr: 段偏移量。段选择符就是内核代码段。
XXX:有system, DPL位3;否则DPL位0

set_task_gate (n, gdt)

只有一次调用:gdt传31, 最后一个段。专门存放处理二次失败中断的进程TSS

中断和异常的硬件处理

中断进入

每执行完一条指令,检查有无中断

执行完一条指令之后, eip变成下一条,
检查执行前一条指令时有无中断、异常。

若有中断

根据中断向量数和idtr, 找IDT条目;根据IDT里的段选择符,找段描述符。
检查权限:CPL与段DPL,与IDT条目里的DPL比较。(CPL都要被许可)
如果段DPL与CPL不同,栈切换。

tr寄存器存了当前CPU的TSS。
在TSS里,用段DPL找到对应的栈(即选择内核栈还是用户栈,FIXME: 硬件能办到吗);设置ss, esp。
在新栈里存原来的ss, esp。

保存eflags, cs, eip到栈中,以备恢复。

如果是fault(可以恢复的),保存之前先用异常时的cs eip装载cs eip寄存器

如果异常带来一个硬件错误码,存在栈里。
load 中断处理的的cs(段选择符), eip (段选择符和offset),开始处理

中断返回: 用iret指令返回. 控制单元做的事:

从栈中装载cs, eip, eflags, 恢复eip
如果中断进入时发生了栈切换(原来存的CPL(刚从栈中放到cs的),与中断处理段的DPL不相等):

从栈中装载ss, esp以恢复处理前的栈
清除遗留段地址 - 清除权限值比CPL还要低(权力大)的段寄存器;因为权限保护,不能访问。

中断、异常的软件处理

可嵌套 (实质是“内核控制路径(kernel control path)”可以嵌套执行)

可嵌套的前提条件:开始存reg, 最后又恢复了,所以说可以嵌套。
代价:中断处理不能阻塞(中断处理时,不允许进程切换。FIXME 用于分时的定时器中断被禁用了? )。
可行性:

只有处理“页故障”时才会阻塞。而中断处理不会产生“页故障”,因为中断处理程序的代码、数据永远在内存中。
假设内核没有bug,就不会产生异常(“页故障”除外),异常只产生在用户态(程序bug,或调试时)。
只有“页故障”才会阻塞,且处理它时不会再产生异常。所以不会再次引发二次“页故障”

嵌套的好处:

提高吞吐量:先禁用中断线,直到CPU发出ack, 然后启用中断。此时CPU处理剩下的部分(此时可嵌套)。
不用考虑优先级:由于可嵌套,就不用考虑中断优先级,简化了软硬件设计。
多CPU时,如果中断处理被分给一个要切换进程的CPU,可以迁移到其他CPU。

多个内核栈

thread_info编译时8K:所有都在同一个内核栈
thread_info编译时4K:三个内核栈,每个大小是4K

异常栈 - 每进程一个, 即thread_info

thread_info存着后两个栈的基址指针。

硬中断 - 每CPU一个 ,所有CPU的在一个数组理:irq_ctx hardirq_stack [nr_cpu]。 hardirq_ctx[nr_cpu]存着每个栈的基址,用于快速定位

软中断 - 每CPU一个, 所有CPU的在一个数组理:irq_ctx softirq_stack [nr_cpu]。 softirq_ctx[nr_cpu]存着每个栈的基址,用于快速定位。

irq_ctx是一个与thread_info完全一样的的结构

异常的软件处理:

非二次失败异常

存大部分寄存器值到内核栈,调真正的handler(C函数,声明寄存器传参)。细节部分除了“设备无效”异常(FPU、页)外,都相同:

压入异常码,处理handler的地址(两者入栈是为了下面的汇编代码都相同)
保存并设置相应寄存器:

压入handler可能用到的Regs
设置DF,使edi, esi用于string指令(块拷贝等)时会自增(cld指令, clear direction)。FIXME: why?

准备函数地址、变量,即把它们都放入寄存器:

栈内错误码放进edx, 栈内的错误码置为-1.
栈内的hander放入 edi; es放入栈内handler的位置
装载用户数据段(__USER_DS)到ds和es (考虑:上述edi, esi的设置)
栈顶位置-> eax,

调用hander:

call edi(handler), 参数已放入 eax(栈顶位置), edx(错误码) 进入异常的handle

handler的处理方式:

调do_trap()记录异常代码,给current进程发信号; 进程再处理信号(通过处理信号来处理异常),选择修复或流产(MS_DOS模式下,处理不同)。

用异常机制管理硬件资源(需要时加载的策略):

进程切换时, 用“设备无效异常”来实现FPU、MMX、XMM的保存与恢复
用页故障来启动换页。

以jmp到汇编函数ret_from_exception的方式 返回

二次失败异常,用task_gate处理

严重错误,esp以无效。
处理时,load TSS内的eip, esp。在TSS自己的栈上执行doublefault_fn()
FIXME: 当前进程怎么办?

处理没实现时,用ignore_init替代:

保存一些寄存器,printk "未知中断"(实际上现在还不能执行,因为打印到console, 还是写到日志文件都需要处理设备中断)。
恢复寄存器,执行iret.

如果内核态出异常:

系统调用参数错误 - 后面的章节介绍
内核的bug - 打印所有Reg值,退出进程

中断的软件处理

特点

中断到得很晚,与当前进程无关 - 不能用信号机制。 中断处理分成三部分:

紧急部分:

发ACK,重新编程PIC、设备控制器,操作与CPU、设备都关联的数据。
要立即完成。过程中,中断禁用。

不紧急部分: 仅与CPU相关,可以快速完成。过程中,中断使能。
可延时部分: 用于向进程(不一定是当前进程)的用户空间拷贝数据。可被延时较长时间。

IRQ少,设备多

IRQ共享。

每个设备一个服务程序。
来了中断,IRQ上的所有设备服务程序要遍历。

IRQ动态分配

程序需要哪个设备,临时给哪个设备分配IRQ。

给一个设备分配IRQ

硬件跳线
安装时运行程序分配(询问用户或系统)。
根据硬件协议,设备声明用那根,系统根据情况分配一个。handle通过设备的I/O port,获得分配到的IRQ。

四个基本动作:

保存IRQ,和寄存器到内核栈
发ACK到PIC,使能中断
执行该IRQ上的所有ISR。
跳到ret_from_inir 结束。

数据结构

每个IRQ一个链表,每个节点内是某个设备的中断服务程序(ISR); 所有链表在一个数组irq_desc里。
链表头的内容:

PIC相关的:

handler : 指向相关的PIC对象。PIC对象:对硬件PIC封装而成的一个数据结构,使在编写驱动时不用考虑PIC硬件差异。

名字: name
方法:

startup, shundown
enable disable

ack - 给PIC发ACK
end - 告诉PIC处理完成

set_affinity - 多CPU时,该PIC倾向处理某些特定的IRQ

handler_data :PIC对象里的函数用到的数据
action:这个IRQ上的所有ISR, 以链表形式。每一个节点对应一个设备,节点的结构irq_action:

name - 设备名
dev_id - 设备号 (主设备号,副设备号)
hander - ISR

irq - irq线号
dir - irq线所在路径/proc/irq/n
flags - 描述IRQ线与设备的关系

SA_INERRUPR - 该handler, 不可被中断
SA_SHIRQ - 该设备允许IRQ共享
SA_SAMPLE_RADOM - 该设备可以所谓随机数发生器。(FIXME 跟中断有什么关系)

mask - 没用
next - 下一个节点指针

多CPU合作: lock, 互斥锁
状态位的组合: status

IRQ_INPROGRESS: 正被处理
IRQ_DISABLE - 该IRQ被禁用了
IRQ_PENDING - 发回ACK了,未服务
IRQ_REPLAY - 当该IRQ刚要被处理,还未上锁时,被其他CPU禁用了。然后被另外的程序手动置成该状态, 表示重发

IRQ_AUTODETCT -
IRQ_WAITING -

IRQ_LEVEL:x86架构中不用
IRQ_PER_CPU:x86架构中不用
IRQ_MASKED:不用

depth:IRQ被禁用的层次

disable_irq()时, depth++,enable_irq()时, depth--。
遇到0时,才真正disable或enable IRQ。 0位,使能状态。

统计信息,用来防止意外中断:

irq_count:中断发生总次数
irqs_unhandled:没处理中断次数。IRQ上的ISR都不识别,或是该IRQ上没有ISR
如果某个IRQ线,总有意外中断,禁用该IRQ线。
根据未处理的中断次数,和中断总数来判断。例如每100,000个中断,有99000是没有处理的,就禁用改中断线。

中断处理:

保存寄存器

入栈:向量减去256。 (大于256的向量是系统调用)
要保护的寄存器入栈 (再加上下步“装载用户数据” 组成:SAVE_ALL宏)

装载用户数据段

(__USER_DS) 放到 ds和es

准备参数: 栈指针 -> eax

eax既是栈指针,又指出了保存的寄存器的值

调用do_IRQ (参数在eax中): 起封装“栈切换”的作用

嵌套层次++;(thread_info.preempt_count)
如果内核栈是4K,检查是否切到了硬件中断栈。如果没有,切换到硬件中断栈。

检查:通过esp确定当前使用的内核栈基地址 ( current_thread_info() ), 再与hardirq_ctx存的地址比较。
栈切换:存当前的pd, esp到irq_ctx中, 装入irq_ctx对应的esp.

调用__do_IRQ() (解决多CPU问题,每次改变status都有加锁)

清除设置相关位。判断有无其他CPU正在处理当前IRQ上的中断。

如果有了,当前交由那个CPU处理,本CPU仅设置pending位,不处理。
如果没有,本CPU反复调用handle_IRQ_event()处理,直到没有CPU再设置pending(把处理交给自己)。handle_IRQ_event:

如果该IRQ可以被中断,使能中断(sti指令);
链表上所有的handler都执行;
禁中断(cli指令);
返回是否有handler成功执行(用来统计无效中断)

如果栈切换了,切回原来的栈
执行irq_exit宏:减少嵌套层次,执行可延迟函数
用ret_from_intr()结束中断处理

遗漏的中断

某个CPU刚选中某个中断,还未上锁。就被另一个CPU禁用中断了。前者只设置pending,没有进入处理的循环
enable_irq()的时候,如果pending了但还没有重发(replay位0);就设置replay位,让本地APIC给自己发中断

中断分类:I/O中断, timer, CPU之间

CPU间的三个中断(IPI):

向量号(_VECTOR左边的是中断名字)目标CPU作用接收者的动作附加说明
CALL_FUNCTION_VECTOR(0xfb)除发出者外的所有CPU使接收者执行某一个函数发回ack, 执行函数函数指针call_data里
RESCHEDUELE_VECTOR(0xfc)某一个,某几个CPU使其调度发回ack, 调度
INVALID_TLB_VECTOR(0xfd)所有CPU强制其TLB无效
发射函数: send_IPI_xyz

目标CPU所有除了自己自己指定
函数xyzallallbutselfselfmaks
处理:BUILD_INTERRUPT宏,内部调C函数smp_name_interrupt() (name是中断名字)

软件上IRQ线的分配(在链表里插入节点):

创建一个irq_action节点:request_irq(irq_num, ISR, flags, name, ......)
插入到相应链表: setup_irq().

检查已有节点是否有SA_SHIRQ属性
插入链
如果是该链上第一个节点,运行PIC对象的setup方法,使能这个IRQ号

释放一个节点(FIXME: 链表上所有节点?):free_irq

irq信号在多CPU间分配

对称多处理:

每个CPU执行中断处理时,时间片相同。
每个CPU轮流得到irq信号.

硬件支持仲裁优先级变换:

local APIC 初始化,任务优先级都相同。
仲裁优先级不断变换,实现轮流得到irq

如果硬件不支持仲裁优先级变换:

用修改I/O APIC的重定向表实现。由kirqd 线程实现, 根据irq_stat来平衡。

调用set_ioapic_affinity_irq(n, mask),修改重定向表。(用户可通过/proc/irq/n/smp_affinity修改)

n:中断向量
32bit mask:哪几个CPU可以收到irq。

类型位irq_stat的irq_cpustat_t数组,每个CPU一个元素。保存每个CPU最近处理irq的情况。用于平衡每个CPU的irq处理。

__softirq_pending: 悬挂的软中断个数
idle_timestamp:只有CPU当前是idle时才有意义,开始idle的时间
__nmi_count:处理nmi中断的次数
apic_timer_irqs:处理局部apic 时间中断的次数

链表数组初始化 init_irq():

所有的IRQ都置成IRQ_DISABLE状态。用中段函数设置中断入口(中断描述表里的,从32开始)。

softirq和tasklet

可延迟函数

ISR要求必须串行执行,而且经常需要中途无中断发生;但是可延迟任务中途允许中断。
中断上下文: 中断handle和softirq
分类:

可延时函数包括softirq和tasklet。
在数据结构上,tasklets在softirq的结构内部,所有经常统称softirq.

softirq:

静态分配,即编译时就确定了。
同类可并行,可同时在多CPU执行

自旋锁保护临界区

可重入。

tasklet反之。使驱动编写简单。

基本操作:

初始化:定义一个新的可延迟函数。一般在kernel初始化,或添加一个模块的时候用
激活:设一个可延迟函数的状态为pending. 表示希望被执行
屏蔽:disable一个可延迟函数
执行:在某个检查点,检查有无pending的函数,并执行它

对于一个可延迟函数,执行它的CPU必须是激活它的CPU。为了硬件cache考虑

softirq

数据结构:softirq_action softirq_vec [32], 32元素的数组,执行时从0开始,0的优先级最高。

action: 函数指针
data: 通用指针,action的参数

设置,即把一个函数设置在softirq_vec的一个元素:open_softirq(indx, action, data)
激活,raise_softirq(index).

设置本softirq的pending位,检查thread_info.preempt_count中的softirq次数与hardirq次数,是否都是零。

thread_info.preempt_count 。四个数的位或

抢占次数:抢占被禁用次数
softirq次数:softirq被禁用次数
hardirq次数:irq处理嵌套次数
PREEMP_ACTIVE标志位
方便:只要检查preempt_count==0, 就可判断是否内核可抢占。

多内核栈时,用irq_ctx内的preempt_count, 但它一般是个正数。FIXME: 那何时激活?

如果都为零,说明不嵌套在中断上下文中。调用wakeup_softirqd()来唤醒执行软中断的内核线程ksoftirqd

内核线程ksoftirqd/n (n指某个CPU, ksoftirqd每个CPU一个,FIXME: 且可以迁移),不断检查,执行。
其他softirq的执行点(只执行一次):

使能本地CPU的softirq:local_bh_enable()
中断处理完成后:

do_IRQ完成处理后:irq_exit宏内
处理完本地timer中断后
处理完IPI: CALL_FUNCTION_VECTOR以后

raise_softirq的开始、结束时要禁止中断,恢复中断

执行

一次执行: do_softirq()

前期准备:检查preempt_count, 禁止中断。如果时多内核栈,且不再softirq栈,切到softirq栈
__soft_irq真正执行:
后期收尾:恢复以前的栈,恢复中断

softirqd不断检查、执行

softirqd不断被唤醒;反复检查有无激活的函数并执行;直到没有pending的函数,进入可中断睡眠。
每次执行调do_softirq(前后要禁用、启用抢占); 然后cond_resched(如果current thread_info的TIF_NEED_RESCHED为1,就执行调度)

tasklet

通过链表组织的可延迟函数。
每CPU一个链表,所有链表在链表数组里。按优先级分成两个链表数组(优先级高函数有"hi")。
softirq_vec的前两个元素,保存链表指针和遍历链表的函数指针
节点tasklet_struct:

state: 2个位:

TASKLET_STATE_SCHED: 表示pending
TASKLET_STATE_RUN: 表明正在执行,多CPU时才有用

count:禁用次数。FIXME: 为什么其他softirq没有单独的count
func
data
next

接口:

分配一个节点:tasklet_init()
禁用、使能: tasklet_disable_nosyns, tasklet_disable, tasklet_enable
激活tasklet_schedule、tasklet_hi_schedule

设置pending位,添加到相应表头,激活softirq_vec中的相应元素。后两步要禁用本地中断

执行:就是softirq_vec里前两个元素的函数是tasklet_hi_action, tasklet_action

拷贝清空本地CPU的链表头指针,禁中断下进行
遍历链表:

TASKLET_STATE_RUN的节点:表示现在有其他CPU在执行这个函数。在链表数组里本地CPU的位置,重新插入该节点。
count>0的节点:被禁用了,重新插入该节点
count<=0的节点:清除pending位,执行函数。注意:每个函数最多执行一次,执行完了不再插入列队

work queue(工作列队)

内核函数被激活,稍候被特殊的内核线程(worker thread)执行。
与可延迟函数的区别:工作列队里的函数可以阻塞
数据结构

一个workqueue_struct,包含一个多CPU数组。每一个元素是一个链表头(cpu_workqueue_struct):

lock
wq:上级指针
worklist: 函数链表。节点结构如下:

pending:
entry: 内嵌链表
wq_data: 上级指针
timer: 用于延迟插入的软件定时器
func: 函数指针
data: 通用指针,函数func的参数。

thread:执行该链表函数的工作线程
run_depth:
more_work:等待列队,等待新的函数的工作线程阻塞在这里
work_done:等待列队, 等待工作列队flush完毕的进程阻塞在这里
remove_sequence:用于判断哪些哪些函数是flush以后才来的。
insert_sequence:用于判断哪些哪些函数是flush以后才来的。

接口

创建工作列队:

create_workqueue: 一共NR_CPU个工作线程,每个链表一个。每个线程都可以到任意的CPU执行
create_singlethread_workqueue:只有一个工作线程。

插入一个函数:

queue_work():把一个节点插入列队,设置pending位。(插入到local CPU的链表)
queue_delayed_work():多一个timer参数,延迟插入。(设置一个timer,定时器到了再插入)
cancel_delayed_work(): 取消插入,必须在实际插入之前调用。

执行:

每一个工作线程都进入worker_thread()内部的一个循环中。
大部分时间睡眠,有函数时唤醒,唤醒后调run_workqueue(摘下该线程对应链表的所有的节点,执行里面的pending函数)

等待执行完:

flush_work_queue: 阻塞当前进程,直到所有的pending函数执行完毕。
但不包括新来的函数。用remove_sequece, insert_sequece两个计数判断哪些是新来的函数

内核预定义的工作列队events: 包含不同kernel层的函数和I/O驱动。 在keventd_wq数组里存着不同的工作列队

从中断和异常中返回

如果内核支持抢占,那么返回时禁中断

中断处理末尾就是禁中断的,所以从异常返回时要首先禁中断。

如果有嵌套的KCP(kernel control path),且不是虚拟8086模式,则恢复kernel。否则恢复用户空间

判断栈中保存的cs的权限位,和eflags的VM位
恢复到内核后:

在允许抢占(thread_info.preempt_count==0)的情况下: 如果以下两个条件都满足,执行抢占调度让出CPU(preempt_schedule_irq)。否则恢复上层KCP

有等待调度的进程(current->thread_info.flags的TIF_NEED_RESCHED被设置了)。
上层要恢复的KCP允许中断(本级是异常时,才有可能“要恢复的KCP不允许中断”)。
被抢占,再次获得CPU后,要重新检查上述两条件

恢复到用户空间:

检查有无剩余事情要做(调度、恢复虚拟8086状态,悬挂信号、恢复单步执行,):

如果有等待调度的进程,调度(schedule())。再次获得CPU后,重新检查。直到没有新来的调度请求了。
如果要恢复虚拟8086状态,在用户空间建立相应的数据结构(FIXME: 以后研究)
处理其他的剩余事情(悬挂信号、恢复单步执行)。

恢复:

SAVE_ALL保存的寄存器出栈。
iret指令。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: