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.