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
Here are my files for reference.
/* 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");
}
}
ENTRY(_start)
SECTIONS {
. = 0x1000; /* Match the address in the bootloader */
.text : {
*(.text)
}
.data : {
*(.data)
}
.bss : {
*(.bss)
}
}
; 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 - 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:
# 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.
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