cassemblyx86bootloaderosdev

Can't get welcome messages to load in QEMU


I am trying to make my own OS and am currently stuck on one tiny problem.
My welcome messages are not loading in the terminal.

Other than that, everything seems to work fine. (I'm not too sure, so maybe there are other bugs as well.)

Here is what QEMU displays after running:

make clear
make 
make run

QEMU

Here are my files for reference.

kernel.c

/* kernel.c - Main kernel entry point */

/* Video memory address */
#define VIDEO_MEMORY 0xB8000
/* Color: white on black */
#define COLOR 0x0F

/* Function to write a character to video memory */
void putchar(char c, int x, int y) {
    unsigned char *video_memory = (unsigned char*)VIDEO_MEMORY;
    int offset = (y * 80 + x) * 2;
    video_memory[offset] = c;
    video_memory[offset + 1] = COLOR;
}

/* Function to clear the screen */
void clear_screen() {
    unsigned char *video_memory = (unsigned char*)VIDEO_MEMORY;
    for(int i = 0; i < 80 * 25 * 2; i += 2) {
        video_memory[i] = ' ';
        video_memory[i + 1] = COLOR;
    }
}

/* Function to print a string */
void print_string(const char *str, int x, int y) {
    int i = 0;
    while(str[i] != '\0') {
        putchar(str[i], x + i, y);
        i++;
    }
}

/* Main kernel function */
void kernel_main() {
    // Clear screen
    
    clear_screen();
    // Display a welcome message
    print_string("NOX OS Kernel Loaded Successfully!", 20, 10);
    print_string("Welcome to the dark side...", 25, 12);
    
    // Halt the CPU
    while(1) {
        __asm__ volatile("hlt");
    }
}

linker.ld

ENTRY(_start)

SECTIONS {
    . = 0x1000;    /* Match the address in the bootloader */
    
    .text : {
        *(.text)
    }
    
    .data : {
        *(.data)
    }
    
    .bss : {
        *(.bss)
    }
}

boot.asm

; boot.asm - A simple bootloader
[bits 16]
[org 0x7c00]

; Set up segments
cli
mov ax, 0
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7c00
sti

; Display a message
mov si, boot_message
call print_string

; Load the kernel from disk
mov ah, 0x02    ; BIOS read sector function
mov al, 15      ; Number of sectors to read (adjust as needed)
mov ch, 0       ; Cylinder number
mov cl, 2       ; Sector number (sectors start from 1, bootloader is at 1)
mov dh, 0       ; Head number
mov dl, 0       ; Drive number (0 = floppy disk)
mov bx, 0x1000  ; Memory location to load the kernel
int 0x13        ; Call BIOS interrupt
jc disk_error   ; Jump if error (carry flag set)

; Switch to protected mode
cli                    ; Disable interrupts
lgdt [gdt_descriptor]  ; Load GDT

; Set protected mode bit
mov eax, cr0
or eax, 0x1
mov cr0, eax

; Far jump to 32-bit code
jmp CODE_SEG:protected_mode_entry

disk_error:
    mov si, disk_error_msg
    call print_string
    jmp hang

; Infinite loop for when we're done
hang:
    jmp hang

; Print string routine
print_string:
    lodsb
    or al, al
    jz done
    mov ah, 0x0E
    int 0x10
    jmp print_string
done:
    ret

; Global Descriptor Table
gdt_start:
    ; Null descriptor
    dd 0x0
    dd 0x0
    
    ; Code segment descriptor
    dw 0xffff    ; Limit (bits 0-15)
    dw 0x0000    ; Base (bits 0-15)
    db 0x00      ; Base (bits 16-23)
    db 10011010b ; Access byte
    db 11001111b ; Flags and Limit (bits 16-19)
    db 0x0       ; Base (bits 24-31)
    
    ; Data segment descriptor
    dw 0xffff    ; Limit 
    dw 0x0000    ; Base (bits 0-15)
    db 0x00      ; Base (bits 16-23)
    db 10010010b ; Access byte
    db 11001111b ; Flags and Limit
    db 0x0       ; Base (bits 24-31)
gdt_end:

gdt_descriptor:
    dw gdt_end - gdt_start - 1  ; GDT size
    dd gdt_start                ; GDT address

; Constants
CODE_SEG equ 0x08
DATA_SEG equ 0x10

; Messages
boot_message db 'NOX OS Booting...', 0
disk_error_msg db 'Error loading kernel!', 0

[bits 32]
protected_mode_entry:
    ; Set up segment registers for protected mode
    mov ax, DATA_SEG
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    
    ; Set up a stack
    mov esp, 0x90000
    
    ; Far jump to the kernel using a code segment selector
    jmp CODE_SEG:0x1000

; Padding and boot signature
times 510-($-$$) db 0
dw 0xAA55

entry.asm

; entry.asm - Assembly entry point that calls our C kernel
[bits 32]
[global _start]
[extern kernel_main]  ; Make sure this matches your C function name

section .text
_start:
    ; We're already in protected mode, skip trying to use BIOS interrupts
    
    ; Show a debug character at position 3
    mov byte [0xB8004], 'E'
    mov byte [0xB8005], 0x07
    
    ; Set up kernel stack
    mov esp, kernel_stack_top
    
    ; Call the C kernel main function
    call kernel_main
    
    ; Kernel should never return, but if it does:
    cli                 ; Disable interrupts
    hlt                 ; Halt the CPU
    jmp $               ; Infinite loop
    
; Reserve space for the kernel stack
section .bss
align 16
kernel_stack_bottom:
    resb 16384  ; 16 KB for kernel stack
kernel_stack_top:

Makefile

# Compiler settings
CC = x86_64-elf-gcc
CFLAGS = -m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector -nostartfiles -nodefaultlibs -Wall -Wextra -c
ASM = nasm
ASMFLAGS = -f elf32
LD = x86_64-elf-ld
LDFLAGS = -T src/kernel/linker.ld -m elf_i386

# Directories
SRC_DIR = src
BUILD_DIR = build

# Files
BOOT_SRC = $(SRC_DIR)/boot/boot.asm
KERNEL_ENTRY = $(SRC_DIR)/kernel/entry.asm
KERNEL_SRC = $(SRC_DIR)/kernel/kernel.c
BOOT_BIN = $(BUILD_DIR)/boot.bin
KERNEL_OBJ = $(BUILD_DIR)/kernel.o
ENTRY_OBJ = $(BUILD_DIR)/entry.o
KERNEL_BIN = $(BUILD_DIR)/kernel.bin
OS_IMAGE = $(BUILD_DIR)/nox-os.img

# Build rules
all: $(OS_IMAGE)

$(BOOT_BIN): $(BOOT_SRC)
    $(ASM) -f bin $< -o $@

$(ENTRY_OBJ): $(KERNEL_ENTRY)
    $(ASM) $(ASMFLAGS) $< -o $@

$(KERNEL_OBJ): $(KERNEL_SRC)
    $(CC) $(CFLAGS) $< -o $@

$(KERNEL_BIN): $(ENTRY_OBJ) $(KERNEL_OBJ)
    $(LD) $(LDFLAGS) -o $@ $^

$(OS_IMAGE): $(BOOT_BIN) $(KERNEL_BIN)
    dd if=/dev/zero of=$@ bs=512 count=2880
    dd if=$(BOOT_BIN) of=$@ conv=notrunc
    # Ensure the kernel is properly aligned at sector 2
    dd if=$(KERNEL_BIN) of=$(OS_IMAGE) seek=1 conv=notrunc bs=512

run: $(OS_IMAGE)
    qemu-system-i386 -fda $(OS_IMAGE) -boot a -monitor stdio -d int -no-reboot

clean:
    rm -rf $(BUILD_DIR)/*
    mkdir -p $(BUILD_DIR)

I tried increasing the number of sectors read in boot.asm, but that didn't work either.


Solution

  • In your Makefile you have:

    LDFLAGS = -T src/kernel/linker.ld -m elf_i386
    KERNEL_BIN = $(BUILD_DIR)/kernel.bin
    [snip]
    $(KERNEL_BIN): $(ENTRY_OBJ) $(KERNEL_OBJ)
        $(LD) $(LDFLAGS) -o $@ $^
    [snip]
    

    Your linker script doesn't have an OUTPUT_FORMAT directive so the default will be an ELF file, not a binary file. The Makefile is creating an ELF file called kernel.bin. ELF files contain metadata and a header in them and as a result they can't be treated as a raw binary.

    The simplest fix is to change linker.ld by adding this directive at the top:

    OUTPUT_FORMAT(binary)
    

    The previous method should resolve your problem, but it isn't a method I recommend. An ELF file can be very valuable when debugging code with GDB while connected to QEMU if it contains debug information.

    I would generate a file called kernel.elf and then use OBJCOPY to convert that ELF file to a file called kernel.bin. You can change your Makefile to look something like:

    # Compiler settings
    CC = x86_64-elf-gcc
    CFLAGS = -g -m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector -nostartfiles -nodefaultlibs -Wall -Wextra -c
    ASM = nasm
    ASMFLAGS = -gdwarf -f elf32
    LD = x86_64-elf-ld
    LDFLAGS = -T src/kernel/linker.ld -m elf_i386
    OBJCOPY = x86_64-elf-objcopy
    
    # Directories
    SRC_DIR = src
    BUILD_DIR = build
    
    # Files
    BOOT_SRC = $(SRC_DIR)/boot/boot.asm
    KERNEL_ENTRY = $(SRC_DIR)/kernel/entry.asm
    KERNEL_SRC = $(SRC_DIR)/kernel/kernel.c
    BOOT_BIN = $(BUILD_DIR)/boot.bin
    KERNEL_OBJ = $(BUILD_DIR)/kernel.o
    ENTRY_OBJ = $(BUILD_DIR)/entry.o
    KERNEL_BIN = $(BUILD_DIR)/kernel.bin
    KERNEL_ELF = $(BUILD_DIR)/kernel.elf
    OS_IMAGE = $(BUILD_DIR)/nox-os.img
    
    # Build rules
    all: $(OS_IMAGE)
    
    $(BOOT_BIN): $(BOOT_SRC)
            $(ASM) -f bin $< -o $@
    
    $(ENTRY_OBJ): $(KERNEL_ENTRY)
            $(ASM) $(ASMFLAGS) $< -o $@
    
    $(KERNEL_OBJ): $(KERNEL_SRC)
            $(CC) $(CFLAGS) $< -o $@
    
    $(KERNEL_ELF): $(ENTRY_OBJ) $(KERNEL_OBJ)
            $(LD) $(LDFLAGS) -o $@ $^
    
    $(KERNEL_BIN): $(KERNEL_ELF)
            $(OBJCOPY) -O binary $^ $@
    
    $(OS_IMAGE): $(BOOT_BIN) $(KERNEL_BIN)
            dd if=/dev/zero of=$@ bs=512 count=2880
            dd if=$(BOOT_BIN) of=$@ conv=notrunc
            # Ensure the kernel is properly aligned at sector 2
            dd if=$(KERNEL_BIN) of=$(OS_IMAGE) seek=1 conv=notrunc bs=512
    
    run: $(OS_IMAGE)
            qemu-system-i386 -fda $(OS_IMAGE) -boot a -monitor stdio -d int -no-reboot
    
    clean:
            rm -rf $(BUILD_DIR)/*
            mkdir -p $(BUILD_DIR)
    

    I've enabled debug info by passing -g to GCC and -gdwarf to NASM. You can use OBJDUMP to output your source code and the generated assembly as well as the section layout with x86_64-elf-objdump -DxS build/kernel.elf >objdump.txt.

    With a kernel.elf file you can use GDB's symbolic debugger to connect to QEMU with a script like:

    #!/bin/sh
    
    hdd=kernel/build/image.hdd
    
    qemu-system-i386 \
        -fda build/nox-os.img \
        -d int \
        -no-reboot \
        -no-shutdown -S -s &
    
    QEMU_PID=$!
    
    #        -ex 'layout src' \
    #        -ex 'layout regs' \
    
    gdb build/kernel.elf \
            -ex 'target remote localhost:1234' \
            -ex 'break kernel_main' \
            -ex 'continue'
    
    ps --pid $QEMU_PID > /dev/null
    if [ "$?" -eq 0 ]; then
        kill -9 $QEMU_PID
    fi
    
    stty sane