Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Memdump

This is a handy little function mentioned in the bonus and of course we aren’t leaving anything on the table here.

Memdump allows us to dump the raw contents of any mapped memory region directly to the screen in classic hex dump format:

0x00100000: 02 B0 AD 1B 03 00 00 00  FB 4F 52 E4 00 00 00 00
0x00100010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00

Those first bytes 02 B0 AD 1B are actually our Multiboot magic number 0x1BADB002 in little-endian format — we’re literally reading our own kernel binary off the metal. Pretty cool.

NPS Argument Parsing

To make memdump work, NPS needed to support arguments. Rather than a full parser, I went for a middle ground — splitn from Rust’s core library splits the command string on spaces without any heap allocation:

#![allow(unused)]
fn main() {
let mut parts = cmd_str.splitn(3, ' ');
let cmd = parts.next().unwrap_or("");
let arg1 = parts.next();
let arg2 = parts.next();
}

splitn(3, ' ') gives us at most 3 parts — the command, and up to two arguments. Clean and simple, no Vec needed.

klib — convert.rs

Making this function kicked off klib, our standard library replacement that will grow with each KFS phase. The first two additions live in klib/convert.rs.

parse_hex converts a hex string like "0x100000" to a u32. It strips the "0x" prefix, then processes each character using the same logic as atoi_base in C — subtracting ASCII offsets to get the digit value, building up the result in base 16:

#![allow(unused)]
fn main() {
pub fn parse_hex(s: &str) -> Option<u32> {
    let s = s.trim_start_matches("0x");
    let mut result: u32 = 0;
    for c in s.chars() {
        let digit = if c >= '0' && c <= '9' {
            c as u32 - '0' as u32
        } else if c >= 'a' && c <= 'f' {
            c as u32 - 'a' as u32 + 10
        } else if c >= 'A' && c <= 'F' {
            c as u32 - 'A' as u32 + 10
        } else {
            return None; // invalid character
        };
        result = result * 16 + digit;
    }
    Some(result)
}
}

parse_u32 is the decimal equivalent — same pattern, base 10, no prefix:

#![allow(unused)]
fn main() {
pub fn parse_u32(s: &str) -> Option<u32> {
    let mut result: u32 = 0;
    for c in s.chars() {
        if c >= '0' && c <= '9' {
            result = result * 10 + (c as u32 - '0' as u32);
        } else {
            return None;
        }
    }
    Some(result)
}
}

Both return Option<u32> — None on invalid input — so the caller can handle bad arguments gracefully.

The Dump

The actual dump loop reads one byte at a time from the requested address, printing 16 bytes per row with a gap in the middle:

#![allow(unused)]
fn main() {
unsafe {
    if hex < 0x100000 || hex + num > 0x400000 {
        println!("Address out of mapped range (0x100000 - 0x400000)");
        return;
    }
    let ptr = hex as *const u8;
    for i in 0..num {
        if i % 16 == 0 {
            print!("\n0x{:08X}: ", hex + i);
        }
        if i % 16 == 8 {
            print!(" ");
        }
        let byte = *ptr.add(i as usize);
        print!("{:02X} ", byte);
    }
    println!();
}
}

The safety check is important — in bare metal there’s nothing stopping you from reading unmapped memory and causing a page fault. We restrict memdump to the identity mapped kernel region 0x100000 - 0x400000 where we know pages exist. Dumping vmalloc memory would require checking the page tables first — a future improvement.