Any attempt to put a string to the screen in Protected Mode causes reboot

0

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.

c
string
x86
osdev
protected-mode
asked on Stack Overflow Dec 21, 2017 by Safal Aryal • edited Dec 22, 2017 by Michael Petch

1 Answer

1

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.


Debugging

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.elffor 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

answered on Stack Overflow Dec 21, 2017 by Michael Petch • edited Feb 13, 2019 by Michael Petch

User contributions licensed under CC BY-SA 3.0