Sitemap

Building a 64-Bit OS — Chapter 5: Virtual Memory Management

15 min readOct 6, 2026

--

Last time, I left the story right after implementing a Physical Memory Manager. At the very end of that post, I dropped a plot twist: the memory touched by the CPU is never physical memory directly. It is always an indirect access through the MMU (Memory Management Unit) using virtual memory.

Press enter or click to view image in full size

If you haven’t read that part yet, here’s the link to it. I highly suggest reading it first. Now, let’s pick up the story exactly where we left off.

What is Virtual Memory?

If your real name is Rakesh and your friends call you “Rocky”, Rocky is the virtual memory address, and Rakesh is the physical memory address. While there might not be a logical reason for someone’s nickname, there is absolutely a concrete reason why we use this system in computers.

Why Do We Need It?

Going by the nickname theory, a virtual memory address is just an address that the MMU maps to a corresponding physical address. Essentially, it is a massive lookup table. But why do we need to call a memory address by a nickname?

Let me first show you what a single entry in this memory map looks like:

typedef struct {
uint64_t present : 1;
uint64_t read_write : 1;
uint64_t user_super : 1;
uint64_t write_through : 1;
uint64_t cache_disabled : 1;
uint64_t accessed : 1;
uint64_t dirty : 1;
uint64_t huge_page : 1;
uint64_t global : 1;
uint64_t available : 3;
uint64_t address : 40;
uint64_t reserved : 11;
uint64_t nx : 1;
} __attribute__((packed)) paging_map_entry_t;

Each entry is exactly 64 bits (8 bytes). The fields from Present to Global represent architectural flags. These flags dictate the physical characteristics and access rights of that specific memory page.

For example, if the read_write flag is set to false, that page becomes strictly read-only. Any process can read the data inside it, but nothing can overwrite it. This demonstrates the first major benefit: Virtual memory provides hardware-level protection.

Every single flag plays a role in enforcing rules upon memory access. You can read about them in detail on the OSDev Wiki Page here. I personally kept this page open while implementing my own paging code.

The Analogy

Suppose you go to a massive, multi-story library looking for Harry Potter and the Sorcerer’s Stone. How do you approach this? The library has hundreds of floors, hundreds of bookshelves on every floor, and hundreds of books on every shelf.

Do you just start from the first book on the first shelf on the ground floor and keep looking? By the time you find it, Harry would’ve grown up and founded the Order of the Phoenix!

There has to be a better way. Naturally, the librarian ordered the books systematically. When you ask the warden, they tell you all European authors are on the second floor. You go there and ask the floor in-charge, who tells you fiction books are on shelf 13. You walk to shelf 13 and find the books ordered alphabetically. Finally, you go to the ‘H’ section and find your book.

This structured search is exponentially faster than a linear scan. Abracadabra, people — that is the exact essence of virtual memory management. Keep this analogy in mind, because everything ahead connects directly to it.

4-Level Paging

Let’s break down how many steps it took to get the book:

  1. We had to know the address to reach the Library.
  2. We went to the correct Floor.
  3. We walked to the correct Shelf.
  4. We narrowed it down to the exact Section.

That is a 4-step progression: Library -> Floor -> Shelf -> Section. This is exactly what a CPU does in 4-Level Paging.

Translating any virtual address to its physical counterpart happens through these exact progressive stages. Just like you didn’t magically get your book by standing at the library gate, the CPU must traverse through levels to find the physical address.

Let’s add some strict numbers to our analogy. Suppose the library has exactly 512 floors. Each floor has 512 shelves, each shelf has 512 sections, and each section holds 512 books. You will soon see why I chose the number 512. Let’s map this directly to computer architecture.

The Root Table (PML4)

This is your whole library. How did you reach the library in the first place? You knew its address. In an OS, if you want to perform address translation, the CPU first needs to know where the Root Table is located. This root map of all virtual addresses is stored inside a special CPU register called CR3.

Here is the structure of a page table:

typedef struct {
paging_map_entry_t entries[512];
} __attribute__((aligned(4096))) paging_page_table_t;

It is an array of exactly 512 elements of type paging_map_entry_t. Each entry is exactly 64 bits. Inside this entry, the physical address field takes up 40 bits.

The Page Directory Pointer Table (PDPT)

We went from the Library to the correct Floor. In address translation, we travel from the Root Table to the PDPT.

