Assembly 64位内核中读取上半个地址的页面错误

Assembly 64位内核中读取上半个地址的页面错误,assembly,nasm,x86-64,paging,osdev,Assembly,Nasm,X86 64,Paging,Osdev,我正在用Rust和NASM汇编程序编写一个64位高半内核。我正在使用一个与Multiboot2(GRUB2)兼容的引导加载程序来最初加载内核。当我的内核在QEMU中运行时,我遇到了一个页面错误(0x0eexception),我不明白为什么。我遇到的问题出现在我的汇编代码中,在它到达用Rust编写的代码之前 我正在设置分页,以便内存如下所示: 0000000000000000: 0000000000000000 --PDA---W 0000000000200000: 000000000020000

我正在用Rust和NASM汇编程序编写一个64位高半内核。我正在使用一个与Multiboot2(GRUB2)兼容的引导加载程序来最初加载内核。当我的内核在QEMU中运行时,我遇到了一个页面错误(
0x0e
exception),我不明白为什么。我遇到的问题出现在我的汇编代码中,在它到达用Rust编写的代码之前

我正在设置分页,以便内存如下所示:

0000000000000000: 0000000000000000 --PDA---W
0000000000200000: 0000000000200000 --P-----W
ffffff0000000000: 0000000000000000 --P-----W
ffffff7f80000000: 0000000000000000 X-P------
(这既是我的意图,也是QEMU的
info mem
的结果)

这些表如下所示:

p4: # pml4
    0o000 <- p3_low | PRESENT | WRITABLE
    0o776 <- p3_hgh | PRESENT | WRITABLE
p3_low: # pdpte
    0o000 <- p2_low | PRESENT | WRITABLE
p3_hgh: # pdpte
    0o000 <- p2_krn | PRESENT | WRITABLE
    0o667 <- p2_mbi | PRESENT | WRITABLE
p2_low: # pde
    0o000 <- 0o000000_000_000_000_000_0000 | PRESENT | WRITABLE | PAGESIZE
    0o001 <- 0o000000_000_000_001_000_0000 | PRESENT | WRITABLE | PAGESIZE
p2_krn: # pde
    0o000 <- 0o000000_000_000_000_000_0000 | PRESENT | WRITABLE | PAGESIZE
p2_mbi: # pde
    0o000 <- 0o000000_000_000_000_000_0000 | PRESENT | PAGESIZE | NOEXEC
分页64.asm

%macro pte_write 4
    mov rax, %4
    or rax, %3
    mov qword [%1+8*%2], rax
%endmacro
extern kernel_start
extern kernel_end

p_present  equ (1<<0)
p_writable equ (1<<1)
p_user     equ (1<<2)
p_pagesize equ (1<<7)
p_noexec   equ (1<<63)

[section .text]

enable_paging:
    ; Calculate start and end address of the multiboot2 info structure.
    mov r9, rdi
    mov r10, r9
    add r10d, dword [r9]
    and r9, 0xfffffffffffff000
    shr r10, 12
    inc r10
    shl r10, 12
    ; Clear out all the page tables.
    movaps xmm1, [blank]
    mov rcx, page_tables_start
.clear_page_tables_loop:
    movaps [rcx], xmm1
    add rcx, 16
    cmp rcx, page_tables_end
    jl .clear_page_tables_loop
    ; TODO Uncomment the recursive page mappings once things actually work -- for now, they just make "info tlb" in QEMU annoying to read.
    ; Fill out the P4 table.
    pte_write p4, 0o000, p3_low, p_present | p_writable
    pte_write p4, 0o776, p3_hgh, p_present | p_writable
;   pte_write p4, 0o777, p4,     p_present | p_writable | p_noexec
    ; Fill out the P3 tables.
    pte_write p3_low, 0o000, p2_low, p_present | p_writable
;   pte_write p3_low, 0o777, p3_low, p_present | p_writable | p_noexec
    pte_write p3_hgh, 0o000, p2_krn, p_present | p_writable
    pte_write p3_hgh, 0o776, p2_mbi, p_present | p_writable
;   pte_write p3_hgh, 0o777, p3_hgh, p_present | p_writable | p_noexec
    ; Identity map the lowest 2MiB.
    pte_write p2_low, 0o000, 0o000000_000_000_000_000_0000, p_present | p_writable | p_pagesize
    pte_write p2_low, 0o001, 0o000000_000_000_001_000_0000, p_present | p_writable | p_pagesize
