启用分页后的指令似乎没有执行

Instruction after paging is enabled doesn't appear to execute

当我必须设置 EIP 寄存器时,程序没有跳转到正确的位置。我期待 jmp *%ecx 使用 LEA 跳到内存中的正确位置,将 EIP 设置为大约 0xC0100000(标签:StartInHigherHalf)。我认为 kmain 不是必需的,因为问题出在它被调用之前。无论如何我都会post它。

我尝试在 QEMU 上使用 -d cpu 标志调试它,在跳转之前(被 HLT 阻止)说 ECX 没有加载 LEA 函数。 LEA指令有可能不执行吗?为什么会这样?我该如何解决?

Boot.S:

.set ALIGN,    1<<0                                       
.set MEMINFO,  1<<1                               
.set FLAGS,    ALIGN | MEMINFO                         
.set MAGIC,    0x1BADB002                      
.set CHECKSUM, -(MAGIC + FLAGS)            

.set KERNEL_VIRTUAL_BASE, 0xC0000000                       
.set KERNEL_PAGE_NUMBER, (KERNEL_VIRTUAL_BASE >> 22)

.section .multiboot
.align 4
.long MAGIC
.long FLAGS
.long CHECKSUM

.section .data
.align 0x1000
BootPageDirectory:

.quad 0x00000083

.rept KERNEL_PAGE_NUMBER - 1
.quad 0        
.endr

.quad 0x00000083
.rept 0x400 - KERNEL_PAGE_NUMBER - 1
.quad 0                    
.endr

.set STACKSIZE, 0x4000

.global __start__
.set __start__, (setup)
setup: 
    mov $(BootPageDirectory - KERNEL_VIRTUAL_BASE), %ecx
    mov %ecx, %cr3 

    mov %cr4, %ecx
    or [=10=]x00000010, %ecx 
    mov %ecx, %cr4    

    mov %cr0, %ecx
    or [=10=]x80000000, %ecx
    mov %ecx, %cr0

    lea StartInHigherHalf, %ecx
    jmp *%ecx      

StartInHigherHalf:
    movl [=10=], (BootPageDirectory)
    invlpg (0)

    mov $(stack + STACKSIZE), %esp                 
    push %eax                                        

    push %ebx

    call _init

    call kmain

    cli
1:  hlt
    jmp 1

.section .bss
.align 32

.lcomm stack, STACKSIZE    
.global gdtFlush            
.extern gp               

gdtFlush:
    lgdt (gp)

    mov [=10=]x10, %eax
    mov %eax, %ds               
    mov %eax, %es
    mov %eax, %gs
    mov %eax, %fs
    mov %eax, %ss

    ljmp [=10=]x08, $setcs                              

setcs:
    ret

linker.ld:

ENTRY(__start__)
OUTPUT_FORMAT(elf32-i386)

SECTIONS {
    . = 0xC0100000;

    .text  ALIGN(0x1000) : AT(ADDR(.text) - 0xC0000000) {
        *(.multiboot)
        *(.text)
    }
    .rodata ALIGN(0x1000) : AT(ADDR(.rodata) - 0xC0000000) {
        *(.rodata*)
    } 
    .data ALIGN(0x1000) : AT(ADDR(.data) - 0xC0000000) {
        *(.data)
    }
    .bss ALIGN(0x1000) : AT(ADDR(.bss) - 0xC0000000) {
        _sbss = .;
        *(COMMON)
        *(.bss)
        _ebss = .;
    }
}

kmain.c:

#include <kernel/tty.h>
#include <kernel/log.h>
#include <kernel/stack-protector.h>
#include <kernel/gdt.h>

__attribute__ ((noreturn));
    void kmain(void) {
    gdtInstall();

    initializeTerminal();
    char c;
    char *buffer = &c;
    char *start = buffer;

    char str[] = "Hello, kernel World!";

    atomicallyLog(1, 1, str, buffer);
    kprintf(start, 1);
}

如果我们暂时假设您的代码实际上达到了 lea StartInHigherHalf, %ecx,并且不认为它实际上执行了这条指令 - 最明显的问题将与分页相关。 LEA 指令恰好是分页位设置后执行的第一条指令。如果分页错误,那么下一条指令可能会出错。

查看您的页面目录,我注意到您使用 .quad(8 字节类型)而不是 .int(4 字节类型)构建它。页目录中的每个条目都是 4 个字节,而不是 8 个字节。这可能是导致问题的主要原因。您可以通过使用 .fill 指令来避免 .rept 宏:

.fill repeat , size , value

result, size and value are absolute expressions. This emits repeat copies of size bytes. Repeat may be zero or more. Size may be zero or more, but if it is more than 8, then it is deemed to have the value 8, compatible with other people's assemblers. The contents of each repeat bytes is taken from an 8-byte number. The highest order 4 bytes are zero. The lowest order 4 bytes are value rendered in the byte-order of an integer on the computer as is assembling for. Each size bytes in a repetition is taken from the lowest order size bytes of this number. Again, this bizarre behavior is compatible with other people's assemblers.

size and value are optional. If the second comma and value are absent, value is assumed zero. If the first comma and following tokens are absent, size is assumed to be 1.

您的引导页目录可以写成:

.align 0x1000
BootPageDirectory:    
.int 0x00000083    
.fill KERNEL_PAGE_NUMBER - 1, 4, 0    
.int 0x00000083
.fill 0x400 - KERNEL_PAGE_NUMBER - 1, 4, 0

.rept 也适用:

.align 0x1000
BootPageDirectory:
.int 0x00000083    
.rept KERNEL_PAGE_NUMBER - 1
.int 0
.endr
.int 0x00000083
.rept 0x400 - KERNEL_PAGE_NUMBER - 1
.int 0
.endr