Here is an interesting detail. You could reach the library without knowing exactly which book you wanted, simply because you had the building’s address. But you couldn’t reach the second floor without knowing which book you were looking for. The book’s specific category dictated the floor. Similarly, the virtual address itself dictates which PDPT to go to.

In a 4-Level paging setup on x86–64, virtual addresses are 64 bits long, but we currently only use the lower 48 bits. The CPU takes bits 39 through 47 of the virtual address to give you an index number (from 0 to 511). The Root Table entry at that specific index contains the physical address of the PDPT you need to visit next.

(Note: By bit-shifting this 40-bit address field left by 12, (uint64_t)(address << 12), you get the exact physical address of the next table).

Now you know why we assumed 512 floors! A 9-bit chunk of the virtual address (2^9) provides exactly 512 possible index entries. We have successfully traveled to the correct floor. Now we need to find the shelf.

The Page Directory (PD)

The analogy holds strong here. From the PDPT, we must find the Page Directory (PD).

The CPU looks at bits 30 through 38 of the virtual address. This gives the index (0–511) inside the PDPT. The entry at that index gives us the physical address of the Page Directory. We are now standing in front of the correct shelf.

The Page Table (PT)

We take the next 9 bits — bits 21 through 29 — to find the correct index inside the Page Directory. This points us to the final Page Table (PT).

Get Dev Sharma’s stories in your inbox

Join Medium for free to get updates from this writer.

We have finally reached the right section on the shelf. What is left? Getting the actual book. Each Page Table has 512 entries, and each entry points directly to a single 4KB physical page of memory (the book).

Seeing It In Action

Let’s break the theoretical barrier and translate a real virtual address: 0x0000_0020_4060_8A2C.

  1. Bits 39–47 evaluate to 0. The Root Table's 0th entry points to our PDPT.
  2. Bits 30–38 evaluate to 129. The PDPT's 129th entry points to our PD.
  3. Bits 21–29 evaluate to 3. The PD's 3rd entry points to our PT.
  4. Bits 12–20 evaluate to 8. The PT's 8th entry points to the exact 4KB Physical Page.
  5. Bits 0–11 evaluate to 0xA2C. This final 12-bit value is the offset. It tells the CPU exactly where inside that 4KB page the actual data resides.

Why 4-Level and Not 3-Level?

Let’s calculate the total address space this hierarchy can hold by returning to our analogy:

  • Each book shelf section (Page Table) has 512 books (4KB pages). 512 * 4KB = 2MB. A single Page Table holds 2MB of virtual memory.
  • Each shelf (Page Directory) has 512 sections. 512 * 2MB = 1GB. A single Page Directory holds 1GB.
  • Each floor (PDPT) has 512 shelves. 512 * 1GB = 512GB. A single PDPT maps 512GB.
  • The library (Root Table) has 512 floors. 512 * 512GB = 256TB.

With 4-Level paging, the CPU can address 256 Terabytes of virtual memory. Logically, it doesn’t make sense to add a 5th level for standard desktop processors right now. Conversely, if we dropped down to 3-level paging, the maximum limit would be just 512GB. That might feel a bit small to power users who can already afford massive amounts of RAM for personal workstations!

How to Implement It

Setting up virtual memory in code is essentially just traversing from the top-level table down to the bottom level, allocating new tables if they don’t exist yet, and then placing your physical address entry at the very bottom.

Once the tables are populated, you simply load the Root Table’s physical address into the CR3 register. Here is how I did it:

typedef enum PAGE_SIZE{
KB_4,
MB_2,
GB_1
} PAGE_SIZE;

typedef enum {
PT_FLAG_PRESENT = 1 << 0,
PT_FLAG_READ_WRITE = 1 << 1,
PT_FLAG_USER_SUPER = 1 << 2,
PT_FLAG_WRITE_THROUGH = 1 << 3,
PT_FLAG_CACHE_DISABLED = 1 << 4,
PT_FLAG_ACCESSED = 1 << 5,
PT_FLAG_DIRTY = 1 << 6,
PT_FLAG_HUGE_PAGE = 1 << 7,
PT_FLAG_GLOBAL = 1 << 8,
PT_FLAG_NX = 1UL << 63,
} paging_flags_t;

#define L4_IDX(v) ((v >> 39) & 0x1FF) //9 bits, from 39-47
#define L3_IDX(v) ((v >> 30) & 0x1FF) //9 bits, from 30-38
#define L2_IDX(v) ((v >> 21) & 0x1FF) //9 bits, from 21-29
#define L1_IDX(v) ((v >> 12) & 0x1FF) //9 bits, from 12-20
//12 bits, from 0-11 is offset within the physical 4kb page

