我最近刚进入保护模式,当开发一个操作系统从零开始。我已经成功地进入C,并使函数打印字符到屏幕上(感谢Michael帮助我达到这个阶段)。不管怎么说,每当我尝试做一个例程,循环一个字符串文字,并打印其中的每个字符,嗯,有一点问题。QEMU只是进入一个引导循环,一次又一次地重新启动,我永远无法看到我美丽的绿色对黑色视频模式。如果我将它从一个例程中移开,并在kmain()
函数中逐个打印它(我已经删除了这个部分),那么一切都很好。下面是我尝试实现字符串打印函数的文件:
vga.c -
#include <vga.h>
size_t terminal_row;
size_t terminal_column;
uint8_t terminal_color;
uint16_t *terminal_buffer;
volatile uint16_t * const VIDMEM = (volatile uint16_t *) 0xB8000;
size_t strlen(const char *s)
{
size_t len = 0;
while(s[len]) {
len++;
}
return len;
}
void terminal_init(void)
{
terminal_row = 0;
terminal_column = 0;
terminal_color = vga_entry_color(LGREEN, BLACK);
for(size_t y = 0; y < VGA_HEIGHT; y++) {
for(size_t x = 0; x < VGA_WIDTH; x++) {
const size_t index = y * VGA_WIDTH + x;
VIDMEM[index] = vga_entry(' ', terminal_color);
}
}
}
void terminal_putentryat(char c, uint8_t color, size_t x, size_t y)
{
const size_t index = y * VGA_WIDTH + x;
VIDMEM[index] = vga_entry(c, color);
}
void terminal_putchar(char c)
{
terminal_putentryat(c, terminal_color, terminal_column, terminal_row);
if(++terminal_column == VGA_WIDTH) {
terminal_column = 0;
if(++terminal_row == VGA_HEIGHT) {
terminal_row = 0;
}
}
}
void terminal_puts(const char *s)
{
size_t n = strlen(s);
for (size_t i=0; i < n; i++) {
terminal_putchar(s[i]);
}
}
我使用引导加载程序代码将内核读入内存:
extern kernel_start ; External label for start of kernel
global boot_start ; Make this global to suppress linker warning
bits 16
boot_start:
xor ax, ax ; Set DS to 0. xor register to itselfzeroes register
mov ds, ax
mov ss, ax ; Stack just below bootloader SS:SP=0x0000:0x7c00
mov sp, 0x7c00
mov ah, 0x00
mov al, 0x03
int 0x10
load_kernel:
mov ah, 0x02 ; call function 0x02 of int 13h (read sectors)
mov al, 0x01 ; read one sector (512 bytes)
mov ch, 0x00 ; track 0
mov cl, 0x02 ; sector 2
mov dh, 0x00 ; head 0
; mov dl, 0x00 ; drive 0, floppy 1. Comment out DL passed to bootloader
xor bx, bx ; segment 0x0000
mov es, bx ; segments must be loaded from non immediate data
mov bx, 0x7E00 ; load the kernel right after the bootloader in memory
.readsector:
int 13h ; call int 13h
jc .readsector ; error? try again
jmp 0x0000:kernel_start ; jump to the kernel at 0x0000:0x7e00
在内核开始时,我有一个程序集存根,它进入保护模式,对BSS部分进行0,发出一个CLD并调用我的C代码:
; These symbols are defined by the linker. We use them to zero BSS section
extern __bss_start
extern __bss_sizel
; Export kernel entry point
global kernel_start
; This is the C entry point defined in kmain.c
extern kmain ; kmain is C entry point
bits 16
section .text
kernel_start:
cli
in al, 0x92
or al, 2
out 0x92, al
lgdt[toc]
mov eax, cr0
or eax, 1
mov cr0, eax
jmp 0x08:start32 ; The FAR JMP is simplified since our segment is 0
section .rodata
gdt32:
dd 0
dd 0
dw 0x0FFFF
dw 0
db 0
db 0x9A
db 0xCF
db 0
dw 0x0FFFF
dw 0
db 0
db 0x92
db 0xCF
db 0
gdt_end:
toc:
dw gdt_end - gdt32 - 1
dd gdt32 ; The GDT base is simplified since our segment is now 0
bits 32
section .text
start32:
mov ax, 0x10
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov esp, 0x9c000 ; Set the stack to grow down from area under BDA/Video memory
; We need to zero out the BSS section. We'll do it a DWORD at a time
cld
lea edi, [__bss_start] ; Start address of BSS
lea ecx, [__bss_sizel] ; Lenght of BSS in DWORDS
xor eax, eax ; Set to 0x00000000
rep stosd ; Do clear using string store instruction
call kmain
我有一个专门的链接器脚本,它将引导程序放在0x7c00,内核放在0x7e00。
有什么问题,我该怎么解决呢?如果需要更多的信息,我已经提供了我的git回购。
发布于 2017-12-21 12:17:48
TL;DR:您还没有用start.asm
中的引导加载程序将整个内核读入内存。缺少代码和/或数据会导致内核出现三重故障,从而导致重新启动。随着内核的增长,您需要读取更多的扇区。
我注意到您生成的lunaos.img
大于1024字节。引导加载程序为512字节,后面的内核略大于512字节。这意味着内核现在跨越多个扇区。在kernel.asm
中,使用以下代码加载单个512字节扇区:
load_kernel:
mov ah, 0x02 ; call function 0x02 of int 13h (read sectors)
mov al, 0x18 ; read one sector (512 bytes)
mov ch, 0x00 ; track 0
mov cl, 0x02 ; sector 2
mov dh, 0x00 ; head 0
; mov dl, 0x00 ; drive 0, floppy 1. Comment out DL passed to bootloader
xor bx, bx ; segment 0x0000
mov es, bx ; segments must be loaded from non immediate data
mov bx, 0x7E00 ; load the kernel right after the bootloader in memory
.readsector:
int 13h ; call int 13h
jc .readsector ; error? try again
特别是:
mov al, 0x01 ; read one sector (512 bytes)
这是你问题的核心。由于您是作为软盘引导,我建议生成一个1.44MiB文件,并将引导加载程序和内核放在其中:
dd if=/dev/zero of=bin/lunaos.img bs=1024 count=1440
dd if=bin/os.bin of=bin/lunaos.img bs=512 conv=notrunc seek=0
第一个命令生成一个1.44MiB文件,其中填充了零。第二种方法是在编写完文件后使用conv=notrunc
告诉DD不要截断文件。seek=0
告诉DD开始编写文件中的第一个逻辑扇区。结果是,os.bin
放在1.44MiB映像中,从逻辑扇区0开始,完成时不截断原始文件。
具有已知软盘大小的适当大小的磁盘映像使其更易于在某些模拟器中使用。
A 1.44 per软盘每个磁道有36个扇区 (每头18个扇区,每个磁道2个磁头)。如果您在实际硬件上运行代码,一些BIOSes可能不会跨轨道边界加载。用磁盘读取35个扇区可能是安全的。第一个扇区被BIOS从0头0读取。第一轨道上还有35个行业。我会把上面的一行修改为:
mov al, 35 ; read 35 sectors (35*512 = 17920 bytes)
这将允许您的内核长度为35*512字节= 17920字节,即使在实际硬件上也是如此。如果大于此,您将不得不考虑使用尝试读取多个轨道的循环来修改引导加载程序。让事情变得更复杂的是,你必须担心更大的内核最终会超过64k的限制。磁盘读取可能需要修改才能使用不是0的段(ES)。如果您的内核有那么大,那么此时可以修复引导加载程序。
调试
由于您处于保护模式并使用QEMU,我强烈建议您考虑使用调试器。QEMU支持使用GDB进行远程调试。设置并不困难,而且由于您已经为内核生成了一个ELF可执行文件,所以您也可以使用符号调试。
您将希望将-Fdwarf
添加到-felf32
之后的NASM程序集命令中,以启用调试信息。在GCC命令中添加-g
选项以启用调试信息。下面的命令应该启动引导程序/内核;在kmain
上自动中断;使用os.elf
作为调试符号;并在终端中显示源代码和寄存器。
qemu-system-i386 -fda bin/lunaos.img -S -s &
gdb bin/os.elf \
-ex 'target remote localhost:1234' \
-ex 'layout src' \
-ex 'layout regs' \
-ex 'break *kmain' \
-ex 'continue'
如果你用谷歌搜索,有很多关于使用GDB的教程。有一个备忘单描述了大多数基本命令及其语法。
如果您发现自己将来遇到中断、GDT或分页问题,我建议您使用Bochs来调试操作系统的这些方面。虽然Bochs没有符号调试器,但它弥补了它比QEMU更容易识别低级别问题的缺点。在Bochs中调试真正的模式代码(如引导程序)更容易,因为它能够理解20位段:偏移地址与QEMU不同。
https://stackoverflow.com/questions/47927290
复制相似问题