【问题标题】:Instruction after paging is enabled doesn't appear to execute启用分页后的指令似乎没有执行
【发布时间】:2019-03-11 05:49:26
【问题描述】:

当我必须设置 EIP 寄存器时,程序没有跳转到正确的位置。我期待jmp *%ecx 使用 LEA 跳转到内存中的正确位置,将 EIP 设置为 0xC0100000 左右(标签:StartInHigherHalf)。我不认为kmain 是必要的,因为问题是在它被调用之前。反正我要发了。

我尝试在 QEMU 上使用 -d cpu 标志对其进行调试,并且在跳转(被 HLT 阻止)之前表示 ECX 未使用 LEA 函数加载。 LEA 指令是否有可能不执行?为什么会这样?我该如何解决?

引导.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 $0x00000010, %ecx 
    mov %ecx, %cr4    

    mov %cr0, %ecx
    or $0x80000000, %ecx
    mov %ecx, %cr0

    lea StartInHigherHalf, %ecx
    jmp *%ecx      

StartInHigherHalf:
    movl $0, (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 $0x10, %eax
    mov %eax, %ds               
    mov %eax, %es
    mov %eax, %gs
    mov %eax, %fs
    mov %eax, %ss

    ljmp $0x08, $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);
}

【问题讨论】:

  • 如果您只是在kmain 返回后才查看ecx 的值,您为什么认为它仍然具有LEA 设置的值?为什么不在 LEA 指令上设置断点?这似乎不是任何东西的 minimal reproducible example,因为您没有显示 kmain 的 asm,也没有显示您实际看到的与预期看到的任何数值。
  • 如果执行到达hlt / jmp 循环,那么jmp *%ecx 可能正在按您的预期工作,否则您可能不会结束。你真的只看ecx 就像你在第二段中所说的那样吗?如果是这样,您为什么不使用调试器在其他东西破坏它之前更早地查看 ECX?
  • ECX 以 0x80000011 的值到达跳转点,因为在我使用该值设置 cr0 之前。要运行 QEMU,我只需传递“-cdrom MyOSName.iso”,而对于调试,我也传递“-d cpu”。

标签: assembly x86 paging bootloader osdev


【解决方案1】:

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

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

.fill repeat , size , value

result、size 和 value 是绝对表达式。这会发出 size 字节的重复副本。重复可能为零或更多。 size 可能是零或更大,但如果大于 8,则认为其值为 8,与其他人的汇编程序兼容。每个重复字节的内容取自一个 8 字节的数字。最高 4 个字节为零。最低顺序的 4 个字节是在计算机上按整数的字节顺序呈现的值,正如正在组装的那样。重复中的每个 size 字节取自该数字的最低顺序 size 字节。同样,这种奇怪的行为与其他人的汇编程序兼容。

大小和值是可选的。如果第二个逗号和 value 不存在,则 value 假定为零。如果第一个逗号和后面的标记不存在,则假定大小为 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 内核的变体。主要区别似乎是您从 NASM 转换为 GNU Assembler。我在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 $0x00000010, %ecx
    mov %ecx, %cr4

    mov %cr0, %ecx
    or $0x80000000, %ecx
    mov %ecx, %cr0

    jmp StartInHigherHalf

.section .text
StartInHigherHalf:
    movl $0, (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 $0x10, %eax
    mov %eax, %ds
    mov %eax, %es
    mov %eax, %gs
    mov %eax, %fs
    mov %eax, %ss

    ljmp $0x08, $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 方法,但它没有任何收获。
  • 您错误地将gdtFlush 放在.bss 部分。它需要移动到.text 部分。我已经在代码中这样做了,但我也将它注释掉以使这个无操作内核与分页一起工作。您需要将其重新添加以使用您的代码库。
  • 我暂时注释掉了call _init 以使这个无操作内核工作。您需要删除评论才能将其集成到您的代码库中。

【讨论】:

  • 现在可以了!太感谢了!但是现在当我想打印到屏幕上时,QEMU 重新启动......我在 0xB8000 和 0xC00B8000 尝试了 VGA_MEMORY,但没有任何效果
  • @MarcoMer 可能是因为您的代码取消了前 4MB 的身份映射,其中包括视频内存地址 0xb8000 。由于您使用movl $0, (BootPageDirectory) invlpg (0) 删除了它,因此您要么必须删除这两行以保留前 4MB 标识映射,要么必须使用高半地址 0xc00b8000 而不是 0xb8000
  • @MarcoMer 如果 0xc00b8000 不起作用,您的打印代码有问题,或者我上面建议的代码有问题。您可以就此提出另一个问题。
  • 现在我解决了这个问题,它工作了。非常感谢!
猜你喜欢
  • 1970-01-01
  • 2022-01-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-06-07
  • 1970-01-01
相关资源
最近更新 更多