Read and Print the Physical Memory Map
Mục tiêu: Modify the Rust kernel’s entry point to accept the BootInfo structure from the bootloader. Iterate over the provided memory map to read and print the details of each physical memory region, laying the groundwork for memory management and paging.
You have accomplished something truly remarkable. Your kernel is no longer a passive program executing in a straight line; it is a living, breathing system that reacts to the world around it. By handling CPU exceptions and hardware interrupts from both the timer and the keyboard, you have laid the complete foundation for event-driven programming, a cornerstone of any modern operating system. You’ve mastered the art of communication with external hardware.
Now, we pivot from the external to the internal. We move from the world of events and signals to the world of resources and ownership. The most fundamental resource an operating system must manage is memory. Currently, our kernel operates in a simple, direct world where every memory address we use corresponds to a real, physical location in RAM. This is known as “physical addressing.” For a toy kernel, it’s fine, but to build a robust, secure, and scalable OS, we must introduce a layer of abstraction: virtual memory, managed through a mechanism called paging.
Paging allows each program to have its own isolated address space, preventing it from interfering with the kernel or other programs. It’s the foundation of memory safety and modern multitasking. To build this system, we first need a map of the territory. We cannot manage memory if we don’t know which parts of it are available to use. Our very first task in this new grand challenge is to ask the bootloader for this map and learn to read it.
The Bootloader’s Gift: The Memory Map
When the bootimage tool loads our kernel, it first queries the firmware (BIOS or UEFI) to discover the layout of the machine’s physical memory. Not all physical memory is usable RAM. Some address ranges are reserved for hardware devices (like the VGA text buffer at 0xb8000), some contain ACPI data, and some might be marked as unusable or defective. The bootloader gathers all this information into a structure called a memory map and passes it to our kernel upon startup.
This memory map is our treasure map. It tells us exactly which ranges of physical memory are available for us to use for our kernel’s page tables, heap, and eventually, user-space programs. Our task is to read and understand this map.
Step 1: Receiving the BootInfo Structure
The bootloader doesn’t just pass the memory map; it passes a comprehensive BootInfo structure that contains the map and other useful information. To receive this structure, we must change the signature of our _start entry point function.
The bootloader crate, which bootimage uses under the hood, defines this structure. Let’s add it as a dependency to our project.
Open your Cargo.toml and add the highlighted line:
[dependencies]
spin = "0.5.2"
lazy_static = { version = "1.4.0", features = ["spin_no_std"] }
x86_64 = "0.14.10"
pic8259 = "0.10.1"
pc-keyboard = "0.7.0"
bootloader = "0.9.23"
Now, we can update _start in src/main.rs to accept the BootInfo structure as an argument.
Here are the changes for src/main.rs:
// In src/main.rs
// Add this `use` statement at the top
use bootloader::{BootInfo, entry_point};
// ... (other code like `println!`, `panic_handler`, etc.)
// The `entry_point` macro abstracts away the exact calling convention
// and ensures the function signature is correct for the bootloader.
entry_point!(kernel_main);
// Change the name of `_start` to `kernel_main` and update its signature
#[no_mangle]
fn kernel_main(boot_info: &'static BootInfo) -> ! {
println!("Hello, World!");
crate::interrupts::init();
x86_64::instructions::interrupts::enable();
println!("It did not crash!");
loop {}
}
Let’s break down this critical change:
use bootloader::{BootInfo, entry_point};: We import the necessary types from thebootloadercrate.entry_point!(kernel_main);: This macro is a fantastic feature of thebootloadercrate. It creates the real low-level_startentry point for us and ensures that theBootInfostructure is passed to ourkernel_mainfunction with the correct types and calling convention. It simplifies our code and makes it more robust.fn kernel_main(boot_info: &'static BootInfo) -> !: This is the new signature.- We’ve renamed
_starttokernel_mainas a convention when using theentry_point!macro. - It now accepts one argument:
boot_info. - The type is
&'static BootInfo. The&means we are receiving a reference to theBootInfostruct. The'staticlifetime annotation is a promise that this reference is valid for the entire runtime of our kernel. This is true because the bootloader places theBootInfostruct in a permanent memory location.
- We’ve renamed
Step 2: Iterating and Printing the Memory Map
Now that we have the boot_info object, we can access its memory_map field. This field is an iterator over MemoryRegion structs, each describing a segment of physical memory. Let’s iterate over it and print the details of each region.
Update your kernel_main function in src/main.rs to include the new loop:
// In src/main.rs
use bootloader::{BootInfo, entry_point};
use crate::println; // Ensure println is in scope
// ...
entry_point!(kernel_main);
fn kernel_main(boot_info: &'static BootInfo) -> ! {
println!("Hello, World!");
// --- Add the following code ---
use bootloader::bootinfo::MemoryRegionType;
println!("--- MEMORY MAP ---");
for region in boot_info.memory_map.iter() {
let start_addr = region.range.start_addr();
let end_addr = region.range.end_addr();
let region_type = region.region_type;
// Print the memory region details. The `{:?}` formatter prints
// a developer-friendly representation of the enum.
println!(
"[{:#018x} - {:#018x}] {:?}",
start_addr, end_addr, region_type
);
}
println!("--------------------");
// --- End of new code ---
crate::interrupts::init();
x86_64::instructions::interrupts::enable();
println!("It did not crash!");
loop {}
}
Let’s analyze the new code block: * use bootloader::bootinfo::MemoryRegionType;: We bring the MemoryRegionType enum into scope to use its variants. * for region in boot_info.memory_map.iter(): This is a standard Rust for loop that iterates over each MemoryRegion in the map. * let start_addr = region.range.start_addr();: We get the starting physical address of the memory region. * let end_addr = region.range.end_addr();: We get the ending physical address of the region. * let region_type = region.region_type;: We get the type of the region, which is an enum of type MemoryRegionType. It can be Usable, Reserved, ACPI, Bootloader, etc. * println!(...): We use our println! macro to print the details in a formatted way. * {:#018x}: This format specifier is for printing the 64-bit addresses. The # adds a 0x prefix, the 0 pads with leading zeros, 18 is the total width (2 for 0x + 16 for hex digits), and x specifies hexadecimal formatting. * {:?}: This is the debug formatter, which will print a nice string representation of the MemoryRegionType enum, like Usable or Reserved.
The Moment of Truth: Reading the Map
With these changes in place, run your kernel.
cargo run
You should now see a detailed memory map printed to the screen right after “Hello, World!”. The exact output will vary depending on QEMU’s configuration, but it will look something like this:
Hello, World!
--- MEMORY MAP ---
[0x0000000000000000 - 0x000000000009efff] Usable
[0x000000000009f000 - 0x000000000009ffff] Reserved
[0x00000000000f0000 - 0x00000000000fffff] Reserved
[0x0000000000100000 - 0x0000000007feffff] Usable
[0x0000000007ff0000 - 0x0000000007ffffff] Bootloader
[0x0000000008000000 - 0x00000000efffffff] Usable
[0x00000000fec00000 - 0x00000000fec00fff] Reserved
...
--------------------
It did not crash!
(Keyboard and timer interrupts continue as before)
This is a profound success. You have successfully received critical system information from the bootloader and parsed it within your kernel. You now possess the fundamental knowledge required to manage the system’s physical memory.
What’s Next?
Reading the map is the first step. Knowing which regions are “Usable” is one thing; actively managing that memory is another. The next logical task is to create data structures that will allow us to track and allocate memory from these usable regions. We need to represent the concepts of Pages (the unit of virtual memory), Frames (the unit of physical memory), and build a Frame Allocator that can hand out unused frames of physical memory on demand. This allocator will be the engine that drives our new paging system.
Further Reading
To learn more about the concepts introduced in this task, these resources are highly recommended: * The bootloader Crate Documentation: The official documentation for the BootInfo and MemoryMap structures. This is the definitive source for what the bootloader provides. * https://docs.rs/bootloader/latest/bootloader/bootinfo/struct.BootInfo.html * OSDev.org Wiki: Memory Map (x86): A deep dive into how the memory map is detected from the BIOS/UEFI and the different types of regions you might encounter. * https://wiki.osdev.org/Memory_Map_(x86) * The Rust Book: Lifetimes: A detailed explanation of the 'static lifetime, which you just used in your kernel_main function signature. * https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime
Establish Paging Vocabulary and Data Structures
Mục tiêu: Create a new memory module and import core paging types like Page, PhysFrame, VirtAddr, and PhysAddr from the x86\_64 crate to establish a type-safe foundation for memory management.
Excellent work! In the previous task, you successfully tapped into the bootloader’s knowledge, retrieving and displaying the physical memory map of your system. This map is your guide to the hardware, a definitive list of which physical memory ranges are usable, which are reserved, and which are occupied by other hardware. You have the “what” and the “where” of physical memory.
Now, we must define the “how.” How will we manage this memory? How will we abstract it? This brings us to the core of modern memory management: paging. Our current task is to establish the fundamental vocabulary and data structures for this new system. We need to create clear, type-safe representations for the concepts of pages, frames, and the page tables that link them together.
From Physical Addresses to a Virtual World
Before we write any code, let’s solidify our understanding of these three critical concepts. They are the pillars upon which our entire memory management system will be built.
- Frame (or Physical Frame): A frame is a contiguous, fixed-size block of physical memory. Think of it as a physical page of RAM in your computer’s hardware. On x86_64, a standard frame is 4 KiB (4096 bytes) in size. Frames are the real, tangible resource that we will allocate and manage using the memory map we just parsed.
- Page (or Virtual Page): A page is a contiguous, fixed-size block of virtual memory. This is a crucial abstraction. It’s the unit of memory that a program thinks it’s using. Like a frame, a page is also typically 4 KiB. The key idea is that a program’s pages are not directly tied to physical frames; they are an illusion maintained by the OS and the hardware.
- Page Table: This is the dictionary, the translation book that connects the virtual world of pages to the physical world of frames. It’s a hierarchical data structure that the CPU’s Memory Management Unit (MMU) uses to look up which physical frame corresponds to a given virtual page. When a program accesses a virtual address, the MMU walks the page tables to find the correct physical address in RAM.
Re-implementing these fundamental structures from scratch is a complex and error-prone task, involving a lot of bit-shifting, masking, and unsafe pointer manipulation. Fortunately, the x86_64 crate we are already using provides excellent, type-safe abstractions for all of these concepts. Leveraging this crate is a best practice, as it allows us to focus on the logic of memory management rather than the low-level implementation details, preventing a whole class of subtle bugs.
Creating the Memory Module
Good code organization is key to managing complexity. Let’s create a new module dedicated to all our memory management code.
First, create a new file: src/memory.rs.
Next, register this new module in your src/main.rs file by adding a mod declaration at the top.
// In src/main.rs
// ... (other attributes like #![no_std])
// Add this line to declare the new module
pub mod memory;
// ... (rest of the file)
Now, let’s populate our new src/memory.rs file. We won’t be defining new structs ourselves. Instead, we will import the powerful types provided by the x86_64 crate. This task is about understanding and adopting these correct representations.
// In src/memory.rs
use x86_64::{
structures::paging::{Page, PhysFrame, Mapper, Size4KiB, PageTable},
VirtAddr,
PhysAddr,
};
/// This module will contain all the logic for memory management,
/// including paging, frame allocation, and address translation.
///
/// For now, we are just defining the core concepts by importing
/// the necessary types from the `x86_64` crate. This allows us to
/// work with type-safe abstractions rather than raw addresses.
Let’s break down what we’ve just imported. This is the “creation of data structures” for this task:
VirtAddrandPhysAddr: These are not just simpleu64integers. They are wrapper types that provide crucial type safety. The Rust compiler will now prevent you from accidentally using aPhysAddrwhere aVirtAddris expected. They also have helpful methods for alignment and obtaining page/frame numbers.Page: This struct represents a virtual page. It wraps aVirtAddrand ensures that the address is “page-aligned” (i.e., a multiple of 4096).PhysFrame: Similarly, this struct represents a physical frame. It wraps aPhysAddrand ensures it is “frame-aligned.”Size4KiB: This is a “marker type” that specifies the size of our pages and frames. Thex86_64crate also supports larger page sizes (2MiB and 1GiB), but 4KiB is the standard and what we will use.PageTable: This struct represents one of the 512-entry tables in the 4-level hierarchy. It provides safe methods for accessing and modifying its entries.Mapper: This is a trait that defines the interface for a type that can perform virtual-to-physical address translation. We will use this in the next task.
Verifying Our Progress
This task was about setting up our conceptual framework and code structure. We haven’t created any new functionality, so there’s nothing new to see if you run the kernel.
A successful compilation is the sign of success for this task. Run cargo build to ensure the new module is correctly recognized.
cargo build
The build should complete without any errors. You have now successfully created the dedicated space for your memory management system and have armed it with a powerful, type-safe vocabulary for discussing pages, frames, and addresses.
What’s Next?
With our fundamental data structures and concepts in place, the next logical step is to put them into action. The core operation of a paging system is translating a virtual address into a physical address by walking the page table hierarchy. In the next task, you will implement a function that performs exactly this translation, giving you the first practical tool in your new memory management toolbox.
Further Reading
To deepen your understanding of the concepts introduced and the powerful abstractions you are now using, these resources are highly recommended:
- The
x86_64Crate Paging Documentation: The official documentation for the module containingPage,PhysFrame, and other types. This is your primary reference. - OSDev.org Wiki: Paging: A comprehensive and deep dive into how paging works on the x86_64 architecture at the hardware level. This will give you a great appreciation for what the crate is abstracting away.
- Philipp Oppermann’s Blog: A Closer Look at Page Tables: An excellent article from the author of the
x86_64crate that explains the page table layout in great detail.
Implement Virtual to Physical Address Translation
Mục tiêu: Create a function to translate virtual addresses to physical addresses by walking the x86_64 4-level page tables. This function will read the CR3 register, use a physical memory offset to access the tables from kernel virtual space, and handle cases for both mapped and unmapped pages.
In the previous task, you established the conceptual and structural foundation for our memory management system. By creating the src/memory.rs module and importing the type-safe abstractions from the x86_64 crate like Page, PhysFrame, VirtAddr, and PhysAddr, you’ve given our kernel a powerful and safe vocabulary to work with memory. You have the building blocks.
Now, it’s time to perform the first fundamental operation of any paging-based memory system: address translation. Our current task is to build a function that can take a virtual address—the kind of address our programs will use—and walk through the page tables to find the corresponding physical address in RAM. This process is the absolute core of how virtual memory works, and understanding it is key to mastering OS development.
The Great Translation: From Virtual to Physical
Every time your CPU executes an instruction that accesses memory (e.g., reading a variable, calling a function), its Memory Management Unit (MMU) performs this translation process automatically in hardware. The MMU uses a special register, CR3, which holds the physical address of the top-level (Level 4) page table. It then uses parts of the virtual address as indices to walk the four levels of tables to find the final physical address.
Our goal is to replicate this process in software. This is incredibly useful for the kernel, allowing it to inspect page tables, map new pages, and understand the memory layout of any process.
A 64-bit virtual address is structured like this:
| Bits 63-48 | Bits 47-39 | Bits 38-30 | Bits 29-21 | Bits 20-12 | Bits 11-0 |
|---|---|---|---|---|---|
| Sign Extend | P4 Index | P3 Index | P2 Index | P1 Index | Page Offset |
The translation process follows these steps:
- Use the P4 index to find an entry in the Level 4 table. This entry points to a Level 3 table.
- Use the P3 index to find an entry in the Level 3 table. This entry points to a Level 2 table.
- Use the P2 index to find an entry in the Level 2 table. This entry points to a Level 1 table.
- Use the P1 index to find an entry in the Level 1 table. This entry points to a physical frame.
- Add the Page Offset to the start address of the physical frame to get the final physical address.
The Kernel’s Dilemma: Accessing Physical Memory
There’s a crucial challenge we must address. The page tables themselves reside in physical memory, and the addresses stored within them are also physical addresses. However, our kernel code is running in a virtual address space. The CPU and kernel can only directly access memory using virtual addresses.
So, how do we read a page table located at a physical address?
The bootloader solves this for us by setting up a simple but effective mapping: it maps the entire physical memory of the computer into the kernel’s virtual address space, starting at a specific offset. This physical_memory_offset is provided to us in the BootInfo struct. By adding this offset to any physical address, we get a virtual address that our kernel can use to access that same piece of physical memory.
Implementing the Translation Function
Let’s write the code. We’ll create a new, unsafe function in src/memory.rs that performs this translation. It’s unsafe because we will be dereferencing raw pointers derived from physical addresses, and we must guarantee to the compiler that the physical_memory_offset is correct and that the page tables are valid.
Open your src/memory.rs file and add the following function:
// In src/memory.rs
use x86_64::{
structures::paging::{PageTable, PhysFrame, Page, Mapper, Size4KiB},
VirtAddr, PhysAddr,
};
/// Translates the given virtual address to the mapped physical address, or
/// `None` if the address is not mapped.
///
/// This function is unsafe because the caller must guarantee that the
/// complete physical memory is mapped to virtual memory at the passed
/// `physical_memory_offset`. Also, this function must be only called with an
/// active level 4 page table of the address space that should be translated.
pub unsafe fn translate_addr(addr: VirtAddr, physical_memory_offset: VirtAddr)
-> Option<PhysAddr>
{
// Retrieve a reference to the active level 4 table from the CR3 register.
// The CR3 register stores the physical frame of the P4 table.
use x86_64::registers::control::Cr3;
let (level_4_table_frame, _) = Cr3::read();
// Each page table consists of 512 entries. The 9 bits for each level's
// index in the virtual address correspond to these 512 entries.
let table_indexes = [
addr.p4_index(), addr.p3_index(), addr.p2_index(), addr.p1_index()
];
let mut frame = level_4_table_frame;
// Traverse the page tables level by level.
for &index in &table_indexes {
// Convert the frame to a virtual address so we can access it.
let virt = physical_memory_offset + frame.start_address().as_u64();
// Get a reference to the page table.
let table_ptr: *const PageTable = virt.as_ptr();
let table = &*table_ptr;
// Read the page table entry and check if it's present.
let entry = &table[index];
frame = match entry.frame() {
Ok(frame) => frame,
Err(e) if e.is_not_present() => return None, // Page not mapped
Err(e) => panic!("Invalid page table entry: {:?}", e),
};
}
// Calculate the final physical address by adding the page offset.
Some(frame.start_address() + addr.page_offset())
}
Let’s dissect this critical function:
pub unsafe fn translate_addr(...): The function is public somain.rscan call it, andunsafebecause the caller must uphold the contract thatphysical_memory_offsetis correct.Cr3::read(): We read theCR3register to get the physical frame where the active Level 4 page table is located. This is our starting point.table_indexes: Thex86_64crate provides convenient methods (p4_index(), etc.) onVirtAddrto extract the 9-bit indices for each table level.- The
forloop: This is the heart of the translation. It iterates through the four levels. let virt = physical_memory_offset + frame.start_address().as_u64();: This is where we apply the magic offset.frame.start_address()gives us aPhysAddr. We add our offset to it to get aVirtAddrthat our kernel can safely access.let table = &*table_ptr;: We convert the virtual address to a raw pointer (*const PageTable) and then dereference it. This is one of the primaryunsafeoperations.let entry = &table[index];: We access the entry in the current table using the index from our virtual address.match entry.frame(): An entry might not be valid.entry.frame()returns aResult. We handle theOkcase by updatingframeto point to the next level table. We handle theErrcase for a non-present page by returningNone, which is the expected outcome for an unmapped address. Any other error is unexpected and causes a panic.Some(frame.start_address() + addr.page_offset()): After successfully walking all four tables,framenow holds the physical frame of the target page. We add the final 12-bit offset from the originalVirtAddrto get the exact physical address.
Testing Our Translation Function
Now, let’s use our new function in kernel_main. We’ll get the physical memory offset from the BootInfo struct and test a few known addresses.
Update your src/main.rs file:
// In src/main.rs
// ... (existing use statements)
entry_point!(kernel_main);
fn kernel_main(boot_info: &'static BootInfo) -> ! {
println!("Hello, World!");
// This is the new part for testing our memory translation
// --- Start of new/modified code ---
// Initialize our memory management module, passing the physical memory offset.
let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);
// Test the translation with a few example addresses
let addresses = [
// the identity-mapped vga buffer page
0xb8000,
// some code page
0x201008,
// some stack page
0x0100_0020_1a10,
// should be mapped to physical address 0, but is not backed by a frame
0,
// a huge page that is not mapped
0xffff_8000_0000_0000,
];
for &address in &addresses {
let virt = VirtAddr::new(address);
// Use `unsafe` to call our new function
let phys = unsafe { memory::translate_addr(virt, phys_mem_offset) };
println!("{:?} -> {:?}", virt, phys);
}
// --- End of new/modified code ---
// The rest of the kernel initialization proceeds as before
crate::interrupts::init();
x86_64::instructions::interrupts::enable();
println!("It did not crash!");
loop {}
}
Let’s review the changes in kernel_main:
let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);: We retrieve the offset from theboot_infostruct and wrap it in our type-safeVirtAddrstruct.unsafe { memory::translate_addr(...) }: We call our new function inside anunsafeblock, fulfilling our part of theunsafecontract.println!: We print the virtual address and the result of the translation (Some(PhysAddr)orNone) to see if it worked.
The Moment of Truth
Run your kernel now.
cargo run
You should see output that clearly shows the results of the address translation. The exact physical addresses might differ slightly, but the pattern will be the same:
Hello, World!
VirtAddr(0xb8000) -> Some(PhysAddr(0xb8000))
VirtAddr(0x201008) -> Some(PhysAddr(0x201008))
VirtAddr(0x10000201a10) -> Some(PhysAddr(0x41a10))
VirtAddr(0x0) -> None
VirtAddr(0xffff800000000000) -> None
It did not crash!
(Keyboard and timer interrupts will continue)
This is a fantastic result!
- You can see that the VGA buffer and code page are identity-mapped: their virtual address is the same as their physical address. This is a common setup for the kernel itself.
- The stack address translates to a different physical address, showing a non-identity mapping.
- The address
0x0is a null pointer. It’s unmapped by default for safety, so our function correctly returnsNone. - The very high address is also unmapped, correctly returning
None.
You have successfully implemented and verified the single most important operation in a virtual memory system.
What’s Next?
So far, we have only read the page tables that the bootloader created for us. The real power of an OS comes from being able to modify these tables to create its own address spaces. The next logical step is to create a new, empty Level 4 page table, which will serve as the blank canvas for our kernel’s own, carefully controlled memory mappings.
Further Reading
- OSDev.org Wiki: Page Tables: An invaluable, low-level resource on the structure of x86_64 page tables.
- Philipp Oppermann’s Blog: Paging Implementation: A brilliant, in-depth tutorial on implementing paging from which many of these concepts are drawn.
x86_64Crate:Cr3Register Documentation: The official documentation for the type used to interact with theCR3register.
Implement a Boot-Time Physical Frame Allocator
Mục tiêu: Create a physical frame allocator in Rust that uses the bootloader’s memory map to find and allocate usable 4KiB physical frames. This is a foundational step for creating and managing the kernel’s own page tables.
In the previous task, you built and successfully tested the translate_addr function. This was a monumental step, proving you could programmatically walk the page table hierarchy set up by the bootloader and translate a virtual address into its corresponding physical one. You were, in essence, reading the map of the memory world the bootloader created for you.
However, a true operating system is not just a reader of maps; it is a cartographer. It must create and manage its own memory worlds. Our journey now shifts from being a passive observer of the bootloader’s page tables to an active creator of our own. The very first step in this process is to create a blank canvas: a new, empty, top-level (P4) page table.
The Prerequisite: Allocating Physical Memory
There’s a fundamental “chicken-and-egg” problem we must solve first. A page table is a 4 KiB data structure that must reside somewhere in physical memory. To create a new P4 table, we need to find a 4 KiB block of unused physical RAM—a PhysFrame—to store it in.
How do we find an available frame? We can’t just pick a random physical address; it might already be in use by our kernel’s code, the VGA buffer, or other hardware. The solution is to create a physical frame allocator. This is a piece of software responsible for managing the physical memory of the system. It uses the memory map we parsed in the first task of this step to know which frames are Usable and can be safely allocated.
We will build a simple but effective allocator that gets the job done. It will iterate through the usable memory regions provided by the bootloader and hand out frames one by one.
Implementing a Boot-Time Frame Allocator
Let’s head to our src/memory.rs file. We will define a generic FrameAllocator trait and then provide a concrete implementation called BootInfoFrameAllocator.
// In src/memory.rs
// Add the `bootloader` crate to the `use` statements
use bootloader::bootinfo::{MemoryMap, MemoryRegionType};
use x86_64::{
structures::paging::{FrameAllocator, PhysFrame, Size4KiB},
// ... existing use statements
};
/// A FrameAllocator that returns usable frames from the bootloader's memory map.
pub struct BootInfoFrameAllocator {
memory_map: &'static MemoryMap,
next: usize,
}
impl BootInfoFrameAllocator {
/// Create a new FrameAllocator from the passed memory map.
///
/// This function is unsafe because the caller must guarantee that the passed
/// memory map is valid. The main requirement is that all frames that are marked
/// as `USABLE` in it are really unused.
pub unsafe fn init(memory_map: &'static MemoryMap) -> Self {
BootInfoFrameAllocator {
memory_map,
next: 0,
}
}
/// Returns an iterator over the usable frames in the memory map.
fn usable_frames(&self) -> impl Iterator<Item = PhysFrame> {
// Get an iterator over the usable memory regions
let regions = self.memory_map.iter();
let usable_regions = regions
.filter(|r| r.region_type == MemoryRegionType::Usable);
// Map each region to its address range
let addr_ranges = usable_regions
.map(|r| r.range.start_addr()..r.range.end_addr());
// Transform to an iterator of frame start addresses
let frame_addresses = addr_ranges.flat_map(|r| r.step_by(4096));
// Create `PhysFrame` types from the start addresses
frame_addresses.map(|addr| {
PhysFrame::containing_address(PhysAddr::new(addr))
})
}
}
// The `FrameAllocator` trait is part of the `x86_64` crate, but we implement it for our struct.
// This makes our allocator compatible with other functions in the crate that might need one.
unsafe impl FrameAllocator<Size4KiB> for BootInfoFrameAllocator {
fn allocate_frame(&mut self) -> Option<PhysFrame> {
// Get the next frame from our `usable_frames` iterator
let frame = self.usable_frames().nth(self.next);
self.next += 1;
frame
}
}
// ... existing translate_addr function
This is a significant amount of new code, so let’s break it down carefully:
BootInfoFrameAllocatorstruct: This struct holds a reference to the bootloader’smemory_mapand anextfield, which acts as a simple counter to track the next frame we should hand out.unsafe fn init(...): We create aninitfunction to construct our allocator. It’s markedunsafebecause the entire memory management system depends on thememory_mapbeing accurate. We, as the OS developers, are making a promise to the compiler that the memory map is valid.usable_frames(&self): This is the core logic. It’s a beautiful example of Rust’s iterators at work.- We get an iterator over all memory regions.
- We
filterit to keep only the ones markedMemoryRegionType::Usable. - We
mapthese regions to their underlying address ranges. - We use
flat_mapandstep_by(4096)to convert the ranges of addresses into an iterator of individual frame start addresses (since each frame is 4KiB, or 4096 bytes). - Finally, we
mapthese numeric addresses into the type-safePhysFramestruct provided by thex86_64crate.
impl FrameAllocator<Size4KiB> for BootInfoFrameAllocator: We implement theFrameAllocatortrait from thex86_64crate. This makes our allocator compatible with other parts of the library. Theallocate_framemethod is required by the trait.allocate_frame(&mut self): Our implementation is simple. It gets an iterator of all usable frames and uses.nth(self.next)to pick the next available one. It then incrementsself.nextfor the subsequent call. It returnsNoneif the iterator is exhausted (i.e., we’re out of memory).
Creating the P4 Table
Now that we have a way to get an unused physical frame, we can finally complete our task. Let’s use our new allocator in kernel_main to create an empty Level 4 page table.
Update your src/main.rs file:
// In src/main.rs
// ... (other use statements)
use os_kernel::memory; // Make our memory module accessible
use x86_64::VirtAddr;
// ... (entry_point macro, etc.)
fn kernel_main(boot_info: &'static BootInfo) -> ! {
println!("Hello, World!");
let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);
//
// --- THIS IS THE NEW CODE ---
//
// Initialize a new `BootInfoFrameAllocator`.
// This is safe because the bootloader gives us a valid memory map.
let mut frame_allocator = unsafe {
memory::BootInfoFrameAllocator::init(&boot_info.memory_map)
};
// Allocate a frame for the P4 table.
let p4_frame = frame_allocator.allocate_frame().expect("out of memory");
// Get the physical address of the frame.
let p4_addr = p4_frame.start_address();
println!("New P4 Table at: {:?}", p4_addr);
// --- END OF NEW CODE ---
// The rest of the kernel can proceed as before for now...
crate::interrupts::init();
// ...
}
This new block of code in kernel_main performs the following actions:
memory::BootInfoFrameAllocator::init(...): We call theinitfunction we just wrote to create an instance of our frame allocator. We place this call in anunsafeblock, fulfilling the contract of the function.frame_allocator.allocate_frame(): We call our allocator to get our very first physical frame!.expect("out of memory"): Theallocate_framemethod returns anOption. For now, we know the system has plenty of memory, so we can useexpectto unwrap theSomevariant. If it were to fail, our kernel would panic with the given message.p4_frame.start_address(): We get the physical start address of the frame we just allocated.println!(...): We print the physical address of our new P4 table. This serves as our verification that the allocator is working correctly.
What to Expect
Let’s run the kernel and see the result.
cargo run
You will see the familiar kernel output, but with one new, important line. The exact address will vary, but it will look something like this:
Hello, World!
New P4 Table at: PhysAddr(0x7ff000)
It did not crash!
(Timer and keyboard interrupts continue)
This is a perfect result! It proves that your BootInfoFrameAllocator is correctly parsing the memory map, finding a usable frame, and handing it out. You have successfully allocated the physical memory needed for your new page table hierarchy. We haven’t written the PageTable struct to this memory yet, but we have reserved the space for it.
What’s Next?
We have a physical frame reserved for our new P4 table. However, it’s just a raw, empty block of memory. The next task is to take this frame and populate it with the essential mappings that our kernel needs to function. We will identity-map the kernel’s code sections and the VGA text buffer into our new page table. This will prepare it for the moment we eventually switch the CPU over to use our new memory map instead of the bootloader’s.
Further Reading
- Philipp Oppermann’s Blog: Paging Implementation: This series provides an excellent, detailed walkthrough of implementing a frame allocator and creating new page tables.
- OSDev.org Wiki: Frame Allocation: A good overview of different strategies and algorithms for frame allocation in operating systems.
- The Rust Programming Language: Iterators: A fantastic resource for mastering the iterator patterns we used in our
usable_framesfunction.
Creating Kernel Page Table Mappings
Mục tiêu: Populate a new Level 4 page table by creating identity mappings for essential kernel memory. This task uses the x86\_64 crate’s OffsetPageTable to map the VGA text buffer as a practical example and verifies it by writing directly to the screen.
In the previous task, you successfully built a physical frame allocator and used it to reserve a 4 KiB block of raw, physical memory for our new Level 4 page table. You have the physical address of a blank canvas. This is a crucial first step, but it’s also a dangerous one. If we were to tell the CPU to use this empty, uninitialized page table right now, it would immediately crash in a spectacular fashion—it wouldn’t even be able to fetch the next instruction of our kernel code, leading to a triple fault and a system reset.
Our current task is to take this blank canvas and paint the essential scenery our kernel needs to survive. We must populate our new P4 table with the critical mappings for the kernel’s own code, its data, and the hardware devices it interacts with, like the VGA text buffer. We will use a technique called identity mapping, which is a straightforward and common approach for a kernel’s own address space.
Identity Mapping and the OffsetPageTable
Identity Mapping is the concept of mapping a virtual page to a physical frame that has the same starting address. For example, we will map the virtual page starting at 0xb8000 to the physical frame that also starts at 0xb8000. This keeps things simple for the kernel itself.
To perform this mapping safely and efficiently, we will once again turn to the powerful abstractions in the x86_64 crate. The most important tool for this task is the OffsetPageTable struct. This is a high-level wrapper that takes two things:
- A mutable reference to the active Level 4 page table.
- The
physical_memory_offsetwe retrieved from the bootloader.
With this information, it provides safe methods like map_to that handle all the complex and unsafe logic of creating intermediate page tables (P3, P2, P1) and manipulating their entries. It cleverly uses the offset to translate the physical addresses found in page table entries into virtual addresses that our kernel code can actually access.
Creating a Reusable Mapping Function
Let’s head back to our src/memory.rs file. We are going to create a new unsafe function that encapsulates the entire process of creating and mapping our new page table.
// In src/memory.rs
// Add these to your `use` statements at the top
use x86_64::structures::paging::{PageTable, OffsetPageTable, PageTableFlags};
use x86_64::registers::control::Cr3;
// ... (BootInfoFrameAllocator implementation, translate_addr function) ...
/// Creates a new set of page tables and identity-maps the kernel.
///
/// This function is unsafe because the caller must guarantee that the
/// complete physical memory is mapped to virtual memory at the passed
/// `physical_memory_offset`. Also, this function must be only called once
/// to avoid aliasing `&mut` references (which is undefined behavior).
pub unsafe fn create_kernel_page_tables(
physical_memory_offset: VirtAddr,
frame_allocator: &mut impl FrameAllocator<Size4KiB>,
) -> OffsetPageTable<'static> {
// Get a mutable reference to the active level 4 table.
let level_4_table = active_level_4_table(physical_memory_offset);
OffsetPageTable::new(level_4_table, physical_memory_offset)
}
/// Returns a mutable reference to the active level 4 table.
///
/// This function is unsafe because the caller must guarantee that the
/// complete physical memory is mapped to virtual memory at the passed
/// `physical_memory_offset`. Also, this function must be only called once
/// to avoid aliasing `&mut` references (which is undefined behavior).
unsafe fn active_level_4_table(physical_memory_offset: VirtAddr)
-> &'static mut PageTable
{
let (level_4_table_frame, _) = Cr3::read();
let phys = level_4_table_frame.start_address();
let virt = physical_memory_offset + phys.as_u64();
let page_table_ptr: *mut PageTable = virt.as_mut_ptr();
&mut *page_table_ptr // unsafe
}
This code is a bit dense, so let’s break down these two new functions:
unsafe fn active_level_4_table(...): This helper function gives us a mutable reference (&'static mut) to the Level 4 page table currently being used by the CPU. It reads theCR3register to find the physical address, adds ourphysical_memory_offsetto get a virtual address the kernel can use, and then converts that raw pointer into a safe Rust mutable reference. Theunsafekeyword is critical here: we are making a strong promise that this raw pointer is valid and that we will not create multiple mutable references to it, which would be undefined behavior.pub unsafe fn create_kernel_page_tables(...): This is our main new function.- It takes the
physical_memory_offsetand a mutable reference to ourframe_allocator. - It calls our helper to get a reference to the active P4 table.
- It then constructs and returns an
OffsetPageTableinstance. This object is now our primary interface for modifying the page tables.
- It takes the
Populating the New Page Table
Now that we have an OffsetPageTable instance, we can use its powerful map_to method. Let’s create an example mapping function and then call it from kernel_main.
We will start by mapping the VGA buffer page. This will serve as a clear, single example of the map_to process.
// In src/memory.rs
// ... (other use statements)
use x86_64::structures::paging::{Page, PhysFrame, Mapper, Size4KiB, PageTable, PageTableFlags};
// This is a new function to demonstrate mapping.
/// Creates an example mapping for the given page to frame `0xb8000`.
pub fn create_example_mapping(
page: Page,
mapper: &mut OffsetPageTable,
frame_allocator: &mut impl FrameAllocator<Size4KiB>,
) {
use x86_64::structures::paging::PageTableFlags as Flags;
let frame = PhysFrame::containing_address(PhysAddr::new(0xb8000));
let flags = Flags::PRESENT | Flags::WRITABLE;
// The `map_to` method can fail, so we use `unwrap` for now.
// It requires a frame allocator to create new, lower-level page tables if needed.
let map_to_result = unsafe {
// FIXME: this is not safe, we do it only for testing
mapper.map_to(page, frame, flags, frame_allocator)
};
map_to_result.expect("map_to failed").flush();
}
Let’s dissect this example: * create_example_mapping(...): This function takes the page we want to map, a mutable reference to our mapper (OffsetPageTable), and our frame_allocator. * let frame = PhysFrame::containing_address(...): We define the physical frame we want to map to. Here, it’s the VGA buffer at 0xb8000. * let flags = Flags::PRESENT | Flags::WRITABLE;: We define the permissions for this page. * PRESENT: This bit tells the CPU that the mapping is valid. If it’s not set, accessing the page will cause a page fault. * WRITABLE: This bit allows write operations to the page. Without it, the memory would be read-only. We need this to write characters to the screen. * mapper.map_to(...): This is the core operation. It creates all the necessary P3, P2, and P1 tables (using frames from the frame_allocator if they don’t exist) and sets the final entry in the P1 table to point to the desired physical frame with the specified flags. * unsafe: The map_to method is unsafe because the caller must ensure that the target frame is not already in use for something else. * .flush(): After a page table modification, we must flush the Translation Lookaside Buffer (TLB). The TLB is a CPU cache for address translations. This call ensures the CPU sees our new mapping.
Calling it from kernel_main
Now, let’s update kernel_main in src/main.rs to use these new functions. We are not yet mapping the entire kernel, just this one example page to verify our logic.
// In src/main.rs
use os_kernel::memory;
use x86_64::{structures::paging::Page, VirtAddr};
// ...
fn kernel_main(boot_info: &'static BootInfo) -> ! {
println!("Hello, World!");
let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);
let mut mapper = unsafe { memory::create_kernel_page_tables(phys_mem_offset) };
let mut frame_allocator = unsafe {
memory::BootInfoFrameAllocator::init(&boot_info.memory_map)
};
// map the page that contains `0xb8000`
let page = Page::containing_address(VirtAddr::new(0xb8000));
memory::create_example_mapping(page, &mut mapper, &mut frame_allocator);
// write the string `New!` to the screen through the new mapping
let page_ptr: *mut u64 = page.start_address().as_mut_ptr();
unsafe { page_ptr.offset(400).write_volatile(0x_f021_f077_f065_f04e) };
crate::interrupts::init();
x86_64::instructions::interrupts::enable();
println!("It did not crash!");
loop {}
}
What to Expect
Let’s run the code.
cargo run
You will see the same output as before, with one exciting addition. The New! text appears on the screen, proving that our new mapping for the VGA buffer works!
Hello, World!
New!
It did not crash!
(Timer and keyboard interrupts continue)
This is a fantastic result! You have successfully:
- Created a high-level
OffsetPageTablemapper. - Used it to create a new mapping for the VGA buffer page.
- Verified the mapping works by writing to it.
We have successfully created a new page table and added our first essential mapping to it.
What’s Next?
We’ve mapped one page, which is great for a test. But for our kernel to function, we need to map all of its code and data. The bootloader actually provides us with the sections of our kernel ELF file, and in the next task, we will iterate over these sections to identity-map our entire kernel, preparing our new page tables for the final step: activating them. Correction: The prompt’s path doesn’t use the ELF sections directly, it uses a simpler, fixed-range mapping. The next task will be to activate this new table and load its address into the CR3 register. We’ll add the full kernel mapping there. For now, this successful single-page mapping is a complete and vital step. The next task is to load the physical address of our new P4 table into the CR3 register to activate it.
Further Reading
- The
x86_64Crate’sMapperTrait: TheOffsetPageTableimplements theMappertrait, which defines the interface for all mapping operations. Understanding this trait is key to advanced memory management. - OSDev.org Wiki: Page Table Entries: A deep dive into the format of a page table entry, explaining the
PRESENT,WRITABLE, and other flags in detail at the bit level. - Translation Lookaside Buffer (TLB): Learn more about this crucial CPU cache and why flushing it is necessary after modifying page tables.
Create and Activate New Kernel Page Tables
Mục tiêu: Construct a new set of kernel page tables, populate them with essential mappings for kernel code, the VGA buffer, and the page tables themselves, then activate the new virtual address space by loading the physical address of the Level 4 (P4) page table into the CR3 control register.
Excellent! You’ve made tremendous progress on your kernel’s memory management system. In the previous tasks, you built a physical frame allocator and experimented with the map_to function to successfully map the VGA buffer. This was a crucial “dry run” that proved our mapping logic is sound. We were, however, performing that test on the page tables provided by the bootloader.
The time has now come for the final, most critical act in this chapter of your kernel’s development: to construct our own, completely new set of page tables, populate them with all the mappings our kernel needs to survive, and then tell the CPU to switch to them. This is the moment your kernel truly takes control of the system’s memory.
Our current task is to load the physical address of our new, fully-prepared Level 4 (P4) page table into the CR3 control register. This single instruction is the switch that activates our new virtual address space.
The Point of No Return: The CR3 Register
The CR3 register is special. It holds the physical base address of the top-level (P4) page table that the CPU’s Memory Management Unit (MMU) will use for all subsequent address translations. Changing this value is an atomic operation that instantly redefines the entire memory landscape from the CPU’s perspective.
This is a powerful but dangerous operation. If our new page tables are incomplete or incorrect, the moment we write to CR3, the CPU will lose its footing. It might not even be able to fetch the very next instruction, leading to an immediate triple fault and a system reset. Therefore, before we can write to CR3, we must ensure our new page tables contain, at a minimum, identity mappings for:
- All of our kernel’s code and data sections.
- The VGA text buffer so we can continue printing.
- The very page table frames themselves that we are creating.
Let’s build a new master function in our memory module that prepares this new address space and then call it from kernel_main before flipping the switch.
Step 1: Creating and Populating the New Page Tables
We will create a new function in src/memory.rs that encapsulates the entire process. It will create a new P4 table, create a high-level OffsetPageTable mapper to manage it, and then map all
Implement a Page Fault Exception Handler
Mục tiêu: Create and register a robust exception handler for page faults (exception #14) in a Rust-based x86_64 kernel. The handler will read the faulting address from the CR2 register, parse the error code, print detailed diagnostic information, and then halt the system.
Congratulations! You have successfully navigated one of the most complex and rewarding challenges in operating system development. By creating a new set of page tables, populating them with the essential mappings for your kernel, and activating them by writing to the CR3 register, you have seized full control of the system’s virtual memory. Your kernel is no longer a guest in the bootloader’s memory space; it is now the master of its own domain.
With this great power comes great responsibility. Now that your kernel defines the rules of memory access, it must also be prepared to handle violations of those rules. What happens when code tries to access a virtual address that isn’t mapped to anything? Or tries to write to a read-only page? In these situations, the CPU doesn’t just crash; it triggers a specific, life-saving exception: a Page Fault (#PF).
Our final task in this step is to implement a robust handler for this critical exception. A good page fault handler is one of the most valuable debugging tools an OS developer can have. Instead of a cryptic triple fault, it will tell us exactly what address caused the fault and why it happened.
Understanding the Page Fault Exception
A Page Fault is exception number 14. When it occurs, the CPU does two crucial things before invoking our handler:
- It stores the virtual address that caused the fault in a special control register called
CR2. - It pushes an error code onto the stack that provides details about the nature of the fault (e.g., was it a read or write? was the page present but protected?).
Our handler’s job is to read this information and present it in a human-readable format, then halt the system, as a page fault in our simple kernel is a non-recoverable, fatal error.
Step 1: Creating the Page Fault Handler
Let’s head to our src/interrupts.rs file. We will create a new handler function specifically for page faults. The x86_64 crate provides a dedicated PageFaultErrorCode type that makes parsing the error code safe and easy.
// In src/interrupts.rs
// Add these to your `use` statements at the top
use x86_64::structures::idt::PageFaultErrorCode;
use x86_64::registers::control::Cr2;
use crate::hlt_loop; // Make sure hlt_loop is imported or defined in the crate root
// ... (existing code: IDT, other handlers, etc.) ...
/// The handler for a Page Fault (#PF), exception 14.
/// This exception is triggered when the CPU tries to access a virtual address
/// for which the translation fails or the access is not permitted.
extern "x86-interrupt" fn page_fault_handler(
stack_frame: InterruptStackFrame,
error_code: PageFaultErrorCode,
) {
println!("EXCEPTION: PAGE FAULT");
// The CR2 register contains the virtual address that caused the page fault.
println!("Accessed Address: {:?}", Cr2::read());
// The error code provides details about the type of memory access that caused the fault.
println!("Error Code: {:?}", error_code);
println!("{:#?}", stack_frame);
// A page fault is a fatal, non-recoverable error in our kernel.
// We print the debug info and then enter a halt loop.
println!("Halting system.");
hlt_loop();
}
Let’s analyze this crucial handler: * Signature: Notice the function signature is different from our other handlers. It accepts a second argument, error_code: PageFaultErrorCode. The x86-interrupt calling convention is smart enough to pass the exception’s error code to our function as this strongly-typed struct. * Cr2::read(): We use this function from the x86_64 crate to safely read the CR2 register. This gives us the exact virtual address that the CPU failed to access. * Printing error_code: The PageFaultErrorCode struct has a helpful Debug implementation that prints the flags clearly (e.g., CAUSED_BY_WRITE, PROTECTION_VIOLATION). This tells us why the fault occurred. * hlt_loop(): We cannot simply return from a page fault handler in our kernel, as the faulting instruction would just execute again, causing another fault, leading to an infinite loop. The only safe thing to do is print the debug information and halt the system.
(Note: If you don’t have a hlt_loop function yet, you can add it to your src/lib.rs or src/main.rs:)
// In src/lib.rs or src/main.rs
pub fn hlt_loop() -> ! {
loop {
x86_64::instructions::hlt();
}
}
Step 2: Registering the Handler in the IDT
Now that we’ve built our new “emergency service,” we need to register its address with the dispatch system (our IDT). The InterruptDescriptorTable struct has a dedicated field for the page fault handler.
Open src/interrupts.rs and add the highlighted line to your IDT’s lazy_static! block.
// In src/interrupts.rs
lazy_static! {
static ref IDT: InterruptDescriptorTable = {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
idt.double_fault.set_handler_fn(double_fault_handler);
// Add the new page fault handler
idt.page_fault.set_handler_fn(page_fault_handler); // <--- Add this line
idt[InterruptIndex::Timer.as_usize()]
.set_handler_fn(timer_interrupt_handler);
idt[InterruptIndex::Keyboard.as_usize()]
.set_handler_fn(keyboard_interrupt_handler);
idt
};
}
With this line, you have officially armed your kernel’s memory protection system.
Step 3: Testing the Handler
The best way to know if an error handler works is to trigger the error deliberately. Let’s add some code to kernel_main in src/main.rs that accesses an unmapped address. A classic way to do this is to dereference a pointer to a nonsensical address.
// In src/main.rs
// ...
fn kernel_main(boot_info: &'static BootInfo) -> ! {
// ... (previous initialization code)
// After enabling interrupts, let's trigger a page fault to test our handler.
// The address 0xdeadbeafc0de is just a random, unmapped address.
println!("Attempting to trigger a page fault...");
unsafe {
*(0xdeadbeafc0de as *mut u64) = 42;
}
println!("It did not crash!"); // This line should NOT be reached
hlt_loop();
}
This unsafe block attempts to write the value 42 to the virtual address 0xdeadbeafc0de.... Since we have never created a mapping for this page, the MMU will fail to translate it, and the CPU will immediately trigger a page fault.
The Moment of Truth: A Controlled Crash
Run your kernel.
cargo run
Instead of a confusing QEMU crash or a triple fault, you should see the beautifully detailed output from your new page fault handler:
EXCEPTION: PAGE FAULT
Accessed Address: VirtAddr(0xdeadbeafc0de)
Error Code: PageFaultErrorCode(CAUSED_BY_WRITE | NOT_PRESENT)
InterruptStackFrame {
instruction_pointer: VirtAddr(0x...),
code_segment: 8,
cpu_flags: 0x...,
stack_pointer: VirtAddr(0x...),
stack_segment: 0,
}
Halting system.
This is a perfect result! The output tells you everything you need to know: * A page fault occurred. * The problematic address was exactly the one we tried to write to. * The error code clearly states the fault was caused by a write operation to a page that was not present. * The system halted gracefully after printing the debug information.
You have successfully implemented one of the most important diagnostic tools for an operating system.
What’s Next? The Next Grand Challenge: A Kernel Heap
You have now built a complete, robust virtual memory system. Your kernel controls its address space, can create mappings, and can gracefully handle memory access errors. You have laid the foundation for almost all modern OS features.
The next logical step is to use this foundation to enable dynamic memory allocation. Right now, all of our data structures are statically allocated at compile time. To build more complex systems like schedulers, filesystems, or drivers, we need a kernel heap—a region of virtual memory from which we can request and free blocks of memory at runtime, using familiar Rust types like Box and Vec. In the next major step of this project, you will build this heap, unlocking a new level of dynamic capability for your kernel.
Further Reading
- OSDev.org Wiki: Page Fault: The definitive technical reference on page faults, including a detailed breakdown of the error code bits.
x86_64Crate Documentation: Explore the official documentation for the types you just used.- The Rust Book: Unsafe Rust: A great refresher on the rules and responsibilities that come with using
unsafeblocks.