I have just recently gone into Protected Mode when developing an OS from scratch. I have managed to get into C and make functions to print characters to the screen (thanks Michael Petch for helping me reach this stage). Anyway, whenever I try to make a routine that loops through a string literal and prints every character in it, well, there's a bit of a problem. QEMU just goes into a boot loop, restarts again and again, and I am never able to see my beautiful green-on-black video mode. If I move this out of a routine and print it character-by-character in the kmain()
function (that part of which I have removed), everything works fine and dandy. Here's the file where I try to implement a string printing function:
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]);
}
}
I read my kernel into memory with this bootloader code:
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
I have an assembly stub at the start of my kernel that enters protected mode, zeroes the BSS section, issues a CLD and calls into my C code:
; 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
I have a specialized linker script that places the bootloader at 0x7c00 and the kernel at 0x7e00.
What's the problem and how can I fix it? I've made my git repo available if more information is needed.
TL;DR : You haven't read your entire kernel into memory with your bootloader in start.asm
. Missing code and/or data is causing your kernel to crash with a triple fault which results in a reboot. You will need to read more sectors as your kernel grows.
I noticed that your generated lunaos.img
is larger than 1024 bytes. The bootloader is 512 bytes, and the kernel after it is slightly more than 512 bytes. That means the kernel now spans multiple sectors. In your kernel.asm
you load a single 512-byte sector with this code:
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
In particular:
mov al, 0x01 ; read one sector (512 bytes)
This is at the heart of your problem. Since you are booting as a floppy I'd recommend generating a 1.44MiB file and placing your bootloader and kernel in it with:
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
The first command makes a 1.44MiB file filled with zeros. The second uses conv=notrunc
to tell DD not truncate the file after writing. seek=0
tells DD to start writing at the first logical sector in the file. The result would be that os.bin
is placed inside of a 1.44MiB image starting at logical sector 0 without truncating the original file when finished.
A properly sized disk image of a known floppy disk size makes it easier to use in some emulators.
A 1.44MiB floppy has 36 sectors per track (18 sectors per head, 2 heads per track). If you run your code on real hardware, some BIOSes may not load across a track boundary. You're likely safe reading 35 sectors with your disk read. The first sector was read by the BIOS off track 0 head 0. There are 35 more sectors on the first track. I'd amend the line above to be:
mov al, 35 ; read 35 sectors (35*512 = 17920 bytes)
This would allow your kernel to be 35*512 bytes long = 17920 bytes with minimum hassles even on real hardware. Any larger than that you will have to consider modifying your bootloader with a loop that attempts to read more than one track. To complicate matters you'd have to concern yourself that larger kernels will eventually exceed the 64k segment limit. The disk reads would probably have to be modified to use a segment (ES) that isn't 0. If your kernel gets that large your bootloader can be fixed at that time.
Since you are in protected mode and using QEMU, I highly suggest you consider using a debugger. QEMU supports remote debugging with GDB. It's not difficult to set up and since you have generated a ELF executable of your kernel you also can use symbolic debugging.
You will want to add -Fdwarf
to your NASM assembly commands right after -felf32
to enable debug information. Add the -g
option to your GCC commands to enable debug information. The command below should start up your bootloader/kernel; automatically break on kmain
; use os.elf
for debug symbols; and display the source code and registers in the terminal.
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'
There are many tutorials on using GDB if you search with Google. There is a cheat sheet that describes most of the basic commands and their syntax.
If you ever find yourself in the future having troubles with Interrupts, GDT or paging I recommend using Bochs for debugging those aspects of an operating system. Although Bochs doesn't have a symbolic debugger it makes up for in being able to identify low level problems more easily than QEMU. Debugging real mode code like bootloaders is easier in Bochs given that it understands 20 bit segment:offset addressing unlike QEMU
User contributions licensed under CC BY-SA 3.0