;   pte_write p2_low, 0o777, p2_low, p_present | p_writable | p_noexec
    ; Map the kernel.
    xor rcx, rcx
    mov rsi, kernel_start
.kernel_loop:
    pte_write p2_krn, rcx, rsi, p_present | p_writable | p_pagesize
    inc rcx
    add rsi, 0o000000_000_000_001_000_0000
    cmp rsi, kernel_end
    jb .kernel_loop
    ; Map the multiboot2 information structure.
    xor rcx, rcx
    mov rsi, r9
.mbi_loop:
    pte_write p2_mbi, rcx, rsi, p_present | p_pagesize | p_noexec
    inc rcx
    add rsi, 0o000000_000_000_001_000_0000
    cmp rsi, r10
    jb .mbi_loop
    ; Load the new page table. We don't need to flush the TLB because we moved into CR3.
    mov rax, p4
    mov cr3, rax
    ; Return.
    ret

[section .data]
align 0x10
blank: times 0x10 db 0x00

[section .bss]

alignb 4096
page_tables_start:

p4: resb 4096
p3_low: resb 4096
p3_hgh: resb 4096
p2_low: resb 4096
p2_krn: resb 4096
p2_mbi: resb 4096

page_tables_end:
bits 64

extern kmain
global start64

%include "macros64.asm"
%include "paging64.asm"

[section .text]

;; The entry point for 64-bit code. We expect the address of the multiboot2
;; info structure in rdi.
start64:
    ; Save the address of the multiboot2 info structure.
    push rdi
    ; Clear interrupts. If we get an interrupt before we have an IDT, we'll
    ; triple fault. We can re-enable it from Rust, later.
    cli
    ; Nuke the segment registers.
    mov rax, 0x10
    mov ss, ax
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    ; Set up paging.
    call enable_paging
    ; The first argument to kmain is the multiboot2 info structure. We need to
    ; adjust the address to the new higher-half location.
    pop rdi
    mov rax, 0xffffff7f80000000
    add rdi, rax
    ; DEBUG
    mov dword [0xb8004], 0xf021f021
    mov rbx, [rdi]
    mov dword [0xb8000], 0xf021f021
    hlt
    ; Call kmain. It's more than 4GiB away, so we have to do an indirect call.
    mov rax, kmain
    call rax
    ; kmain should never return; call halt if it does.
    jmp halt

halt:
    ; Write "kexit?!?" to the upper right corner.
    mov dword [0xb8000], 0x4f654f6b
    mov dword [0xb8004], 0x4f694f78
    mov dword [0xb8008], 0x4f3f4f74
    mov dword [0xb800c], 0x4f3f4f21
    ; Disable interrupts and halt.
    cli
    hlt
    ; Just in case... something? happens.
    jmp halt
%macro pte_write 4
    mov rax, %4
    or rax, %3
    mov qword [%1+8*%2], rax
%endmacro

%macro pte_write_res 4
    mov rax, %4
    mov r11, 0x7fffffffffe00000
    and r11, %3
    or  rax, r11
    mov qword [%1+8*%2], rax
%endmacro
.kernel_loop:
    pte_write p2_krn, rcx, rsi, p_present | p_writable | p_pagesize
    inc rcx

将新页表移动到CR3中后,执行将正确继续。但是,一旦我试图从
start64.asm
中的高内存读取值,就会出现页面错误。故障发生在该线路上:

    mov rbx, [rdi]
前面的行没有正确写入
mov dword[0xb8004],0xf021f021
显示在屏幕上
[rdi]
是可以找到Multiboot2信息记录的上半个地址


完整代码的副本可以在中找到。

我将根据我做的另一个实验进行有根据的猜测。我猜想,如果您向后滚动并查看抛出的异常(您的一个图像的开头被切断),您会在QEMU中看到如下输出:

特别是,您将查找以
new 0xe
开头的异常,这是页面错误异常。为了简洁,我剪掉了

在第二行,您可能会看到
e=0009
。这是在进入页面处理程序之前将推送到堆栈上的错误代码。您没有页面处理程序,因此您需要将错误增加三倍,之后还会出现其他异常

错误代码0x0009是什么意思?有一个描述:

您的值
e=0009
是01001的位掩码。这意味着发生了页面保护冲突(而不是页面不存在错误),并意味着从保留字段读取了1

所讨论的保留字段(位)位于页面表层次结构底部的页面表(PT)的实际页面表条目(PTE)中。当使用带有的2MB页面大小时,页面表中的PTE必须将位12到20设置为零。这是您当前代码的情况。保留位的特殊之处在于,如果它们包含值1,那么您将在QEMU输出中获得
e=0009

要解决此问题,必须确保实际页面表(PTs)中的页面表条目(PTE)将这些位设置为0。一个快速的破解方法可能是在macros64.asm中执行类似的操作:

%macro pte_write 4
    mov rax, %4
    or rax, %3
    mov qword [%1+8*%2], rax
%endmacro
extern kernel_start
extern kernel_end

p_present  equ (1<<0)
p_writable equ (1<<1)
p_user     equ (1<<2)
p_pagesize equ (1<<7)
p_noexec   equ (1<<63)

[section .text]

enable_paging:
    ; Calculate start and end address of the multiboot2 info structure.
    mov r9, rdi
    mov r10, r9
    add r10d, dword [r9]
    and r9, 0xfffffffffffff000
    shr r10, 12
    inc r10
    shl r10, 12
    ; Clear out all the page tables.
    movaps xmm1, [blank]
    mov rcx, page_tables_start
.clear_page_tables_loop:
    movaps [rcx], xmm1
    add rcx, 16
    cmp rcx, page_tables_end
    jl .clear_page_tables_loop
    ; TODO Uncomment the recursive page mappings once things actually work -- for now, they just make "info tlb" in QEMU annoying to read.
    ; Fill out the P4 table.
    pte_write p4, 0o000, p3_low, p_present | p_writable
    pte_write p4, 0o776, p3_hgh, p_present | p_writable
;   pte_write p4, 0o777, p4,     p_present | p_writable | p_noexec
    ; Fill out the P3 tables.
    pte_write p3_low, 0o000, p2_low, p_present | p_writable
;   pte_write p3_low, 0o777, p3_low, p_present | p_writable | p_noexec
    pte_write p3_hgh, 0o000, p2_krn, p_present | p_writable
    pte_write p3_hgh, 0o776, p2_mbi, p_present | p_writable
;   pte_write p3_hgh, 0o777, p3_hgh, p_present | p_writable | p_noexec
    ; Identity map the lowest 2MiB.
    pte_write p2_low, 0o000, 0o000000_000_000_000_000_0000, p_present | p_writable | p_pagesize
    pte_write p2_low, 0o001, 0o000000_000_000_001_000_0000, p_present | p_writable | p_pagesize
;   pte_write p2_low, 0o777, p2_low, p_present | p_writable | p_noexec
    ; Map the kernel.
    xor rcx, rcx
    mov rsi, kernel_start
.kernel_loop:
    pte_write p2_krn, rcx, rsi, p_present | p_writable | p_pagesize
    inc rcx
    add rsi, 0o000000_000_000_001_000_0000
    cmp rsi, kernel_end
    jb .kernel_loop
    ; Map the multiboot2 information structure.
    xor rcx, rcx
    mov rsi, r9
.mbi_loop:
    pte_write p2_mbi, rcx, rsi, p_present | p_pagesize | p_noexec
    inc rcx
    add rsi, 0o000000_000_000_001_000_0000
    cmp rsi, r10
    jb .mbi_loop
    ; Load the new page table. We don't need to flush the TLB because we moved into CR3.
    mov rax, p4
    mov cr3, rax
    ; Return.
    ret

[section .data]
align 0x10
blank: times 0x10 db 0x00

[section .bss]

alignb 4096
page_tables_start:

p4: resb 4096
p3_low: resb 4096
p3_hgh: resb 4096
p2_low: resb 4096
p2_krn: resb 4096
p2_mbi: resb 4096

page_tables_end:
bits 64

extern kmain
global start64

%include "macros64.asm"
%include "paging64.asm"

[section .text]

