Payload
What the shim boots: the kernel, the device tree and a command line in one file.
The payload is one file that holds everything the loader needs to start XNU: the kernel’s segments, the device tree
(ADT) and the default boot arguments. mkpayload.py makes it; the shim reads it from the ramdisk range that LK passes
for the boot image (linux,initrd-start and -end in /chosen), copies the kernel into place and jumps to it.
Why a payload
XNU is built as a position-independent Mach-O with chained fixups. Nothing on the phone resolves those, and XNU only
rebases static kernelcaches itself, so mkpayload.py walks the fixup chains and rewrites every pointer to its
unslid target. The shim then only copies flat segments. The kernel carries no binds: the script refuses one.
Layout
All little endian, all offsets from the start of the payload.
| Offset | Size | Field |
|---|---|---|
0x000 | 8 | magic SWEETXNU |
0x008 | u32 | version, 1 |
0x00c | u32 | nsegs, the number of segments |
0x010 | u64 | virt_base: the kernel virtual address that maps to phys_base |
0x018 | u64 | entry_va: the LC_UNIXTHREAD program counter |
0x020 | u64 | adt_off, offset of the device tree (a multiple of 8) |
0x028 | u64 | adt_size |
0x030 | u64 | kernel_end_va: end of the highest segment, page aligned |
0x038 | u64 | phys_base: the kernel-managed RAM, the same values as the ADT’s dram-base and dram-size |
0x040 | u64 | mem_size |
0x048 | 608 | the default command line, NUL padded |
0x2a8 | 32 each | one entry per segment: vmaddr, vmsize, file_off (in the payload), file_size |
The shim checks the magic, the version and the alignments and stops with “bad payload header” otherwise; the header’s size is asserted at compile time.
virt_base is the kernel’s __TEXT address rounded down to 32 MB, the size of an L2 block with 16 KB pages, so the
kernel’s early block mapping, which starts at the header’s block, begins exactly at phys_base. kernel_end_va is
page aligned to 16 KB. --ramdisk IMAGE appends an image as one more segment after the kernel, moves kernel_end
past it, and adds /chosen/memory-map RAMDisk to the ADT, which XNU turns into md0.
What the shim does with it
- Finds the payload (
SWEETXNU) at the initrd start, and asksbootrec.cfor the command line: the boot-args record’s if one applies, else the payload’s own. - Chooses the RAM layout (the memory map).
- Copies each segment to
phys_base + (vmaddr - virt_base)and zeroes what a segment leaves past its file size. - Copies the ADT behind the kernel and fills in the phone’s serial from LK’s command line when it finds one.
- Builds
boot_args(revision 2, version 2) in the next 16 KB page: the layout bases, the framebuffer (FB_BASE, stride, width, height, depth 32),topOfKernelData, the device tree’s address and length, and the command line. When the record replaced the payload’s line, it keeps the payload’s own, prefixedVIVAPAYL, at+0x1000of that page, for the kernel’skern.mapple_nextbootargs_orig. With the 4 GB layout it also leaves the hole list at+0x2000. - Drops the instruction cache and jumps to
entry_vawith the MMU and caches off,x0the physical address ofboot_args.
Everything below topOfKernelData belongs to the loader and is never handed to the VM.
The payload’s command line is the one that stays when the record is off, and the one a safe boot uses: it is never
written from iOS (kern.mapple_nextbootargs writes the record only).
Source: tools/mkpayload.py of mapple_build at f7431bb; shim/loader.c of mapple_loader at 2aa5786; bsd/dev/mapple_sysctl.c
of the kernel at 76b737c.
Applies to Release 1, MediaTek · Updated 15 November 2026