Documentation
On this page

Concepts

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.

OffsetSizeField
0x0008magic SWEETXNU
0x008u32version, 1
0x00cu32nsegs, the number of segments
0x010u64virt_base: the kernel virtual address that maps to phys_base
0x018u64entry_va: the LC_UNIXTHREAD program counter
0x020u64adt_off, offset of the device tree (a multiple of 8)
0x028u64adt_size
0x030u64kernel_end_va: end of the highest segment, page aligned
0x038u64phys_base: the kernel-managed RAM, the same values as the ADT’s dram-base and dram-size
0x040u64mem_size
0x048608the default command line, NUL padded
0x2a832 eachone 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

  1. Finds the payload (SWEETXNU) at the initrd start, and asks bootrec.c for the command line: the boot-args record’s if one applies, else the payload’s own.
  2. Chooses the RAM layout (the memory map).
  3. Copies each segment to phys_base + (vmaddr - virt_base) and zeroes what a segment leaves past its file size.
  4. Copies the ADT behind the kernel and fills in the phone’s serial from LK’s command line when it finds one.
  5. 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, prefixed VIVAPAYL, at +0x1000 of that page, for the kernel’s kern.mapple_nextbootargs_orig. With the 4 GB layout it also leaves the hole list at +0x2000.
  6. Drops the instruction cache and jumps to entry_va with the MMU and caches off, x0 the physical address of boot_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