跳入保护模式时出现三重故障

Triple fault when jumping into protected mode

我正在开发一个引导加载程序,它会在切换到保护模式后引导到一个简单的内核。我在第四章或第五章的某处使用 this paper 作为教程。理论上应该以16位实模式启动,加载内核到内存,切换到32位保护模式,开始执行内核代码。

但是,当我切换到保护模式并跳远或跳到另一个网段时,它会出现三重故障。这是主引导扇区代码:

[org 0x7c00]

KERNEL_OFFSET equ 0x1000

mov [BOOT_DRIVE], dl    ;Get the current boot drive from the BIOS

mov bp, 0x9000          ;Set up stack, with enough room to grow downwards
mov sp, bp

mov bx, REAL_MODE_MSG
call print_string

call load_kernel

call switch_to_pm

jmp $                       ;Jump to current position and loop forever

%include "boot/util/print_string.asm"
%include "boot/util/disk.asm"
%include "boot/gdt/gdt.asm"
%include "boot/util/print_string_pm.asm"
%include "boot/switch_to_pm.asm"

[bits 16]
load_kernel:
    mov bx, LOAD_KERNEL_MSG ;Print a message saying we are loading the kernel
    call print_string
    mov bx, KERNEL_OFFSET       ;Set up disk_load routine parameters
    mov dh, 15
    mov dl, [BOOT_DRIVE]
    call disk_load              ;Call disk_load
    ret

[bits 32]
BEGIN_PM:
    mov ebx, PROT_MODE_MSG
    call print_string_pm
    call KERNEL_OFFSET

    jmp $

; Data
BOOT_DRIVE: db 0
REAL_MODE_MSG: db "Started in real mode.", 0
PROT_MODE_MSG: db "Successfully entered 32-bit protected mode.", 0
LOAD_KERNEL_MSG: db "Loading Kernel into memory", 0

; Bootsector padding
times 510-($-$$) db 0
dw 0xaa55

这是 GDT:

;Global Descriptor Table
gdt_start:

gdt_null:   ; We need a null descriptor at the start (8 bytes)
    dd 0x0
    dd 0x0

gdt_code:   ; Code segment descriptor
    ; Base=0x0, Limit=0xfffff
    ; 1st flags : (present)1 (privilege)00 (descriptor type)1 -> 1001b
    ; type flags : (code)1 (conforming)0 (readable)1 (accessed)0 -> 1010b
    ; 2nd flags : (granularity)1 (32 - bit default)1 (64 - bit seg)0 (AVL)0 -> 1100b
    dw 0xffff       ; Limit (bits 0-15)
    dw 0x0      ; Base (0-15)
    dw 0x0          ; Base (16-23)
    db 10011010b    ; 1st flags and type flags
    db 11001111b    ; 2nd flags and Limit (16-19)
    db 0x0          ; Base (24-31)

gdt_data:   ; Data segment descriptor
    ;Same as CSD except for type flags
    ; (code)0 (expand down)0 (writable)1 (accessed)0 -> 0010b
    dw 0xffff       ; Limit (bits 0-15)
    dw 0x0          ; Base (0-15)
    dw 0x0          ; Base (16-23)
    db 10010010b    ; 1st flags and type flags
    db 11001111b    ; 2nd flags and Limit (16-19)
    db 0x0          ; Base (24-31)

gdt_end:


;GDT Descriptor
gdt_descriptor:
    dw gdt_end - gdt_start - 1
    dd gdt_start

;Some Constants
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start

以下是切换到保护模式的代码,其中出现三重故障:

[bits 16]
switch_to_pm:
    cli
    lgdt [gdt_descriptor]   ; load the gdt
    mov eax, cr0            ; turn pm on
    or eax, 0x1
    mov cr0, eax
    jmp CODE_SEG:init_pm    ; THIS IS WHERE THE PROBLEM IS!

[bits 32]
init_pm:
    mov ax, DATA_SEG ; Point segment registers to the data
    mov ds, ax       ; selector defined in the gdt
    mov ss, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ebp, 0x90000 ; Update our stack
    mov esp, ebp
    call BEGIN_PM ;Move on

当我将 jmp $ 指令放置在某个位置空闲时,就在 jmp CODE_SEG:init_pm 指令之前,它在那里空闲并且不会出现三重故障。当我将它放在该指令之后,在标签 init_pm 内时,它会出现三重故障。所以我相当确定这是原因。我不太确定为什么,也许这是 GDT 的问题。我是操作系统开发和引导加载程序的新手。关于如何解决这个问题有什么建议吗?

问题出在你身上jmp CODE_SEG:init_pm。在 16 位模式下,它是一个 4 字节跳转到 16 位地址的 segment:offset。但是你需要做6字节远跳转到32位地址。在 fasm 语法中它将是

jmp fword CODE_SEG:init_pm

这将为指令添加一个操作数大小前缀 0x66,并将 init_pm 视为 32 位偏移量。不确定如何在 nasm 中实现相同的目标,但你明白了。

Michael Petch 在评论中给出了这个问题的正确答案。不幸的是,这似乎被一些人忽略了,因为现在已经发布了三个不正确的答案,其中两个犯了同样的错误。下面是他的评论作为答案发布,希望它能使它更加明显:

Are you sure your GDT is correct? I think the thing that stands out upon cursory look is that each of your entries is 9 byte (72 bits). A GDT entry is 8 bytes (64-bits). it appears that maybe you meant db 0x0 ; Base (16-23) instead of dw 0x0 ; Base (16-23)? Note the difference is that dw is changed to db. Wrong GDT entries would generate a triple fault.

Michael Petch 也做了很好的后续评论,指出了引导加载程序的其他问题:

I'd also recommend looking at my . You make the assumption that the DS (data segment) register is zero upon entry (since you use org 0x7c00). You should set it to zero explicitly. You also set the stack in an odd way. You set SP to 9000 but you don't set SS which means you don't really know where you are putting the stack in memory. You should set the SS register followed by setting the SP register. My bootloader tips offer an example.