Intel x86 - 中断服务例程责任

Intel x86 - Interrupt Service Routine responsibility

我没有真正意义上的问题,但我会尝试澄清一个内容问题。假设我们有一个微内核(PC Intel x86;32 位保护模式),其工作 中断描述符 Table (IDT) 中断服务例程 (ISR) 每个 CPU 异常。 ISR 被成功调用,例如 Division by Zero 异常。

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

问题是一次又一次地抛出中断。结果,ISR 被一次又一次地调用。通过反复试验,我发现引发异常的那一行, int x = 5 / 0 在循环中执行,因此 指令指针 (EIP) 不会递增

当我增加手动推送到堆栈的 IP 值时,出现了预期的行为。 CPU 执行恶意代码行之后的下一条指令。当然是调用一次ISR之后。

针对我的实际问题:ISR 是否有必要增加 IP?或者这是"CPU/Hardware"的责任?继续前进的正确行为是什么?

When I increment IP's value pushed to stack manually, the expected behavior occurs.

这不是预期的行为。异常 可以 被视为 严重故障 需要终止程序。因此,简单地恢复业务通常不是一种选择。

Is it necessary that the ISR increments the IP?

没有。通常,进程以 "General protection fault" 或 "Division by zero error" 或类似的方式终止。

Or is this the responsibility of the "CPU/Hardware"?

如果你想在某处继续执行代码(比如 SEH(结构化异常处理),你的 OS 必须管理它。你可以随时这样做,这是你的选择清理可能的混乱。

What's the correct behavior to move on?

正确的行为是您喜欢的样子,因为您是 OS 设计师,不是吗? ;-) CPU/hardware 只是通知您当前状态。

Intel64 和 IA-32 架构软件开发人员手册第 3 卷(3A、3B、3C 和 3D):系统编程指南,第 6.5 章异常分类 说:

Faults A fault is an exception that can generally be corrected and that, once corrected, allows the program to be restarted with no loss of continuity. When a fault is reported, the processor restores the machine state to the state prior to the beginning of execution of the faulting instruction. The return address (saved contents of the CS and EIP registers) for the fault handler points to the faulting instruction, rather than to the instruction following the faulting instruction.

虽然通常无法更正被零除,但Table 6-1。保护模式异常和中断 仍然表明 cpu 设计者决定 #DE Divide 错误 应该是故障类型异常。

您有责任了解处理器如何以及为何调用您的中断服务例程,并相应地为您的 ISR 编写代码。您正在尝试将除以零错误生成的异常视为由硬件中断生成。然而,这不是 Intel x86 处理器处理此类异常的方式。

x86 处理器如何处理中断和异常

有几种不同类型的事件会导致处理器调用中断向量 table 中给出的中断服务例程。这些统称为中断和异常,处理器可以通过三种不同的方式处理中断或异常,作为 fault,作为 trap ,或作为 abort。您的除法指令会生成一个除法错误 (#DE) 异常,该异常将作为故障处理。硬件和软件中断作为陷阱处理,而其他类型的异常作为这三种方式之一处理,具体取决于异常的来源。

故障

如果异常的性质允许以某种方式更正异常,则处理器会将异常作为故障处理。因此,压入堆栈的 return 地址指向产生异常的指令,因此故障处理程序知道导致故障的确切指令,并使得在修复错误指令后恢复执行故障指令成为可能问题。 Page Fault (#PF) 异常就是一个很好的例子。它可用于通过让故障处理程序为故障指令尝试访问的地址提供有效的虚拟映射来实现虚拟内存。有了有效的页面映射,就可以恢复和执行指令,而不会产生另一个页面错误。

陷阱

中断和某些类型的异常,所有这些都是软件异常,都作为陷阱处理。陷阱并不意味着在执行指令时出现错误。硬件中断发生在指令执行之间,软件中断和某些软件异常有效地模仿了这种行为。陷阱是通过推送下一条正常执行的指令的地址来处理的。这允许陷阱处理程序恢复中断代码的正常执行。

中止

严重且不可恢复的错误将作为中止处理。只有两个异常会生成中止,机器检查 (#MC) 异常和双重故障 (#DF)。机器检查指令是检测到处理器本身硬件故障的结果,无法修复,也无法可靠地恢复正常执行。当在处理中断或异常期间发生异常时,会发生双重故障异常。这使得 CPU 处于不一致状态,处于调用 ISR 的所有许多必要步骤中间的某个位置,无法恢复。压入堆栈的 return 值可能与导致中止的原因无关。

通常如何处理除法错误异常

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

请注意,虽然您可以通过 "incrementing EIP" 处理错误异常,但这比您想象的要难,而且不会产生非常有用的结果。你不能只向 EIP 添加一个或一些其他常量值,你需要跳过整个指令,它可能是 2 到 15 个字节长的任何地方。有可能导致此异常的三个指令,AAM、DIV 和 IDIV,这些指令可以使用各种前缀和操作数字节进行编码。您需要解码指令以确定它有多长。执行此递增的结果就像从未执行过指令一样。错误指令不会计算出有意义的值,您也不会知道程序运行不正常的原因。

阅读文档

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

What's the correct behavior to move on?

让我们谈谈程序员检测(然后修复)错误的能力。从最好到最差(或 "how quickly programmers find out about the mistake" 的顺序),选项是:

  • 在程序员输入源代码时检测错误

  • 在 compile/link 时间检测到错误

  • 在 运行-time

  • 检测到错误
  • 在花了 3 个月的时间试图弄清楚为什么您会收到来自最终用户的一波敌对 "your software is dodgy trash" 电子邮件(其中没有任何有用的线索)后检测到错误

对于除以零的整数,在程序员输入时检测错误将需要一种语言并且 IDE 专门为此目的而设计(这对于大多数现有语言来说是不切实际的);即便如此,它也不能 100% 有效(例如,错误可能在编译器中,而不是在程序员的源代码中)。在 compile/link 时间检测到错误也有类似的问题。

这意味着 "least worst practical option" 正在 运行 时检测到错误。

但是;检测错误只是第一步 - 例如如果当 random/unknown 最终用户 运行 在英国的笔记本电脑上使用软件并且开发人员在美国时检测到错误,开发人员如何获得他们修复错误所需的信息?

理想情况下;你想要某种自动化系统,其中(在有问题的进程中的所有线程都停止之后,但在进程终止之前)所有相关信息(错误发生在哪个程序的哪个版本,以及寄存器内容等) ) 被收集,然后用 "do you want to submit this info as a bug report" 对话框提示最终用户,然后(如果用户同意)将信息转发到某种允许跟踪统计信息的 "bug collection database"(以便开发人员可以确定错误发生的频率、错误是否仅发生在使用特定版本的人身上、错误是否仅在人们使用特定功能时发生等)。

注意:80x86 上的"divide error" 表示溢出,而不是被零除(被零除只是溢出的原因之一)。例如,如果使用 DIV 指令除以 64 位整数并得到 32 位结果;然后是“0x0123456789ABCDEF / 3 = 除法错误异常”,因为结果不适合 32 位。