Skip to main content

Boot Chain

Discord-PXE does not begin in a process, a kernel module, or a booted operating system. Its first useful execution environment is the PXE firmware itself.

The boot path therefore has to preserve enough firmware state to keep using PXE services while simultaneously preparing a larger runtime that will eventually take over the machine.

Primary source:

Two-stage model​

network boot firmware
│
▼
small PXE NBP / stage 1
│
├─ preserve firmware entry state
├─ obtain cached DHCP/PXE data
├─ TFTP runtime.bin
└─ close transfer
│
▼
runtime.bin @ physical 0x20000
│
├─ platform discovery
├─ memory map
├─ PXE metadata
├─ paging
└─ x86-64 long mode
│
▼
64-bit bare-metal runtime

The split exists because the runtime has grown beyond what is convenient to treat as one tiny PXE network bootstrap program.

Preserving the PXE entry environment​

Older PXE ROMs can depend on the stack they used when entering the network bootstrap program.

The stage-1 loader captures the original:

SS
SP
client IP
server IP
gateway IP
subnet
router
DNS

into a fixed handoff structure below the later runtime/thunk region.

This is not decorative metadata. It exists so later code can make firmware calls without guessing which stack or network configuration the PXE ROM expects.

Firmware memory is not normal application memory

A region that appears unused from the perspective of the boot image may still contain a stack or scratch area used by the firmware implementation. Overwriting it before the firmware call path is finished can produce failures that look unrelated to memory corruption.

Guarding firmware-sensitive RAM​

The loader source documents a real example: the target PXE entry stack occupied memory around physical 0x83E68, while the growing runtime image eventually crossed that address during TFTP download.

The fix is not "hope the ROM no longer uses it." The loader explicitly preserves a neighborhood around the PXE stack while the transfer is active.

That is a recurring bare-metal rule in this project:

firmware-visible state must remain valid until the firmware dependency is actually gone.

PXENV+ validation​

The loader requests the PXE API through BIOS interrupt 0x1A and checks the returned PXENV+ structure.

Validation includes:

  • the PXENV+ signature
  • a non-zero structure length
  • checksum verification
  • extraction of the real-mode PXE entry point

A malformed or missing PXE environment is a hard stop, because every later TFTP operation depends on that contract.

Code example of validating a PXENV+ structure
; SI = pointer to PXENV+ structure
cmp byte [si + 0], 'P'
jne .bad
cmp byte [si + 1], 'X'
jne .bad
cmp byte [si + 2], 'E'
jne .bad
cmp byte [si + 3], 'N'
jne .bad
cmp byte [si + 4], 'V'
jne .bad
cmp byte [si + 5], '+'
jne .bad

movzx cx, byte [si + 8] ; declared structure length
test cx, cx
jz .bad

xor ax, ax ; AL accumulates 8-bit checksum
xor bx, bx
.sum:
add al, [si + bx]
inc bx
cmp bx, cx
jb .sum

test al, al ; PXENV+ checksum must wrap to zero
jnz .bad

This is a teaching equivalent of the validation logic, not a verbatim extract from the loader.

Cached DHCP/PXE state​

The stage-1 path uses PXENV_GET_CACHED_INFO to recover network state already negotiated by firmware.

That avoids implementing a second competing DHCP client just to discover values firmware already knows.

The handoff stores network values that the later runtime can reuse after switching execution modes.

Loading runtime.bin​

The first-stage image opens a TFTP transfer through PXE services and copies the larger runtime to physical address 0x20000.

The code deliberately protects:

  • loader state
  • the firmware stack guard area
  • the later runtime destination
  • fixed handoff structures

while the copy is in progress.

Once the transfer is complete and closed, execution jumps to the runtime segment.

Runtime relocation and bootstrap stack​

The larger runtime still begins in 16-bit mode.

It relocates its working segments and chooses a bootstrap stack below the runtime image so continued growth of runtime.bin cannot silently overwrite return addresses or counters.

This was specifically changed after the older high stack began overlapping executable bytes as the runtime grew.

Why these comments matter

Comments about memory addresses in this source are not historical clutter. They document concrete hardware failures that occurred as the binary layout changed. In a bare-metal project, preserving that context prevents a future "cleanup" from reintroducing an invisible platform dependency.

Platform discovery before long mode​

Before leaving the firmware-oriented environment, the runtime collects or validates:

  • BIOS E820 memory map
  • PXE API information
  • cached network information
  • x86-64 CPU capability

If long mode support is unavailable, the runtime cannot continue.

Long-mode transition​

The runtime then:

  1. enables A20
  2. constructs page tables
  3. loads a GDT
  4. enables PAE
  5. loads CR3
  6. enables EFER.LME
  7. enables paging/protected mode
  8. far-jumps into the 64-bit code segment

The page-table setup uses fixed low physical addresses during transition.

Why PXE remains visible after boot​

Discord-PXE is not merely "loaded using PXE." PXE affects early memory layout, firmware call safety, cached network state, and the design of the stage-1/runtime boundary.

That is why the project treats the boot chain as an engineering subsystem rather than as a build/deployment detail.

Failure boundaries​

Useful boot failures are intentionally distinguishable:

PXE API unavailable
PXENV+ invalid
cached DHCP unavailable
TFTP runtime load failed
x86-64 unsupported
long-mode transition failed
runtime started but later subsystem failed

