跳转到 64 位 longmode 时出现三重故障
Triple fault when jumping to 64-bit longmode
以下从 32 位保护模式(启用 A20)转换到 64 位长模式的代码似乎给我带来了问题。我将 1GiB 页面从 0x00000000 标识映射到 0x3fffffff;启用PAE;启用 EFER MSR 中的 longmode 位;安装 GDT;启用分页;然后对我的 64 位入口点执行模拟 FAR JMP:
lea eax, [PML4]
mov cr3, eax
mov eax, cr4
or eax, 100000b
mov cr4, eax
mov ecx, 0xc0000080
rdmsr
or eax, 100000000b
wrmsr
mov eax, cr0
mov ebx, 0x1
shl ebx, 31
or eax, ebx
mov cr0, eax
call gdt64_install
push 8
push longmode
retf ;<===================== faults here
当执行 RETF
指令时,BOCHS 中的程序出现三重错误,但似乎 return 没有任何错误。如果我在此跳转之前键入 info tab
,我会得到:
0x00000000-0x3fffffff -> 0x000000000000-0x00003fffffff
在我看来,分页功能正常。这是 sreg
输出:
es:0x0010, dh=0x00cf9300, dl=0x0000ffff, valid=1
Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
cs:0x0008, dh=0x00cf9b00, dl=0x0000ffff, valid=1
Code segment, base=0x00000000, limit=0xffffffff, Execute/Read, Non-Conforming, Accessed, 32-bit
ss:0x0010, dh=0x00cf9300, dl=0x0000ffff, valid=31
Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
ds:0x0010, dh=0x00cf9300, dl=0x0000ffff, valid=31
Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
fs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
gs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
tr:0x0000, dh=0x00008b00, dl=0x0000ffff, valid=1
gdtr:base=0x0000000000008252, limit=0x1f
idtr:base=0x0000000000000000, limit=0x3ff
我的 GDT 条目是:
gdt64_install:
lgdt[GDT_addr]
ret
GDT_addr:
dw (GDT64_end - GDT64) - 1
dd GDT64
GDT64:
dd 0, 0
dd 0xffff ; segment limit
dd 0xef9a00
dd 0xffff ; segment limit
dd 0xef9200
dd 0, 0
GDT64_end:
我的页面 table 使用 PML4 and PDP 的结构定义为:
align 4096 ;;align to 4 KB
PML4:
dq 0 or 1b or 10b or PDP;;preset bit, r/w bit
dq 511 dup(PDP or 10b)
PDP:
dq 0 or 1b or 10000000b ;;dq zero, because we map memory from start so 0x0000, present bit
;;PDPE.PS to indicate 1gb pages
dq 511 dup(10000000b)
知道为什么会出现三重故障吗?
可以在 Github
上找到我的项目的副本
主要问题是您的 GDT 似乎是为 32 位设计的。对于 64 位描述符,您需要设置 64 位描述符位。从 OSDev wiki 我们可以看到 GDT 的布局以及标志和访问位:
如 wiki 中所述,这些更改适用于 64 位描述符:
x86-64 Changes
- 'L' bit (bit 21, next to 'Sz') is used to indicate x86-64 descriptor
- 'Sz' bit (bit 22) has to be 0 when the 'L' bit is set, as the combination Sz = 1, L = 1 is reserved for future use (and will throw an exception if you try to use it)
Intel 还建议出于性能原因在 8 字节边界上对齐 GDT。在 64 位描述符中,base 和 limit 应该设置为 0。如果你打算以后在 64 位模式下使用 GDT table 你会想要将 dd GDT64
更改为四字。考虑到这些事情,我修改了你的 GDT 使其更具可读性:
GDT_addr:
dw (GDT64_end - GDT64) - 1
dq GDT64 ; Use quadword so we can use this GDT table
; from 64-bit mode if necessary
align 8 ; Intel suggests GDT should be 8 byte aligned
GDT64: ; Global Descriptor Table (64-bit).
; 64-bit descriptors should set all limit and base to 0
; NULL Descriptor
dw 0 ; Limit (low).
dw 0 ; Base (low).
db 0 ; Base (middle)
db 0 ; Access.
db 0 ; Flags.
db 0 ; Base (high).
; 64-bit Code descriptor
dw 0 ; Limit (low).
dw 0 ; Base (low).
db 0 ; Base (middle)
db 10011010b ; Access (present/exec/read).
db 00100000b ; Flags 64-bit descriptor
db 0 ; Base (high).
; 64-bit Data descriptor
dw 0 ; Limit (low).
dw 0 ; Base (low).
db 0 ; Base (middle)
db 10010010b ; Access (present/read&write).
db 00100000b ; Flags 64-bit descriptor.
db 0 ; Base (high).
GDT64_end:
其他观察结果
您使用它来转换到 64 位长模式:
push 8
push longmode
retf
虽然这有效,但如果您使用的是 FASM,或者 NASM 如果您仍处于 32 位模式,则使用 FAR JMP 会容易得多:
jmp 0x08:longmode
自某些 early AMD64 processor types didn't support JMP mem16:64 以来,在 64 位代码中执行一次 FAR JMP 存在问题。使用PUSH/RETF方法使代码更加通用。在 64 位长模式下执行一次这样的 FAR JMP 只会在极少数情况下使用。
您的代码中还有另一个关于读取扇区的问题。我发现并不是所有的代码和数据都被读入内存。在您的 exread.inc
中,您定义:
SECTOREAD equ 20
我发现当我构建你的软盘映像时,文件大小是13976。也就是28个扇区(512*28=14336)。您的 20
值阅读不足。确保这对您来说不是问题,如果需要,请阅读更多扇区。
与手头的问题无关,我注意到你的 Makefile
你有:
qrun: deploy_all
qemu-system-i386 kernel.bin
如果你想在 QEMU 中使用 运行 64 位代码,你需要使用 qemu-system-x86_64
而不是 qemu-system-i386
。我发现这更有用:
qrun: deploy_all
qemu-system-x86_64 -fda floppy.bin -no-shutdown -no-reboot -d int
-no-shutdown -no-reboot -d int
选项对调试很有用。这将导致 QEMU 在三重故障时无法重启和关闭。 -d int
提供有关所抛出的中断和异常的有用信息。
以下从 32 位保护模式(启用 A20)转换到 64 位长模式的代码似乎给我带来了问题。我将 1GiB 页面从 0x00000000 标识映射到 0x3fffffff;启用PAE;启用 EFER MSR 中的 longmode 位;安装 GDT;启用分页;然后对我的 64 位入口点执行模拟 FAR JMP:
lea eax, [PML4]
mov cr3, eax
mov eax, cr4
or eax, 100000b
mov cr4, eax
mov ecx, 0xc0000080
rdmsr
or eax, 100000000b
wrmsr
mov eax, cr0
mov ebx, 0x1
shl ebx, 31
or eax, ebx
mov cr0, eax
call gdt64_install
push 8
push longmode
retf ;<===================== faults here
当执行 RETF
指令时,BOCHS 中的程序出现三重错误,但似乎 return 没有任何错误。如果我在此跳转之前键入 info tab
,我会得到:
0x00000000-0x3fffffff -> 0x000000000000-0x00003fffffff
在我看来,分页功能正常。这是 sreg
输出:
es:0x0010, dh=0x00cf9300, dl=0x0000ffff, valid=1
Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
cs:0x0008, dh=0x00cf9b00, dl=0x0000ffff, valid=1
Code segment, base=0x00000000, limit=0xffffffff, Execute/Read, Non-Conforming, Accessed, 32-bit
ss:0x0010, dh=0x00cf9300, dl=0x0000ffff, valid=31
Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
ds:0x0010, dh=0x00cf9300, dl=0x0000ffff, valid=31
Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
fs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
gs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
tr:0x0000, dh=0x00008b00, dl=0x0000ffff, valid=1
gdtr:base=0x0000000000008252, limit=0x1f
idtr:base=0x0000000000000000, limit=0x3ff
我的 GDT 条目是:
gdt64_install:
lgdt[GDT_addr]
ret
GDT_addr:
dw (GDT64_end - GDT64) - 1
dd GDT64
GDT64:
dd 0, 0
dd 0xffff ; segment limit
dd 0xef9a00
dd 0xffff ; segment limit
dd 0xef9200
dd 0, 0
GDT64_end:
我的页面 table 使用 PML4 and PDP 的结构定义为:
align 4096 ;;align to 4 KB
PML4:
dq 0 or 1b or 10b or PDP;;preset bit, r/w bit
dq 511 dup(PDP or 10b)
PDP:
dq 0 or 1b or 10000000b ;;dq zero, because we map memory from start so 0x0000, present bit
;;PDPE.PS to indicate 1gb pages
dq 511 dup(10000000b)
知道为什么会出现三重故障吗?
可以在 Github
上找到我的项目的副本主要问题是您的 GDT 似乎是为 32 位设计的。对于 64 位描述符,您需要设置 64 位描述符位。从 OSDev wiki 我们可以看到 GDT 的布局以及标志和访问位:
如 wiki 中所述,这些更改适用于 64 位描述符:
x86-64 Changes
- 'L' bit (bit 21, next to 'Sz') is used to indicate x86-64 descriptor
- 'Sz' bit (bit 22) has to be 0 when the 'L' bit is set, as the combination Sz = 1, L = 1 is reserved for future use (and will throw an exception if you try to use it)
Intel 还建议出于性能原因在 8 字节边界上对齐 GDT。在 64 位描述符中,base 和 limit 应该设置为 0。如果你打算以后在 64 位模式下使用 GDT table 你会想要将 dd GDT64
更改为四字。考虑到这些事情,我修改了你的 GDT 使其更具可读性:
GDT_addr:
dw (GDT64_end - GDT64) - 1
dq GDT64 ; Use quadword so we can use this GDT table
; from 64-bit mode if necessary
align 8 ; Intel suggests GDT should be 8 byte aligned
GDT64: ; Global Descriptor Table (64-bit).
; 64-bit descriptors should set all limit and base to 0
; NULL Descriptor
dw 0 ; Limit (low).
dw 0 ; Base (low).
db 0 ; Base (middle)
db 0 ; Access.
db 0 ; Flags.
db 0 ; Base (high).
; 64-bit Code descriptor
dw 0 ; Limit (low).
dw 0 ; Base (low).
db 0 ; Base (middle)
db 10011010b ; Access (present/exec/read).
db 00100000b ; Flags 64-bit descriptor
db 0 ; Base (high).
; 64-bit Data descriptor
dw 0 ; Limit (low).
dw 0 ; Base (low).
db 0 ; Base (middle)
db 10010010b ; Access (present/read&write).
db 00100000b ; Flags 64-bit descriptor.
db 0 ; Base (high).
GDT64_end:
其他观察结果
您使用它来转换到 64 位长模式:
push 8
push longmode
retf
虽然这有效,但如果您使用的是 FASM,或者 NASM 如果您仍处于 32 位模式,则使用 FAR JMP 会容易得多:
jmp 0x08:longmode
自某些 early AMD64 processor types didn't support JMP mem16:64 以来,在 64 位代码中执行一次 FAR JMP 存在问题。使用PUSH/RETF方法使代码更加通用。在 64 位长模式下执行一次这样的 FAR JMP 只会在极少数情况下使用。
您的代码中还有另一个关于读取扇区的问题。我发现并不是所有的代码和数据都被读入内存。在您的 exread.inc
中,您定义:
SECTOREAD equ 20
我发现当我构建你的软盘映像时,文件大小是13976。也就是28个扇区(512*28=14336)。您的 20
值阅读不足。确保这对您来说不是问题,如果需要,请阅读更多扇区。
与手头的问题无关,我注意到你的 Makefile
你有:
qrun: deploy_all
qemu-system-i386 kernel.bin
如果你想在 QEMU 中使用 运行 64 位代码,你需要使用 qemu-system-x86_64
而不是 qemu-system-i386
。我发现这更有用:
qrun: deploy_all
qemu-system-x86_64 -fda floppy.bin -no-shutdown -no-reboot -d int
-no-shutdown -no-reboot -d int
选项对调试很有用。这将导致 QEMU 在三重故障时无法重启和关闭。 -d int
提供有关所抛出的中断和异常的有用信息。