为什么在 286 上尝试处理异常时会出现三重错误,但在现代 CPU 或 Bochs 上却没有?

Why do I get triple fault when trying to handle an exception on 286 but not on a modern CPU nor Bochs?

我正在尝试在 AMD 286 系统上使用异常处理来初始化保护模式。我已经在 Bochs 上调试了下面的代码,它在那里运行良好。在 Pentium 4 机器上 运行 时也是如此。但是在 286 上,当它到达 int3 指令时,它只会出现三重故障。可观察到的行为是:如果我注释掉 int3,我会无限期地在屏幕上显示 "OK",而按原样使用代码,系统会重新启动。

代码将被FASM编译,二进制文件放入HDD或FDD的引导扇区。我实际上是从一张 1.4M 的软盘 运行 中下载它的。

 org 0x7c00
 use16

 CODE_SELECTOR     = code_descr - gdt
 DATA_SELECTOR     = data_descr - gdt

    ; print "OK" on the screen to see that we've actually started
    push     0xb800
    pop      es
    xor      di,di
    mov      ax, 0x0700+'O'
    stosw
    mov      ax, 0x0700+'K'
    stosw
    ; clear the rest of the screen
    mov      cx, 80*25*2-2
    mov      ax, 0x0720
    rep stosw

    lgdt     [cs:gdtr]
    cli
    smsw     ax
    or       al, 1
    lmsw     ax
    jmp      CODE_SELECTOR:enterPM
enterPM:
    lidt     [idtr]
    mov      cx, DATA_SELECTOR
    mov      es, cx
    mov      ss, cx
    mov      ds, cx

    int3     ; cause an exception
    jmp      $

intHandler:
    jmp      $

gdt:
    dq       0
data_descr:
    dw       0xffff     ; limit
    dw       0x0000     ; base 15:0
    db       0x00       ; base 23:16
    db       10010011b  ; present, ring0, non-system, data, extending upwards, writable, accessed
    dw       0          ; reserved on 286
code_descr:
    dw       0xffff     ; limit
    dw       0x0000     ; base 15:0
    db       0x00       ; base 23:16
    db       10011011b  ; present, ring0, non-system, code, non-conforming, readable, accessed
    dw       0          ; reserved on 286

gdtr:
    dw       gdtr-gdt-1
 gdtBase:
    dd       gdt

idt:
 rept 14
 {
    dw       intHandler
    dw       CODE_SELECTOR
    db       0
    db       11100111b    ; present, ring3, system, 16-bit trap gate
    dw       0            ; reserved on 286
 }
idtr:
    dw       idtr-idt-1
 idtBase:
    dd       idt

finish:
    db       (0x7dfe-finish) dup(0)
    dw       0xaa55

我想我正在使用一些 286 不支持的 CPU 功能,但究竟是什么以及在哪里?

  • 在你的保护模式代码中你有:

    lidt     [idtr]
    mov      cx, DATA_SELECTOR
    mov      es, cx
    mov      ss, cx
    mov      ds, cx
    

    这依赖于 DS 在进入保护模式之前被设置为 0x0000(并且在 DS 描述符中相应的基地址为 0缓存)在执行 lidt [idtr] 之前。该指令有一个隐含的 DS 段。在使用 16 位选择器设置段寄存器之后放置 lidt 指令,而不是之前。

  • 虽然它并没有在您的硬件上表现为一个错误,但在实模式下您的代码还依赖于 CS 被设置为 0x0000 的指令lgdt [cs:gdtr]。不能保证 CS 为 0x0000,因为某些 BIOS 很可能使用非零 CS 来访问您的引导加载程序。例如 0x07c0:0x0000 也会到达物理地址 0x07c00 (0x07c0<<4+0x0000=0x07c00)。在实模式代码中,我建议将 DS 设置为零并使用 lgdt [gdtr].

  • 进入保护模式后,在使用堆栈之前,您应该设置SP。中断将要求堆栈指针在某处有效。将其初始化为 0x0000 将使堆栈从 64KiB 段的顶部向下增长。你不应该依赖它碰巧指向某个地方,一旦进入保护模式就不会干扰你的 运行 系统(即在你的引导加载程序 code/data 之上)。

  • 在使用 STOS/SCAS/CMPS/LODS 等任何字符串指令之前,您应该确保方向标志已按照您的预期设置。由于您依赖于向前移动,因此您应该使用 CLD 清除方向标志。您不应该假设在进入引导加载程序时方向标志是明确的。

我的 在另一个 Whosebug 答案中捕获了其中的许多问题。