Skip to content

Debugging Guide

NtinosTheGamer2324 edited this page Dec 11, 2025 · 1 revision

Debugging Guide

Comprehensive guide to debugging ModuOS.

Debugging Tools

Serial Port Logging (COM)

Primary method for debugging kernel code.

Setup: Automatically enabled in run.bat

Viewing Logs:

# View in real-time
tail -f com1.log
tail -f com2.log

# Or use Log Viewer (Windows)
vendor/NTSoftware/Log Viewer.exe com1.log

Adding Debug Output:

#include "moduos/kernel/COM/com.h"

COM_LOG_INFO(COM1_PORT, "Starting initialization");
COM_LOG_DEBUG(COM1_PORT, "Variable value: %d", value);
COM_LOG_ERROR(COM1_PORT, "Failed to initialize");

Log Levels:

  • COM_LOG_INFO: General information
  • COM_LOG_DEBUG: Detailed debug info
  • COM_LOG_WARN: Warnings
  • COM_LOG_ERROR: Errors

VGA Text Output

For early boot before COM is initialized:

#include "moduos/drivers/graphics/VGA.h"

VGA_print("Debug message\n");
VGA_print_hex(value);

QEMU Monitor

Access: Press Ctrl+Alt+2 in QEMU window

Useful Commands:

info registers          # Show all CPU registers
info mem                # Memory mappings
info pic                # PIC status
info tlb                # TLB entries
x/10i $rip              # Disassemble 10 instructions
x/16x 0xaddress         # Dump memory

Switch back to console: Ctrl+Alt+1

GDB Debugging

Setup

Start QEMU with GDB server:

qemu-system-x86_64 -cdrom dist/AMD64/kernel.iso -s -S

Options:

  • -s: Enable GDB server on port 1234
  • -S: Pause CPU at startup

Connect GDB:

gdb dist/AMD64/mdsys.sqr
(gdb) target remote localhost:1234
(gdb) break kernel_main
(gdb) continue

Useful GDB Commands

# Breakpoints
break kernel_main       # Set breakpoint at function
break *0x100000         # Set breakpoint at address
info breakpoints        # List breakpoints
delete 1                # Delete breakpoint 1

# Execution
continue (c)            # Continue execution
step (s)                # Step into
next (n)                # Step over
finish                  # Step out

# Inspection
print variable          # Print variable value
print /x variable       # Print in hex
x/10i $rip             # Disassemble
x/16x address          # Dump memory
backtrace (bt)         # Stack trace
info registers         # Show registers

# Memory
x/10gx $rsp            # Show stack (10 qwords)
watch variable         # Watchpoint on write

GDB with Symbols

Compile with debug symbols:

CFLAGS += -g

This allows GDB to show source code and variable names.

Common Debugging Scenarios

Kernel Panic

Symptom: System halts with panic message

Debug:

  1. Check VGA output for panic message
  2. Review com1.log for stack trace
  3. Note the faulting address and registers

Example Panic:

KERNEL PANIC: Page Fault
Faulting address: 0x0000000000000000
Error code: 0x0002 (Write to non-present page)
RIP: 0xFFFFFFFF80100123

Investigation:

# Find function at RIP
objdump -d dist/AMD64/mdsys.sqr | grep 80100123

Triple Fault

Symptom: QEMU resets immediately

Causes:

  • Invalid IDT entry
  • Stack overflow
  • Page fault in fault handler
  • Invalid page tables

Debug:

  1. Add -d int,cpu_reset to QEMU
  2. Check qemu.log for fault details
  3. Use GDB to step through early boot

Hang/Freeze

Symptom: System stops responding

Debug:

  1. Check if interrupts are enabled: sti called?
  2. Look for infinite loops
  3. Check if waiting for hardware that never responds

GDB Method:

# Attach GDB to running instance
gdb dist/AMD64/mdsys.sqr
(gdb) target remote localhost:1234
(gdb) interrupt
(gdb) backtrace

Memory Corruption

Symptom: Random crashes, data corruption

Debug:

  1. Check for buffer overflows
  2. Verify malloc/free pairs
  3. Check stack alignment
  4. Use memory debugging

Add Memory Guards:

#define GUARD_VALUE 0xDEADBEEF

void *kmalloc_debug(size_t size) {
    void *ptr = kmalloc(size + 8);
    *(uint32_t*)ptr = GUARD_VALUE;
    *(uint32_t*)(ptr + 4 + size) = GUARD_VALUE;
    return ptr + 4;
}

void kfree_debug(void *ptr) {
    ptr -= 4;
    assert(*(uint32_t*)ptr == GUARD_VALUE);
    kfree(ptr);
}

Page Fault

Error Code Bits:

  • Bit 0: Page present (0=not present, 1=protection)
  • Bit 1: Write access (0=read, 1=write)
  • Bit 2: User mode (0=kernel, 1=user)
  • Bit 3: Reserved bit violation
  • Bit 4: Instruction fetch

Handler:

void page_fault_handler(registers_t *regs) {
    uint64_t cr2;
    __asm__ volatile("mov %%cr2, %0" : "=r"(cr2));
    
    uint32_t err = regs->err_code;
    
    COM_LOG_ERROR(COM1_PORT, "Page Fault at 0x%llx", cr2);
    COM_LOG_ERROR(COM1_PORT, "Present: %d", err & 1);
    COM_LOG_ERROR(COM1_PORT, "Write: %d", (err >> 1) & 1);
    COM_LOG_ERROR(COM1_PORT, "User: %d", (err >> 2) & 1);
}

Debugging Techniques

Printf Debugging

Simple but effective:

COM_LOG_DEBUG(COM1_PORT, "Reached checkpoint 1");
COM_LOG_DEBUG(COM1_PORT, "Variable x = %d", x);

Binary Search

For isolating bugs:

  1. Add debug print at middle of suspicious code
  2. If bug occurs before print, search first half
  3. If bug occurs after print, search second half
  4. Repeat until bug location found

Assertion

Catch invalid states early:

#define assert(expr) \
    if (!(expr)) panic("Assertion failed: " #expr)

assert(ptr != NULL);
assert(size > 0);
assert(index < array_length);

Memory Dump

Inspect memory contents:

void hexdump(void *addr, size_t len) {
    uint8_t *ptr = (uint8_t*)addr;
    for (size_t i = 0; i < len; i += 16) {
        COM_LOG_DEBUG(COM1_PORT, "%p: ", ptr + i);
        for (size_t j = 0; j < 16 && i + j < len; j++) {
            COM_LOG_DEBUG(COM1_PORT, "%02x ", ptr[i + j]);
        }
        COM_LOG_DEBUG(COM1_PORT, "\n");
    }
}

QEMU Debugging Flags

Enhanced Debug Output

qemu-system-x86_64 \
    -cdrom kernel.iso \
    -d int,cpu_reset,guest_errors \
    -D qemu.log

Options:

  • -d int: Log all interrupts
  • -d cpu_reset: Log CPU resets (triple faults)
  • -d guest_errors: Log guest errors
  • -D qemu.log: Output to file

No Reboot

qemu-system-x86_64 -cdrom kernel.iso -no-reboot

Prevents automatic restart on triple fault, shows error state.

Stack Traces

Manual Stack Walk

void print_stack_trace(void) {
    uint64_t *rbp;
    __asm__ volatile("mov %%rbp, %0" : "=r"(rbp));
    
    for (int i = 0; i < 10 && rbp; i++) {
        uint64_t rip = *(rbp + 1);
        COM_LOG_DEBUG(COM1_PORT, "  [%d] RIP: 0x%llx", i, rip);
        rbp = (uint64_t*)*rbp;
    }
}

Objdump Symbols

# Find function containing address
objdump -d dist/AMD64/mdsys.sqr | grep -B5 address

# Disassemble specific function
objdump -d dist/AMD64/mdsys.sqr | grep -A20 "kernel_main:"

Performance Debugging

Timing Code

uint64_t rdtsc(void) {
    uint32_t lo, hi;
    __asm__ volatile("rdtsc" : "=a"(lo), "=d"(hi));
    return ((uint64_t)hi << 32) | lo;
}

uint64_t start = rdtsc();
// ... code to measure ...
uint64_t end = rdtsc();
COM_LOG_DEBUG(COM1_PORT, "Cycles: %llu", end - start);

Best Practices

  1. Add logging early: Easier to remove than to add later
  2. Use descriptive messages: Include context and values
  3. Check return values: Don't ignore errors
  4. Validate inputs: Assert preconditions
  5. Use version control: Git bisect to find regressions
  6. Test incrementally: Small changes are easier to debug
  7. Keep backups: Known-good versions to compare against

Next Steps

Clone this wiki locally