bool cr3_loaded = false;

void paging_set_flags(paging_page_table_t* page_table, uint64_t index, uint64_t flags, uint64_t phy_addr){

page_table->entries[index].address = (uint64_t) phy_addr >> 12;
page_table->entries[index].present = (flags & PT_FLAG_PRESENT) ? 1 : 0;
page_table->entries[index].read_write = (flags & PT_FLAG_READ_WRITE) ? 1 : 0;
page_table->entries[index].user_super = (flags & PT_FLAG_USER_SUPER) ? 1 : 0;
page_table->entries[index].write_through = (flags & PT_FLAG_WRITE_THROUGH) ? 1 : 0;
page_table->entries[index].cache_disabled = (flags & PT_FLAG_CACHE_DISABLED) ? 1 : 0;
page_table->entries[index].accessed = (flags & PT_FLAG_ACCESSED) ? 1 : 0;
page_table->entries[index].dirty = (flags & PT_FLAG_DIRTY) ? 1 : 0;
page_table->entries[index].huge_page = (flags & PT_FLAG_HUGE_PAGE) ? 1 : 0;
page_table->entries[index].global = (flags & PT_FLAG_GLOBAL) ? 1 : 0;
page_table->entries[index].nx = (flags & PT_FLAG_NX) ? 1 : 0;

}

void paging_set_new_page_table(paging_page_table_t* page_table, uint64_t index, uint64_t flags){

void* new_page_table_addr = pmm_request_page(); //physical address to store new table
void* new_page_table = NULL; //corresponding virtual address

if(!cr3_loaded){
memset( (void*) P2V(new_page_table_addr), 0, 4096);
new_page_table = (void*) P2V((uint64_t)new_page_table_addr);
}else{
memset( (void*) P2V_DIRECT(new_page_table_addr), 0, 4096);
new_page_table = (void*) P2V_DIRECT((uint64_t)new_page_table_addr);
}

if(new_page_table == NULL){ //overflow check
return;
}

//setting up bit values
paging_set_flags(page_table, index, flags, (uint64_t) new_page_table_addr);

}

paging_page_table_t* get_table_ptr(paging_map_entry_t* entry){

//address field is 40 bits, after left-shift entry_address gets 52 bits
//with left most 12 bits as 0 which is important since pages are 4kb aligned
uint64_t entry_address = entry->address << 12;

if(!cr3_loaded){
return (paging_page_table_t*) P2V(entry_address);
}else{
return (paging_page_table_t*) P2V_DIRECT(entry_address);
}

}

void paging_map_page(paging_page_table_t* pml4, void* virtual_addr_ptr, uint64_t physical_addr, uint64_t flags, PAGE_SIZE page_size){

uint64_t virtual_address_value = (uint64_t) virtual_addr_ptr;
uint64_t level_4_index = L4_IDX(virtual_address_value);
uint64_t level_3_index = L3_IDX(virtual_address_value);
uint64_t level_2_index = L2_IDX(virtual_address_value);
uint64_t level_1_index = L1_IDX(virtual_address_value);

//----------------------------PML4 (ROOT LEVEL)--------------------------------------------------

if(!pml4->entries[level_4_index].present){ //setting new page table if not present
paging_set_new_page_table(pml4, level_4_index, PT_FLAG_PRESENT | PT_FLAG_READ_WRITE | PT_FLAG_USER_SUPER);
}
paging_map_entry_t pml4_entry = pml4->entries[level_4_index]; //getting pdp entry

//------------------------PDPT (PAGE DIRECTOR POINTER TABLE)------------------------------------

paging_page_table_t* pdp = get_table_ptr(&pml4_entry); //going to address provided by the above entry
if(!pdp->entries[level_3_index].present){
paging_set_new_page_table(pdp, level_3_index, PT_FLAG_PRESENT | PT_FLAG_READ_WRITE | PT_FLAG_USER_SUPER);
}

//-------------------------PAGE DIRECTORY--------------------------------------------------------------

paging_page_table_t* pd = get_table_ptr(&(pdp->entries[level_3_index]));

if(page_size == MB_2){
paging_set_flags(pd, level_2_index, flags, physical_addr);
return;
}

//------------------------------PAGE TABLE------------------------------------------------------

if(!pd->entries[level_2_index].present){
paging_set_new_page_table(pd, level_2_index, PT_FLAG_PRESENT | PT_FLAG_READ_WRITE | PT_FLAG_USER_SUPER);
}
paging_page_table_t* pt = get_table_ptr(&(pd->entries[level_2_index]));
paging_set_flags(pt, level_1_index, flags, physical_addr);

}

You might notice a page_size parameter in my code. Isn't the page size strictly fixed to 4KB? Yes, but we are talking about mappings here.

Suppose you want to map a massive 2MB chunk of contiguous memory with identical flags. It is inefficient to map 512 individual 4KB pages. Instead, you can place a single entry inside the Page Directory (PD) and flip the Huge Page bit to true. The CPU now knows that this single PD entry maps an entire 2MB block natively. You can do the exact same thing at the PDPT level to map a full 1GB region in one go.

(Note: P2V() and V2P() are macros I use to translate physical addresses to their virtual higher-half counterparts and vice-versa.)

#define KERNEL_VIRT_BASE 0xFFFFFFFF80000000
#define KERNEL_PHSY_BASE 0x8000000
#define P2V(a) ((uint64_t)(a) + KERNEL_VIRT_BASE -KERNEL_PHSY_BASE)
#define V2P(a) ((uint64_t)(a) + KERNEL_PHSY_BASE - KERNEL_VIRT_BASE)

#define DIRECT_MAP_BASE 0xFFFF800000000000
#define P2V_DIRECT(a) ((uint64_t)(a) + DIRECT_MAP_BASE)
#define V2P_DIRECT(a) ((uint64_t)(a) - DIRECT_MAP_BASE)

The Twist

Earlier, I said that anytime you touch memory, you are using virtual addresses. But wait — if we are just now writing the code to set up virtual memory inside our kernel, how did we manage to load the GDT, IDT, and PMM beforehand?

Here is the twist: We were using virtual memory all along.

During the bootloader phase, memory is managed by UEFI, which already configured its own virtual memory tables. The CR3 register was already loaded with a page table created by the firmware (usually an identity map).

So, why are we doing it again? Once you call ExitBootServices() and jump to the kernel, the UEFI firmware is officially retired. While its page tables might technically still function, there is no guarantee how long they will remain safe. You jumped to the kernel; the kernel is the owner now. Every hardware responsibility falls on you.

The Two-Part Setup

I actually set up paging twice in my OS. When I first jumped from the bootloader to the kernel, the very first thing my kernel did (even before the GDT) was configure a temporary page table. Why? Because you cannot execute a single instruction in memory without a valid page table. If the UEFI tables were suddenly dropped or overwritten, accessing memory would cause a Page Fault, which escalates into a Triple Fault.

Here is my temporary setup:

%define KERNEL_VIRTUAL_BASE 0xFFFFFFFF80000000
%define KERNEL_PHYSICAL_BASE 0x8000000

section .data
align 4096

; =====================================================================
; LEVEL 4: PAGE MAP LEVEL 4 (PML4)
; =====================================================================
BootPML4:
; Entry 0: Temporary Identity Map (0x0 -> Lower 512GB)
dq (BootPDP_Low - KERNEL_VIRTUAL_BASE + KERNEL_PHYSICAL_BASE) + 0x3

times 510 dq 0 ; Padding for entries 1 through 510

; Entry 511: Higher-Half Kernel Map (0xFFFFFFFF80000000 -> Upper 512GB)
dq (BootPDP_High - KERNEL_VIRTUAL_BASE + KERNEL_PHYSICAL_BASE) + 0x3

; =====================================================================
; LEVEL 3: PAGE DIRECTORY POINTER TABLES (PDPT)
; =====================================================================
BootPDP_Low:
; Maps the bottom of virtual memory space (0x0 -> 512GB)
dq (BootPD_Low - KERNEL_VIRTUAL_BASE + KERNEL_PHYSICAL_BASE) + 0x3
times 511 dq 0

BootPDP_High:
; Maps the extreme top of virtual memory space (-2GB kernel region)
times 510 dq 0
dq (BootPD_High - KERNEL_VIRTUAL_BASE + KERNEL_PHYSICAL_BASE) + 0x3
dq 0

; =====================================================================
; LEVEL 2: PAGE DIRECTORIES (PD) - Generates 2MB Huge Pages (1GB Total each)
; =====================================================================
BootPD_Low:
; Maps Virtual 0x0 -> Physical 0x0 (Identity mapping)
%assign i 0
%rep 512
dq (i * 0x200000) + 0x83 ; 0x83 = Present + Writable + Huge Page (2MB)
%assign i i+1
%endrep

BootPD_High:
; Maps Virtual 0xFFFFFFFF80000000 -> Physical KERNEL_PHYSICAL_BASE (0x8000000)
%assign i 0
%rep 512
dq (i * 0x200000) + KERNEL_PHYSICAL_BASE + 0x83
%assign i i+1
%endrep

This setup establishes two crucial mappings:

  1. Identity Map (First 1GB): Virtual addresses 0x0 to 0x3FFF_FFFF map directly to identical physical addresses. UEFI uses an identity map, so preserving this prevents my kernel from crashing while it sets up its own permanent tables. I mapped 1GB because I gave my QEMU instance 256MB of RAM, making 1GB more than enough to cover it.
  2. Higher Half Map (Kernel Space): Virtual addresses 0xFFFF_FFFF_8000_0000 through 0xFFFF_FFFF_BFFF_FFFF map to physical addresses starting at 0x0800_0000. As I mentioned in my previous blogs, I am building a Higher Half Kernel. My kernel uses extremely high virtual addresses to keep user-space memory completely separate and clean. Since my linker script hardcoded 0xFFFF_FFFF_8000_0000 as the kernel's starting address, the virtual mapping must match.

Before we look at the setup, let’s talk about those weird 0xFFFF prefixes in my kernel addresses. In 64-bit x86, pointers are 64 bits wide, but as we saw earlier, 4-level paging only uses the lower 48 bits for actual translation. So what happens to the top 16 bits? The CPU enforces a strict hardware rule: bits 48 through 63 must be identical copies of bit 47. This is called sign extension, and addresses that follow this rule are known as Canonical Addresses. If bit 47 is 0, the top 16 bits must be all zeros (0x0000...), creating the lower half of memory (traditionally used for user-space apps). If bit 47 is 1, the top 16 bits must be all ones, which translates to Fs in hexadecimal (0xFFFF...). This creates the higher half of memory. Since I am building a Higher Half Kernel, my kernel addresses must live in this upper range. That is exactly why my kernel's starting address is loaded with that massive 0xFFFF_FFFF block.

What Do You Need to Map?

Once your final, permanent page tables are loaded into CR3, attempting to access any unmapped region will throw a Page Fault. You must be precise. During my final setup, I meticulously mapped the following items:

1. The Page Table itself:

uint64_t pml4_phsy = (uint64_t)(pmm_request_page());
kernel_pml4 = (paging_page_table_t*) P2V(pml4_phsy);

paging_map_page(
kernel_pml4, //page table
(void*) P2V(pml4_phsy), //virtual address
pml4_phsy, //phsyical address
PT_FLAG_PRESENT | PT_FLAG_READ_WRITE, //flags
KB_4
);

2. The PMM Bitmap:

uint64_t pages_required_for_bitmap = (get_bitmap_size() / 4096) + 1;
uint64_t bitmap_start_address = V2P(get_bitmap_address());
for(uint64_t i = 0; i < pages_required_for_bitmap; i++){
uint64_t addr = (uint64_t)bitmap_start_address + (i * 4096);
paging_map_page(
kernel_pml4, //page table
(void*)P2V(addr), //virtual address
addr, //phsyical address
PT_FLAG_PRESENT | PT_FLAG_READ_WRITE, //flags
KB_4
);
}

3. The full available physical memory (using 2MB Huge Pages):

This is a “do it once and forget it” step. Now, whenever I need to access physical RAM, I can instantly reach it via its higher-half virtual address without constantly updating the page tables.

uint64_t memory_end = memory_get_m_size(boot_info);
for(uint64_t addr = 0; addr < memory_end ; addr += 0x200000){
paging_map_page(
kernel_pml4, //page table
(void*)P2V_DIRECT(addr), //virtual address
addr, //phsyical address
PT_FLAG_PRESENT | PT_FLAG_READ_WRITE | PT_FLAG_HUGE_PAGE, //flags
MB_2
);
}

Notice that this complete mapping will start at offset 0xFFFF800000000000 which is different from the kernel virtual address offset 0xFFFFFFFF80000000. Also, For these mappings, The User flag has not been set. Thus, it can only be accessed by the Kernel (Ring 3).

4. The Framebuffer, Font Buffer, and the Kernel itself:

The Framebuffer, Font Buffer, and the Kernel itself:

uint64_t frame_buffer_base = (uint64_t) boot_info->frame_buffer.frame_buffer_base;
uint64_t frame_buffer_size = boot_info->frame_buffer.frame_buffer_size;

if(frame_buffer_base % 4096 != 0){
frame_buffer_base = frame_buffer_base & (~0x0FFF);
}

for(uint64_t addr = 0; addr < frame_buffer_size; addr += 4096){
paging_map_page(
kernel_pml4, //page table
(void*)P2V(frame_buffer_base + addr), //virtual address
frame_buffer_base + addr, //phsyical address
PT_FLAG_PRESENT | PT_FLAG_READ_WRITE | PT_FLAG_CACHE_DISABLED, //flags
KB_4
);
}

uint64_t font_ptr = (uint64_t) boot_info->font;

if(font_ptr % 4096 != 0){
font_ptr = font_ptr & (~0x0FFF);
}

paging_map_page(
kernel_pml4, //page table
(void*)font_ptr, //virtual address
V2P(font_ptr), //phsyical address
PT_FLAG_PRESENT, //flags
KB_4
);


paging_map_page(
kernel_pml4, //page table
(void*)boot_info->font->header, //virtual address
V2P((uint64_t)boot_info->font->header), //phsyical address
PT_FLAG_PRESENT, //flags
KB_4
);

uint64_t font_base = (uint64_t) boot_info->font->glyph_buffer;
uint64_t font_size = 0;
if(boot_info->font->header->mode == 1){
font_size = (uint64_t)boot_info->font->header->char_size * (uint64_t)512;
}else{
font_size = (uint64_t)boot_info->font->header->char_size * (uint64_t)256;
}

if(font_base % 4096 != 0){
font_base = font_base & (~0x0FFF);
}

font_size = (font_size + 4096) & (~0x0FFF);

for(uint64_t addr = 0; addr < font_size; addr += 4096){
paging_map_page(
kernel_pml4, //page table
(void*)(font_base + addr), //virtual address
V2P(font_base + addr), //phsyical address
PT_FLAG_PRESENT, //flags
KB_4
);
}

uint64_t kernel_start = V2P((uint64_t)&_KernelStart);
uint64_t kernel_end = V2P((uint64_t)&_KernelEnd);
uint64_t kernel_size = kernel_end - kernel_start;
uint64_t pages_required_for_kernel = (kernel_size / 4096) + 1;
for(uint64_t i = 0; i < pages_required_for_kernel; i++){
uint64_t addr = (uint64_t)kernel_start + (i * 4096);
paging_map_page(
kernel_pml4, //page table
(void*)P2V(addr), //virtual address
addr, //phsyical address
PT_FLAG_PRESENT | PT_FLAG_READ_WRITE, //flags
KB_4
);
}

Once all these critical assets are mapped, you are finally cleared to load your new, permanent page table physical address into the CR3 register.

void load_cr3(void* cr3_value) {
asm volatile("mov %0, %%cr3" :: "r" (cr3_value) : "memory");
}

Finally, remember in my PMM blog when I locked all physical memory below KERNEL_PHYS_BASE? Now you know why. During the first temporary setup, the higher-half map only started at KERNEL_PHYS_BASE. Now that our permanent tables are fully operational and mapping the whole system safely into the higher half, we can finally unlock that lower memory.

Shortcomings and Future Improvements

Virtual memory gave me my fair share of headaches. It kept throwing issues at me long after I had moved on to Ring 3 and ELF loading. But once you truly understand it, you see the enormous power it provides. Still, if I had to do it all over again, I would definitely refactor a few things. You only learn how to build the elegant version after you’ve built the ugly one!

Here is what I would improve:

  • Eliminate the Two-Part Setup: Doing a temporary setup just to build a runway for the final setup is redundant. I could shift the creation of the final page tables into the bootloader phase, pass them to the kernel jump, and load CR3 immediately. The kernel would wake up with a correct (if incomplete) setup and could simply append mappings like the bitmap as needed.
  • Ditch the Hardcoded Macros: Using hardcoded macros like P2V() and V2P() is messy. The solution, just like in my PMM implementation, is to use Position Independent Executables (PIE).
  • Refactor Parameter Bloat: The number of parameters passed into my paging functions is overwhelming. While all the data is necessary, it desperately needs to be bundled into configuration structs to improve code readability.

That wraps up this chapter on virtual memory! Let me know your thoughts. You can follow me on LinkedIn and X, and check out the full source code for my OS on GitHub.

--

--