;; The entry point for 64-bit code. We expect the address of the multiboot2
;; info structure in rdi.
start64:
    ; Save the address of the multiboot2 info structure.
    push rdi
    ; Clear interrupts. If we get an interrupt before we have an IDT, we'll
    ; triple fault. We can re-enable it from Rust, later.
    cli
    ; Nuke the segment registers.
    mov rax, 0x10
    mov ss, ax
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    ; Set up paging.
    call enable_paging
    ; The first argument to kmain is the multiboot2 info structure. We need to
    ; adjust the address to the new higher-half location.
    pop rdi
    mov rax, 0xffffff7f80000000
    add rdi, rax
    ; DEBUG
    mov dword [0xb8004], 0xf021f021
    mov rbx, [rdi]
    mov dword [0xb8000], 0xf021f021
    hlt
    ; Call kmain. It's more than 4GiB away, so we have to do an indirect call.
    mov rax, kmain
    call rax
    ; kmain should never return; call halt if it does.
    jmp halt

halt:
    ; Write "kexit?!?" to the upper right corner.
    mov dword [0xb8000], 0x4f654f6b
    mov dword [0xb8004], 0x4f694f78
    mov dword [0xb8008], 0x4f3f4f74
    mov dword [0xb800c], 0x4f3f4f21
    ; Disable interrupts and halt.
    cli
    hlt
    ; Just in case... something? happens.
    jmp halt
%macro pte_write 4
    mov rax, %4
    or rax, %3
    mov qword [%1+8*%2], rax
%endmacro

%macro pte_write_res 4
    mov rax, %4
    mov r11, 0x7fffffffffe00000
    and r11, %3
    or  rax, r11
    mov qword [%1+8*%2], rax
%endmacro
.kernel_loop:
    pte_write p2_krn, rcx, rsi, p_present | p_writable | p_pagesize
    inc rcx
主要区别在于,
pte_write_res
通过将保留位设为0,专门强制执行保留位的规则。然后必须修改使用这些宏的代码。在您的情况下,它似乎位于
paging64.asm中的这两个位置:

%macro pte_write 4
    mov rax, %4
    or rax, %3
    mov qword [%1+8*%2], rax
%endmacro
extern kernel_start
extern kernel_end

p_present  equ (1<<0)
p_writable equ (1<<1)
p_user     equ (1<<2)
p_pagesize equ (1<<7)
p_noexec   equ (1<<63)

[section .text]

enable_paging:
    ; Calculate start and end address of the multiboot2 info structure.
    mov r9, rdi
    mov r10, r9
    add r10d, dword [r9]
    and r9, 0xfffffffffffff000
    shr r10, 12
    inc r10
    shl r10, 12
    ; Clear out all the page tables.
    movaps xmm1, [blank]
    mov rcx, page_tables_start
.clear_page_tables_loop:
    movaps [rcx], xmm1
    add rcx, 16
    cmp rcx, page_tables_end
    jl .clear_page_tables_loop
    ; TODO Uncomment the recursive page mappings once things actually work -- for now, they just make "info tlb" in QEMU annoying to read.
    ; Fill out the P4 table.
    pte_write p4, 0o000, p3_low, p_present | p_writable
    pte_write p4, 0o776, p3_hgh, p_present | p_writable
;   pte_write p4, 0o777, p4,     p_present | p_writable | p_noexec
    ; Fill out the P3 tables.
    pte_write p3_low, 0o000, p2_low, p_present | p_writable
;   pte_write p3_low, 0o777, p3_low, p_present | p_writable | p_noexec
    pte_write p3_hgh, 0o000, p2_krn, p_present | p_writable
    pte_write p3_hgh, 0o776, p2_mbi, p_present | p_writable
;   pte_write p3_hgh, 0o777, p3_hgh, p_present | p_writable | p_noexec
    ; Identity map the lowest 2MiB.
    pte_write p2_low, 0o000, 0o000000_000_000_000_000_0000, p_present | p_writable | p_pagesize
    pte_write p2_low, 0o001, 0o000000_000_000_001_000_0000, p_present | p_writable | p_pagesize
;   pte_write p2_low, 0o777, p2_low, p_present | p_writable | p_noexec
    ; Map the kernel.
    xor rcx, rcx
    mov rsi, kernel_start
.kernel_loop:
    pte_write p2_krn, rcx, rsi, p_present | p_writable | p_pagesize
    inc rcx
    add rsi, 0o000000_000_000_001_000_0000
    cmp rsi, kernel_end
    jb .kernel_loop
    ; Map the multiboot2 information structure.
    xor rcx, rcx
    mov rsi, r9