其他建议

您的代码似乎是 OSDev Higher Half x86 Bare Bones kernel. The main difference appears to be that you converted from NASM to GNU Assembler. I've written about a major deficiency of this code on the OSDev Forum 的变体。它的结构方式是,所有地址都是假设所有内容都是高半部分生成的,而不是将 0x100000 的下半部分与 0xC0100000 的高半部分分开。这种设计导致以 Multiboot 规范未真正定义的方式使用 Mulitboot。

您可以通过使用链接描述文件将两者分开并将适当的部分放在 boot.S 中来解决此问题。链接描述文件 (linker.ld) 可能如下所示:

ENTRY(setup)
OUTPUT_FORMAT(elf32-i386)

KERNEL_VIRTUAL_BASE = 0xC0000000;

SECTIONS {
   /* The multiboot data and code will exist in low memory
      starting at 0x100000 */

   . = 0x00100000;

   .lowerhalf ALIGN(0x1000) : {
       *(.lowerhalf.data)
       *(.lowerhalf.text)
   }

   /* The kernel will live at 3GB + 1MB in the virtual
      address space, which will be mapped to 1MB in the
      physical address space. */

   . += KERNEL_VIRTUAL_BASE;
   .text ALIGN(0x1000) : AT(ADDR(.text) - KERNEL_VIRTUAL_BASE) {
       *(.text)
   }

   .data ALIGN (0x1000) : AT(ADDR(.data) - KERNEL_VIRTUAL_BASE) {
       *(.data)
       *(.rodata*)
   }

   .bss ALIGN (0x1000) : AT(ADDR(.bss) - KERNEL_VIRTUAL_BASE) {
       _sbss = .;
       *(COMMON)
       *(.bss)
       _ebss = .;
   }

   /DISCARD/ : {
       *(.eh_frame);
       *(.comment*);
   }
}

您的 boot.S 文件可以这样写:

.set ALIGN,    1<<0
.set MEMINFO,  1<<1
.set FLAGS,    ALIGN | MEMINFO
.set MAGIC,    0x1BADB002
.set CHECKSUM, -(MAGIC + FLAGS)

.set KERNEL_VIRTUAL_BASE, 0xC0000000
.set KERNEL_PAGE_NUMBER, (KERNEL_VIRTUAL_BASE >> 22)

.section .lowerhalf.data,"aw",@progbits
.align 4
.long MAGIC
.long FLAGS
.long CHECKSUM

.align 0x1000
BootPageDirectory:
.int 0x00000083
.fill KERNEL_PAGE_NUMBER - 1, 4, 0    
.int 0x00000083
.fill 0x400 - KERNEL_PAGE_NUMBER - 1, 4, 0

.set STACKSIZE, 0x4000

.section .lowerhalf.text,"axw",@progbits
.global setup
setup:
    mov $BootPageDirectory, %ecx
    mov %ecx, %cr3

    mov %cr4, %ecx
    or [=14=]x00000010, %ecx
    mov %ecx, %cr4

    mov %cr0, %ecx
    or [=14=]x80000000, %ecx
    mov %ecx, %cr0

    jmp StartInHigherHalf

.section .text
StartInHigherHalf:
    movl [=14=], (BootPageDirectory)
    invlpg (0)

    mov $(stack + STACKSIZE), %esp
    push %eax

    push %ebx

    #call _init

    call kmain

    cli
1:  hlt
    jmp 1

/*
.global gdtFlush
.extern gp

gdtFlush:
    lgdt (gp)

    mov [=14=]x10, %eax
    mov %eax, %ds
    mov %eax, %es
    mov %eax, %gs
    mov %eax, %fs
    mov %eax, %ss

    ljmp [=14=]x08, $setcs

setcs:
    ret
*/

.section .bss
.align 32

.lcomm stack, STACKSIZE

什么都不做的内核 (kernel.c) 可能看起来像:

volatile unsigned short int *const video_mem = (unsigned short int *)0xc00b8000;
void kmain(void) {
    /* print MDP in upper left of display using white on magenta */
    video_mem[0] = (0x57 << 8) | 'M';
    video_mem[1] = (0x57 << 8) | 'D';
    video_mem[2] = (0x57 << 8) | 'P';

    while (1) __asm__ ("hlt");
}

您可以生成 ELF 可执行文件:

i686-elf-gcc -c -g -m32 boot.S -o boot.o
i686-elf-gcc -c -g -m32 -O3 kernel.c -o kernel.o -ffreestanding -std=gnu99 \
    -mno-red-zone -fno-exceptions -Wall -Wextra

i686-elf-gcc -nostdlib -Wl,--build-id=none -T linker.ld boot.o kernel.o -lgcc -o kernel.elf

评论:

  • 我更喜欢使用 GCC 交叉编译器,尽管您也可以使用本地主机编译器。
  • 文件 kernel.elf 可以通过 GRUB(在 ISO 文件中)或直接使用 QEMU-kernel 选项加载。
  • 我通过直接跳转到标签 StartInHigherHalf 简化了 JMP。您仍然可以使用 LEA / JMP 方法,但它不会获得任何收益。
  • 您在 .bss 部分中错误地放置了 gdtFlush。它需要移动到 .text 部分。我已经在代码中这样做了,但我也将其注释掉以使这个不执行任何操作的内核可以处理分页。您需要将其重新添加以使用您的代码库。
  • 我暂时注释掉了 call _init 以使这个什么都不做的内核工作。您需要删除评论才能将其集成到您的代码库中。