Intel x86 - 中断服务例程的职责

4

我并没有实际问题,而是希望澄清一下内容问题。假设我们有一个微内核(PC Intel x86;32位受保护模式),它具有工作的中断描述符表(IDT)和每个CPU异常的中断服务例程(ISR)。当出现除以零异常时,成功调用了ISR。

global ir0
extern isr_handler

isr0:

    cli
    push 0x00   ; Dummy error code
    push %1     ; Interrupt number

    jmp isr_exc_handler

isr_exc_handler:

; Save the current processor state

    pusha

    mov ax, ds
    push eax

    mov ax, 0x10 ; Load kernel data segment descriptor
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax

    ; Push current stack pointer

    mov eax, esp
    push eax

    call isr_handler ; Additional C/C++ handler function

    pop eax     ; Remove pushed stack pointer

    pop ebx     ; Restore original data segment descriptor
    mov ds, bx
    mov es, bx
    mov fs, bx
    mov gs, bx

    popa

    add esp, 0x08 ; Clean up pushed error code and ISR number
    sti

    iret

问题在于中断一再发生。结果是,中断服务程序被一遍又一遍地调用。通过试错,我发现触发异常的代码行int x = 5 / 0在循环中执行,因此指令指针(EIP)没有递增
当我手动递增压入堆栈的IP值时,就会出现预期的行为。CPU随后执行恶意代码行之后的下一条指令。当然,在ISR被调用一次后。
我的实际问题是:ISR是否需要递增IP?还是这是“CPU/硬件”的责任?正确的移动方式是什么?

2
由于这是一个异常,前面的EIP指向了错误指令,如果你想继续执行,你必须自己更改它。硬件不会对此做任何事情。通常代码将被终止,因此没有理由修改它,而将错误发生的实际位置指向指针更有用。 - Sami Kuhmonen
4个回答

5
你需要了解处理器如何调用中断服务例程以及编写相应的ISR代码。你试图将由零除错误生成的异常视为由硬件中断生成的异常,但这不是Intel x86处理器处理这些异常的方式。
x86处理器如何处理中断和异常
有几种不同类型的事件会导致处理器在中断向量表中给出的中断服务例程中调用。这些事件被统称为中断和异常,并且处理器可以通过三种不同的方式处理中断或异常,即作为故障(fault)、陷阱(trap)或终止(abort)。您的除法指令会生成一个Divide Error (#DE)异常,这被处理为故障。硬件和软件中断被处理为陷阱,而其他类型的异常根据异常来源而采取这三种方式之一进行处理。
故障
如果异常的性质允许以某种方式进行纠正,则处理器将其处理为故障。因此,在堆栈上推送的返回地址指向生成异常的指令,以便故障处理程序知道导致故障的确切指令并使得在修复问题后可以恢复执行故障指令。页面故障(#PF)异常就是很好的例子。它可以通过使故障处理程序为故障指令尝试访问的地址提供有效的虚拟映射来实现虚拟内存。在放置了有效页映射之后,可以恢复并执行该指令而不会生成另一个页面故障。
陷阱
中断和某些类型的异常(所有这些都是软件异常)被处理为陷阱。陷阱不意味着执行指令时出现错误。硬件中断发生在执行指令之间,而软件中断和某些软件异常有效地模拟了这种行为。陷阱通过将本来要执行的下一条指令的地址推入堆栈来进行处理。这使得陷阱处理程序可以恢复被中断代码的正常执行。
终止
严重且无法恢复的错误被视为终止处理。只有两个异常会生成终止处理,即机器检查(#MC)异常和双重故障(#DF)异常。机器检查指令是检测到处理器本身的硬件故障的结果,这无法修复,并且无法可靠地恢复正常执行。双重故障异常发生在处理中断或异常时发生异常。这将使CPU处于不一致状态,在调用ISR所需的许多必要步骤的中间位置,其中一个步骤无法恢复。推送到堆栈上的返回值可能与导致终止的任何内容都没有关系。
零除错误异常通常如何处理

通常情况下,大多数操作系统通过将除法错误异常传递给执行进程中的处理程序来处理,如果无法处理,则终止该进程,并指示其已崩溃。例如,大多数Unix系统向进程发送SIGFPE信号,而Windows使用其结构化异常处理机制进行类似操作。这样,进程的编程语言运行时就可以设置自己的处理程序,以实现所使用的编程语言所需的任何行为。由于在C和C ++中除以零会导致未定义行为,因此崩溃是可接受的行为,因此这些语言通常不安装除以零处理程序。

请注意,虽然您可以通过“增加EIP”来处理除法错误异常,但这比您想象的要困难,并且不会产生非常有用的结果。您不能只将1或其他常量值添加到EIP,而需要跳过长度为2到15个字节的整个指令。有三个指令可以导致此异常,即AAM、DIV和IDIV,它们可以使用各种前缀和操作数字节进行编码。您需要解码指令以确定其长度。执行此递增的结果将与从未执行指令一样。故障指令将不计算有意义的值,您将没有任何迹象表明程序的行为不正确。

阅读文档

如果您正在编写自己的操作系统,则需要准备好Intel软件开发人员手册,以便经常参考。特别是,您需要阅读并学习第3卷:系统编程指南中的几乎所有内容,不包括虚拟机扩展章节和其后面的所有内容。该手册详细介绍了关于中断和异常的所有知识,以及您需要了解的许多其他内容。


4
当我手动增加IP的值并将其推入堆栈时,会发生预期的行为。
那不是预期的行为。异常可以被认为是一种严重的故障,需要终止程序。因此,简单地返回正常状态通常不是一个选项。
ISR是否需要增加IP呢?
不需要。通常情况下,进程会以“General protection fault”或“Division by zero error”等形式终止。
还是由“CPU/硬件”负责?
如果你想在某个地方继续执行代码(比如SEH(结构化异常处理)),你的操作系统必须管理这个过程。你总是可以这样做,清理可能出现的混乱是你自己的选择。
正确的移动方式是什么?
正确的移动方式是你喜欢的方式,因为你是操作系统设计者,对吧?;-)CPU/硬件只是通知您当前状态。

理论上,ISR 可以找到一种纠正除 0 错误并继续代码执行的方法,例如在简单的嵌入式代码中,必须不停止执行,但实际上更容易事先检查 0 除数并采取补救措施。如果您没有这样做,则情况是意外的,并且捕获了一个错误,而不是以某种方式继续执行。 - Weather Vane
@WeatherVane:我实际上思考了一下这个问题:如何(数学上)证明零除以零是可以的,并将其作为操作系统的合法部分。但目前我无法阐述,因为我不记得了。抱歉。 - zx485
没问题,我只是想补充你的回答并点赞了它。 - Weather Vane
操作系统过于通用,无法针对特定实例进行处理 - 当故障出现在它不了解的应用程序中时。 - Weather Vane

4
这是《Intel64和IA-32架构软件开发人员手册第3卷(3A、3B、3C和3D):系统编程指南》第6.5章“异常分类”中的内容:
故障(Faults)是一种通常可以纠正的异常,纠正后允许程序重新启动而不会丢失连续性。当报告故障时,处理器将机器状态恢复到执行故障指令之前的状态。故障处理程序的返回地址(CS和EIP寄存器的保存内容)指向故障指令,而不是指向故障指令后面的指令。
尽管除零错误通常无法纠正,但《表6-1:保护模式下的异常和中断》仍然显示CPU设计者决定将#DE除法错误作为故障类型异常。

2
除以零错误可以进行修正,例如通过调整寄存器内容使得除法得出0或其他期望的值。 - fuz
@fuz:对于整数除法,无法进行正确的校正。您只能允许“错误校正”的值传播,同时使识别错误原因变得更加困难。对于浮点数,有更多的选项(“信号NaN”,无限大),但仍然没有“始终正确”的选项。 - Brendan

1
"正确的行为是什么?"
让我们谈一下程序员发现(并修复)错误的能力。按照最好到最差(或按“程序员多快发现错误”排序),选项如下:
- 在程序员输入源代码时检测到错误 - 在编译/链接时检测到错误 - 在运行时检测到错误 - 在花费3个月时间试图弄清楚为什么会收到一大堆充满敌意的“您的软件垃圾”的电子邮件(不包含任何有用的线索)之后检测到错误
对于整数除以零,要在程序员输入源代码时检测到错误需要使用专门设计用于此目的的语言和IDE(这对于大多数现有语言来说不太实际);即使如此也无法100%有效(例如错误可能在编译器中而不是程序员的源代码中)。在编译/链接阶段检测到错误存在类似的问题。
这意味着“最不糟糕的实际选项”是在运行时检测到错误。
然而,发现错误只是第一步 - 例如,如果在英国的笔记本电脑上随机/未知的最终用户运行软件时检测到错误,并且开发人员在美国,那么开发人员如何获取他们需要修复错误的信息?
理想情况下,您希望有某种自动化系统,在所有线程停止但进程终止之前,收集所有相关信息(包括错误发生的程序版本和版本,以及寄存器内容等),然后提示最终用户使用“您要将此信息提交为错误报告”对话框,然后(如果用户同意)将信息转发给某种“错误收集数据库”,该数据库允许跟踪统计数据(以便开发人员确定诸如错误发生频率,错误是否仅在使用特定版本的人中发生,错误是否仅在使用某个功能时发生等)。
注意:在80x86上,“divide error”表示溢出,而不是除以零(除以零只是溢出的一个原因)。例如,如果使用DIV指令来将64位整数除以并得到32位结果,则“0x0123456789ABCDEF / 3 = divide error exception”,因为结果无法适应32位。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接