.mbi_loop:
    pte_write p2_mbi, rcx, rsi, p_present | p_pagesize | p_noexec
    inc rcx
    add rsi, 0o000000_000_000_001_000_0000
    cmp rsi, r10
    jb .mbi_loop
    ; Load the new page table. We don't need to flush the TLB because we moved into CR3.
    mov rax, p4
    mov cr3, rax
    ; Return.
    ret

[section .data]
align 0x10
blank: times 0x10 db 0x00

[section .bss]

alignb 4096
page_tables_start:

p4: resb 4096
p3_low: resb 4096
p3_hgh: resb 4096
p2_low: resb 4096
p2_krn: resb 4096
p2_mbi: resb 4096

page_tables_end:
bits 64

extern kmain
global start64

%include "macros64.asm"
%include "paging64.asm"

[section .text]

;; The entry point for 64-bit code. We expect the address of the multiboot2
;; info structure in rdi.
start64:
    ; Save the address of the multiboot2 info structure.
    push rdi
    ; Clear interrupts. If we get an interrupt before we have an IDT, we'll
    ; triple fault. We can re-enable it from Rust, later.
    cli
    ; Nuke the segment registers.
    mov rax, 0x10
    mov ss, ax
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    ; Set up paging.
    call enable_paging
    ; The first argument to kmain is the multiboot2 info structure. We need to
    ; adjust the address to the new higher-half location.
    pop rdi
    mov rax, 0xffffff7f80000000
    add rdi, rax
    ; DEBUG
    mov dword [0xb8004], 0xf021f021
    mov rbx, [rdi]
    mov dword [0xb8000], 0xf021f021
    hlt
    ; Call kmain. It's more than 4GiB away, so we have to do an indirect call.
    mov rax, kmain
    call rax
    ; kmain should never return; call halt if it does.
    jmp halt

halt:
    ; Write "kexit?!?" to the upper right corner.
    mov dword [0xb8000], 0x4f654f6b
    mov dword [0xb8004], 0x4f694f78
    mov dword [0xb8008], 0x4f3f4f74
    mov dword [0xb800c], 0x4f3f4f21
    ; Disable interrupts and halt.
    cli
    hlt
    ; Just in case... something? happens.
    jmp halt
%macro pte_write 4
    mov rax, %4
    or rax, %3
    mov qword [%1+8*%2], rax
%endmacro

%macro pte_write_res 4
    mov rax, %4
    mov r11, 0x7fffffffffe00000
    and r11, %3
    or  rax, r11
    mov qword [%1+8*%2], rax
%endmacro
.kernel_loop:
    pte_write p2_krn, rcx, rsi, p_present | p_writable | p_pagesize
    inc rcx
现在将成为:

.kernel_loop:
    pte_write_res p2_krn, rcx, rsi, p_present | p_writable | p_pagesize
    inc rcx
.mbi_loop:
    pte_write_res p2_mbi, rcx, rsi, p_present | p_pagesize | p_noexec
    inc rcx

现在将成为:

.kernel_loop:
    pte_write_res p2_krn, rcx, rsi, p_present | p_writable | p_pagesize
    inc rcx
.mbi_loop:
    pte_write_res p2_mbi, rcx, rsi, p_present | p_pagesize | p_noexec
    inc rcx

在这两种情况下,我们都需要写入页面表的页面表条目,其中RSI可能设置了我们需要为0的位。

添加了数据段后,问题仍然会发生。如今,什么真正取决于非CS细分市场?我认为它们已经过时了?啊,今天早上我在看两个不同的OSDev问题。在您的情况下,您使用的是64位长模式。在该模式下,唯一不接受0值(限制为2^64)的寄存器是可以设置的FS和GS。很抱歉。不过,
movaps
是一个可能的问题。除非明确使用类似于
align 16
的内容,否则数据节中的数据可能会以8(而不是16)字节边界结束。这是否是一个问题将取决于编译器/汇编器/链接器及其放置位置。正如我所说,我只是在观察——我只看了一眼你的代码是的,我在
空白区域设置了对齐。我确实设法把范围缩小到失败的指令;我只是不知道为什么。是的,这是可行的,但对[0xb8000]的写入没有发生。如果您有调试提示,我可以连接GDB。不,0xb8004
可以工作;只有
0xb8000
不能: