ChrisVM physical memory map¶
About this chapter ChrisVM platform
In this chapter
- ChrisVM physical memory map
- Scope
- Default layout
- RAM region
- ELF loading is effectively identity-oriented
- Boot-reserved regions inside RAM
- Initial page-table topology
- Framebuffer physical range
- Framebuffer virtual mapping
- Why RAM cannot exceed 32 MiB today
- Generic MMIO regions
- Physical dispatch precedence
- Mixed-region requests are not split
- MMIO transaction width is lost
- Unmapped physical memory
- Test MMIO at 96 MiB
- Loader collision checks are incomplete by design
- Physical versus virtual addresses
- GDT placement and virtual access
- Stack placement and growth
- Reserved page-table memory and guest access
- No central region descriptor
- Relationship to boot protocol v1
- Current validation evidence
- Hardening priorities
- Revision note
Scope¶
The ChrisVM physical map is currently defined by a small set of fixed constants plus runtime RAM size and MMIO registrations. There is no central region table describing every physical range. Instead, ownership is distributed across boot constants, RAM allocation, framebuffer state and the MMIO registry.
That makes the map simple enough to inspect directly, but it also creates hidden coupling. The boot loader, page-table builder, framebuffer and physical dispatcher must all agree on addresses without a single authoritative map object validating the complete layout.
This chapter documents the physical-address contract at ChrisOS revision e05a17fd76333114a3fb5c2452f38ca747d4ac56.
Default layout¶
With the default 16 MiB RAM configuration, the important fixed physical ranges are:
| Range / address | Role |
|---|---|
| 0x00000000 .. RAM end | guest RAM |
| 0x00070000 .. 0x00070fff | boot-protocol GDT reservation |
| 0x0007f000 .. 0x0007ffff | loader-reserved initial stack page |
| 0x00080000 | initial RSP value, stack grows downward |
| RAM_size - 0x4000 | PML4 page |
| RAM_size - 0x3000 | PDPT page |
| RAM_size - 0x2000 | PD page |
| RAM_size - 0x1000 .. RAM_size | reserved by loader but unused by install_tables |
| 0x02000000 .. 0x0212bfff | linear framebuffer backing bytes |
| arbitrary registered ranges | generic MMIO outside RAM/framebuffer |
Port I/O such as serial 0x3f8..0x3ff and shutdown port 0x501 belongs to a separate address space and is not part of this physical memory map.
RAM region¶
ChrisMachine allocates one contiguous zeroed host buffer for guest RAM.
The requested RAM size must be:
- at least 2 MiB;
- aligned to 2 MiB.
The public default is 16 MiB.
The framebuffer placement currently imposes an additional effective ceiling of 32 MiB because chris_fb_attach rejects ram_size values above CHRIS_FB_PHYS.
Thus, for the current machine implementation:
2 MiB <= RAM <= 32 MiB
RAM size is a multiple of 2 MiB
The first physical byte after RAM is not automatically treated as a guard page or reserved area. It can become framebuffer or MMIO depending on the chosen address.
ELF loading is effectively identity-oriented¶
chris_load_elf accepts 64-bit little-endian x86-64 ELF images.
For each PT_LOAD segment it uses p_vaddr as the destination offset into guest RAM. p_paddr is not used as an independent physical address.
The segment must fit entirely inside RAM.
The loader also rejects higher-half virtual addresses at or above:
0xffff800000000000
under boot protocol v1.
This produces an identity-oriented early machine model: guest ELF virtual load addresses are copied directly into the same-numbered physical RAM offsets that the initial page tables then identity-map.
It is not yet a general loader for arbitrary virtual-to-physical segment placement.
Boot-reserved regions inside RAM¶
The loader rejects PT_LOAD segments that overlap three categories of boot-owned memory.
GDT page¶
The fixed GDT physical address is:
CHRIS_GDT_PHYS = 0x70000
The loader reserves one full 4 KiB page starting at that address.
install_tables currently writes three 8-byte descriptors there:
- null descriptor;
- long-mode code descriptor;
- data descriptor.
Most of the 4 KiB reservation is unused today but protected from ELF loading.
Initial stack page¶
The fixed initial stack pointer is:
CHRIS_STACK_RSP = 0x80000
The loader rejects overlap with the 4 KiB range immediately below it:
0x7f000 .. 0x7ffff
The stack therefore begins at 0x80000 and grows downward into that reserved page.
The current boot path does not allocate a dynamic stack region. It relies on this convention inside already-zeroed guest RAM.
Page-table reserve¶
The loader reserves the final:
CHRIS_PT_RESERVE = 0x4000
bytes of RAM.
That is four 4 KiB pages.
install_tables currently places only three paging structures:
PML4 = RAM_size - 0x4000
PDPT = RAM_size - 0x3000
PD = RAM_size - 0x2000
The final page:
RAM_size - 0x1000 .. RAM_size
is kept out of guest ELF segments but is not used by install_tables in the inspected revision.
This reserved-but-unused page provides some expansion space, but its purpose is not encoded as a distinct structure.
Initial page-table topology¶
The boot path constructs a minimal hierarchy:
PML4[0] -> PDPT
PDPT[0] -> PD
The PD contains 2 MiB identity mappings for RAM.
For each RAM chunk:
PDE[i] = physical_base | 0x83
where 0x83 represents Present, Writable and Page Size.
The current layout uses one PD only, so the theoretical initial-map limit is 512 * 2 MiB = 1 GiB.
The machine's fixed framebuffer rule limits RAM much earlier, at 32 MiB.
Framebuffer physical range¶
The framebuffer begins at:
CHRIS_FB_PHYS = 0x02000000
or 32 MiB.
Its dimensions are:
width = 640
height = 480
pitch = 640 * 4
The backing size is:
640 * 480 * 4 = 1,228,800 bytes = 0x12c000
Therefore its byte range is:
0x02000000 .. 0x0212bfff
with the first address after the framebuffer at:
0x0212c000
The backing storage is not part of the RAM allocation. It is a separate host buffer owned by ChrisFb.
Framebuffer virtual mapping¶
install_tables identity-maps the framebuffer into the same virtual address range.
It aligns the beginning downward to a 2 MiB boundary and installs 2 MiB PDEs until the entire framebuffer size is covered.
Since 0x02000000 is already 2 MiB aligned and the framebuffer occupies less than 2 MiB, one large page covers:
0x02000000 .. 0x021fffff
Only the first 0x12c000 bytes correspond to framebuffer backing. Physical accesses to the remaining addresses of that 2 MiB virtual mapping do not automatically belong to framebuffer memory.
If no generic MMIO region exists there, such accesses fail at the physical dispatcher.
This distinction between page-table coverage and device backing is important.
Why RAM cannot exceed 32 MiB today¶
For ram_size exactly 32 MiB, RAM occupies:
0x00000000 .. 0x01ffffff
and the framebuffer begins immediately afterward at 0x02000000.
That layout is valid.
If RAM were larger than 32 MiB, it would overlap the fixed framebuffer base.
Rather than relocating the framebuffer, chris_fb_attach rejects machine creation.
The limit is therefore a memory-map collision policy encoded indirectly in the framebuffer device.
A future machine map should detect and resolve regions centrally instead of allowing one device constructor to define the maximum guest RAM size.
Generic MMIO regions¶
Generic MMIO can be registered at runtime using chris_mmio_map.
Each region supplies:
- base;
- size;
- read callback;
- write callback;
- context pointer.
Up to eight regions can exist.
The registration function does not test for overlap with:
- RAM;
- framebuffer;
- another MMIO slot.
This means the stored registry may contain a region that can never be reached because a higher-precedence region owns the same physical address.
Physical dispatch precedence¶
chris_phys_read and chris_phys_write apply this precedence:
1. RAM
2. framebuffer
3. generic MMIO
The first two checks require the complete access to fit inside the corresponding region.
Generic MMIO is used only after both fast paths fail.
Therefore the effective map is not simply the set of registered ranges. It is the registered ranges filtered by dispatcher precedence.
RAM shadows MMIO¶
If generic MMIO overlaps RAM, a request wholly contained in RAM is always served by RAM.
The MMIO callback is not reached.
Framebuffer shadows MMIO¶
If generic MMIO overlaps the framebuffer and the request fits completely in framebuffer backing, framebuffer wins.
MMIO only owns remaining addresses¶
Generic MMIO is effectively a fallback physical region class.
This behavior should be documented as part of the platform contract until overlap is rejected explicitly.
Mixed-region requests are not split¶
A request spanning two region types can fail even when each byte is individually backed.
For example:
[last 2 RAM bytes][first 2 MMIO bytes]
as one 4-byte physical request.
The RAM fast path rejects the request because all four bytes do not fit in RAM.
The generic MMIO loop then starts at the original address and tries to resolve even the first two RAM bytes as MMIO.
If no MMIO mapping covers them, the request fails.
There is no region-boundary splitter.
The same design issue can occur at framebuffer boundaries.
A mature physical map should decompose requests into region-owned subranges or reject unsupported cross-region accesses in a deliberate, architecture-aware way.
MMIO transaction width is lost¶
Generic MMIO access is processed one byte at a time.
A 4-byte guest transaction produces four callback calls with:
size = 1
rather than one callback with size 4.
Therefore the physical map currently preserves byte values but not transaction width.
That matters for registers whose behavior depends on byte/word/dword/qword access width.
Future device work needs a dispatcher that identifies the owning region first, then sends the largest valid transaction to that device.
Unmapped physical memory¶
If an address lies outside RAM, framebuffer and registered MMIO, chris_phys_read/chris_phys_write return failure.
When the virtual-memory wrapper encounters this after a valid guest page-table translation, ChrisCPU classifies it as:
CHRIS_EXIT_UNMAPPED
rather than generating a normal guest #PF.
That is deliberate. The page tables may be valid, while the emulator has no machine resource backing the translated physical address.
This separates guest paging faults from missing virtual-hardware implementation.
Test MMIO at 96 MiB¶
test_chrisvm.c demonstrates a generic MMIO device at:
0x06000000
or 96 MiB.
The test manually installs a 2 MiB PDE that maps the virtual address to that physical region, then runs guest code that writes and reads the mapped cell.
This is important evidence that generic MMIO does not need to sit below the 32 MiB framebuffer or inside RAM.
The page-table mapping and physical backing are separate concerns.
Loader collision checks are incomplete by design¶
chris_load_elf explicitly rejects overlap with:
- final 16 KiB page-table reserve;
- GDT page;
- stack page.
It does not inspect the runtime MMIO registry.
It also does not need to reject framebuffer overlap directly because PT_LOAD segments must already fit inside RAM, and framebuffer starts at or above the RAM ceiling.
A future configurable memory map will need a generic region-collision API rather than a hard-coded list of boot constants.
Physical versus virtual addresses¶
The current boot protocol identity-maps RAM and framebuffer, but ChrisVM still maintains a conceptual distinction.
The MMU translates:
guest virtual address -> guest physical address
Then the machine dispatcher resolves:
guest physical address -> RAM / framebuffer / MMIO / unmapped
The two stages must remain separate.
The fact that many initial addresses are numerically identical is a property of boot protocol v1, not a reason to collapse the two models.
This separation is required for later higher-half kernels, user address spaces, shared mappings and device remapping.
GDT placement and virtual access¶
The GDT is stored in physical RAM at 0x70000.
Because RAM is identity-mapped during boot, GDTR.base is also set to 0x70000 and descriptor reads through guest virtual memory reach the same backing bytes.
This again depends on the initial identity mapping.
A future non-identity boot path can keep the GDT at one physical address while exposing it through a different virtual mapping, provided GDTR contains the virtual linear address expected by the CPU model.
Stack placement and growth¶
RSP begins at 0x80000 by default.
The reserved loader range is only one 4 KiB page below it.
The emulator does not enforce a dedicated stack-region limit. If guest code pushes beyond that page into lower mapped RAM, the MMU can continue to permit accesses as long as page tables allow them.
Thus the "stack page" is a loader ownership reservation, not a guard-enforced stack object.
A true guard page would require a not-present or protected mapping below the intended stack.
Reserved page-table memory and guest access¶
The final 16 KiB of RAM are excluded from ELF load segments, but they remain ordinary physical RAM after boot.
The initial identity map includes those physical addresses because RAM is mapped in 2 MiB chunks.
Guest code can therefore access or overwrite its own page tables through the identity mapping.
That is normal for privileged kernel code, but it means "reserved from loader" does not mean "protected from guest execution."
Protection would require page-table permissions or a separate ownership policy.
No central region descriptor¶
ChrisMachine does not currently have a generic structure such as:
region {
base
size
type
owner
permissions
access widths
priority
}
Instead, the map is inferred from:
- ram_size;
- framebuffer constants;
- MMIO slots;
- boot constants;
- dispatcher ordering.
This makes simple code possible, but complicates validation, visualization, snapshots and future hotplug.
A central region model would allow ChrisVM to generate its own authoritative map and reject collisions before execution.
Relationship to boot protocol v1¶
Boot protocol v1 assumes:
- guest ELF inside low RAM;
- identity-mapped RAM;
- fixed low GDT;
- fixed low stack;
- paging structures at top of RAM;
- fixed framebuffer at 32 MiB;
- no firmware-created map;
- no ACPI/e820-style discovery protocol.
The guest therefore receives a machine by convention, not by enumerating a firmware memory map.
This is sufficient for the current standalone ChrisVM guests but not yet enough to boot arbitrary operating systems expecting firmware memory discovery.
Current validation evidence¶
The repository tests verify:
- ELF segments cannot overlap boot-reserved regions;
- default boot creates functioning page tables;
- MMIO at 96 MiB can be mapped and accessed;
- framebuffer writes reach the fixed framebuffer backing;
- missing physical backing causes CHRIS_EXIT_UNMAPPED;
- page-table and paging permission behavior is checked independently by the MMU contract fixture.
These tests establish key map behavior but do not exhaustively test region collisions or every boundary crossing.
Hardening priorities¶
The highest-value next steps are:
- introduce one authoritative physical-region registry for RAM, framebuffer and MMIO;
- detect overlap at registration/construction time;
- remove the framebuffer-imposed RAM ceiling through relocatable device placement;
- split physical requests safely at region boundaries;
- preserve guest access width in MMIO dispatch;
- express boot-reserved regions through the same map rather than hard-coded ELF checks;
- define a real firmware/boot memory-map handoff for guests;
- add guard-page semantics for the initial stack if that protection is desired;
- classify the fourth reserved top-of-RAM page explicitly or release it;
- add boundary tests for RAM end, framebuffer end and adjacent MMIO regions;
- distinguish region permissions from page-table permissions;
- expose the generated physical map to debugger/documentation tooling.
Revision note¶
At revision e05a17fd76333114a3fb5c2452f38ca747d4ac56, the ChrisVM physical map is intentionally simple: low contiguous RAM, fixed boot structures inside RAM, a fixed framebuffer at 32 MiB and a small fallback MMIO registry. The design is sufficient for current ChrisCPU smoke guests, but it lacks one authoritative region model. The most important consequences are the 32 MiB RAM ceiling, implicit dispatch priority, unvalidated overlaps and inability to split one physical request across region boundaries.