Serial breadcrumbs are inserted early enough to place the failure before most higher-level systems exist.

See Diagnostics.

Long-mode activation sequence​

The following is the real transition sequence from pxe_boot.asm. The Rust, C, and Python tabs are semantic equivalents. The NASM tab is the authoritative implementation.

Context before the block
Symbol / registerMeaning
gdt_ptrPointer to the temporary GDT used for the mode switch.
PML4_PHYSPhysical address of the PML4 page, currently 0x90000.
CR4.PAEEnables Physical Address Extension, required by IA-32e paging.
IA32_EFER.LMELong Mode Enable bit in MSR 0xC0000080.
CR0.PGTurns paging on.
CR0.PEProtected-mode enable bit.
0x0864-bit code-segment selector in the GDT.
long_mode_entryFirst instruction stream executed with a 64-bit CS.
cli ; Stop maskable interrupts during descriptor/control-register changes.
lgdt [gdt_ptr] ; Load GDTR with the temporary GDT descriptor.

mov eax, cr4 ; Read the current CR4 feature bits.
or eax, 1 << 5 ; Set CR4.PAE, required for IA-32e page-table format.
mov cr4, eax ; Commit the updated CR4 value.

mov eax, PML4_PHYS ; EAX = physical address of the prepared PML4.
mov cr3, eax ; Make that PML4 the active paging root.

mov ecx, 0xC0000080 ; ECX = IA32_EFER MSR number.
rdmsr ; Read EFER into EDX:EAX.
or eax, 1 << 8 ; Set EFER.LME (Long Mode Enable).
wrmsr ; Write the modified EFER value back.

mov eax, cr0 ; Read CR0.
or eax, (1 << 31) | 1 ; Set CR0.PG (paging) and CR0.PE (protected mode).
mov cr0, eax ; Activates paging with long mode armed.

db 0x66,0xEA ; Emit operand-size override + far-jump opcode manually.
dd RELOC_BASE + long_mode_entry ; 32-bit offset of the 64-bit entry point.
dw 0x08 ; GDT selector for the 64-bit code segment.
Architectural boundary

The CPU is not fully executing 64-bit code merely because EFER.LME is set. Long mode becomes active through the combination of PAE, long-mode-capable paging, CR0.PG, and finally loading a 64-bit code segment with the far jump.

Success condition

After the far jump, CS references the 64-bit descriptor and execution continues at long_mode_entry. If the descriptor, page tables, or control bits are wrong, the machine typically faults or resets before higher-level diagnostics exist.

State after the block​

  • CR4.PAE = 1
  • CR3 = PML4_PHYS
  • IA32_EFER.LME = 1
  • CR0.PG = 1
  • CR0.PE = 1
  • CS has been reloaded with selector 0x08
  • instruction decoding is now on the 64-bit path

PXE firmware stack handoff​

Some PXE ROMs remain dependent on the stack they used when entering the NBP. Discord-PXE temporarily restores that exact stack around firmware calls.

Context before the block
Symbol / registerMeaning
pxe_initial_ss:spStack supplied by the PXE ROM when it invoked the NBP.
loader_ss:spDiscord-PXE loader stack that must be restored after the firmware call.
pxe_entryReal-mode far pointer to the PXE firmware API entry point.
BXPXE function opcode supplied by the caller.
ES:DIPXE parameter-block pointer supplied by the caller.
pxe_call: ; BX=function, ES:DI=PXE parameter block.
cli ; Prevent an interrupt from using a half-switched stack.
mov [cs:loader_sp], sp ; Save Discord-PXE's current SP.
mov ax, ss ; AX = Discord-PXE's current SS.
mov [cs:loader_ss], ax ; Save Discord-PXE's current SS.

mov ax, [cs:pxe_initial_ss] ; AX = stack segment originally supplied by PXE ROM.
mov ss, ax ; Restore firmware's expected stack segment.
mov sp, [cs:pxe_initial_sp] ; Restore firmware's expected stack pointer.
sti ; Firmware call may now execute with interrupts enabled.

push ds ; Preserve caller-visible DS across firmware.
push si ; Preserve SI.
push bp ; Preserve BP.
call far [cs:pxe_entry] ; Invoke the PXE ROM through its far real-mode entry point.
pop bp ; Restore BP after firmware returns.
pop si ; Restore SI.
pop ds ; Restore DS.

cli ; Protect the stack transition back to the loader.
mov dx, [cs:loader_ss] ; DX = saved loader SS.
mov ss, dx ; Restore loader stack segment.
mov sp, [cs:loader_sp] ; Restore loader stack pointer.
cld ; Re-establish DF=0 because firmware is not trusted to preserve it.
sti ; Resume normal interrupt handling.
ret ; Return to the loader caller.
Why CLI surrounds the stack swap

An interrupt between writing SS and restoring the matching SP could push an interrupt frame onto the wrong stack. The critical section keeps the pair coherent.

ABI reality

This is not a normal C ABI call. The firmware contract includes segment registers, a far function pointer, a caller-provided PXE opcode in BX, and a parameter block in ES:DI.

State after the block​

Discord-PXE is back on its own loader stack, DF=0, the PXE call has completed, and the caller can inspect the firmware status/result fields without leaving the ROM's stack active.

Source​