ChainloadApp carries the payload it launches as a C array, which takes a two-stage build that a single build invocation cannot express.
BuildChainloadEmbedded.sh drives both stages: first UEFIPAYLOAD.fd, then GenPayloadHdr.py to embed it as a C array, then ChainloadApp.efi. The __has_include() mechanism in ChainloadApp.c selects the generated header over the stub, so GenPayloadHdr.py emits no include guard of its own. Both build passes use one -D option set, so pass 2 does not rebuild the payload with different libraries than the embedded FD. Add a README next to the application documenting what ChainloadApp does, which HOBs it emits, the AArch64 MMU-on handoff on launcher-owned page tables, and how to build and test it with BuildChainloadEmbedded.sh and QEMU. Cc: Benjamin Doron <[email protected]> Cc: Gua Guo <[email protected]> Cc: Guo Dong <[email protected]> Cc: James Lu <[email protected]> Cc: Sean Rhodes <[email protected]> Cc: Shuo Liu <[email protected]> Cc: Ard Biesheuvel <[email protected]> Cc: Leif Lindholm <[email protected]> Cc: Sami Mujawar <[email protected]> Cc: Vishal Oliyil Kunnnil <[email protected]> Assisted-by: claude-opus-5 Signed-off-by: Alexander Graf <[email protected]> --- UefiPayloadPkg/BuildChainloadEmbedded.sh | 125 +++++++++++++++++++ UefiPayloadPkg/ChainloadApp/GenPayloadHdr.py | 61 +++++++++ UefiPayloadPkg/ChainloadApp/README.md | 116 +++++++++++++++++ 3 files changed, 302 insertions(+) create mode 100755 UefiPayloadPkg/BuildChainloadEmbedded.sh create mode 100644 UefiPayloadPkg/ChainloadApp/GenPayloadHdr.py create mode 100644 UefiPayloadPkg/ChainloadApp/README.md diff --git a/UefiPayloadPkg/BuildChainloadEmbedded.sh b/UefiPayloadPkg/BuildChainloadEmbedded.sh new file mode 100755 index 0000000000..d6324d71f7 --- /dev/null +++ b/UefiPayloadPkg/BuildChainloadEmbedded.sh @@ -0,0 +1,125 @@ +#!/bin/bash +## @file +# Build ChainloadApp with embedded payload +# +# Copyright (c) 2026, Amazon.com, Inc. or its affiliates. All Rights Reserved.<BR> +# SPDX-License-Identifier: BSD-2-Clause-Patent +## + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# Under PACKAGES_PATH or any multi-repo workspace -- the normal +# edk2-platforms arrangement -- UefiPayloadPkg's parent directory is not +# the edk2 root and holds no edksetup.sh. Prefer $WORKSPACE when the +# caller has already sourced edksetup.sh, and only fall back to guessing +# from the script location otherwise. +if [ -n "$WORKSPACE" ]; then + EDK2_DIR="$WORKSPACE" +else + EDK2_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + if [ ! -f "$EDK2_DIR/edksetup.sh" ]; then + echo "Error: WORKSPACE is not set and $EDK2_DIR/edksetup.sh does not exist." >&2 + echo "Source edksetup.sh from your edk2 workspace before running this script." >&2 + exit 1 + fi + + cd "$EDK2_DIR" + + # edksetup.sh returns non-zero on several paths, including some it + # recovers from, which under set -e would abort here before anything + # useful is printed. Check for the variable it is meant to export + # instead of trusting its exit status. + set +e + # shellcheck disable=SC1091 + source edksetup.sh + set -e + + if [ -z "$WORKSPACE" ]; then + echo "Error: sourcing $EDK2_DIR/edksetup.sh did not set WORKSPACE." >&2 + exit 1 + fi +fi + +# Build configuration +BUILD_TARGET="${BUILD_TARGET:-RELEASE}" +ARCH="${ARCH:-X64}" + +# Select toolchain based on architecture +if [ "$ARCH" = "AARCH64" ]; then + TOOL_CHAIN="${TOOL_CHAIN_TAG:-GCC5}" + export GCC5_AARCH64_PREFIX="${GCC5_AARCH64_PREFIX:-aarch64-unknown-linux-gnu-}" +else + TOOL_CHAIN="${TOOL_CHAIN_TAG:-GCC5}" +fi + +cd "$EDK2_DIR" + +# Both build passes below share one Build/ output tree, so any +# BUILD_DEFINES entry that changes library selection must be applied +# to both. BUILD_ARCH gives each ISA its own OUTPUT_DIRECTORY so +# concurrent X64 and AArch64 builds do not overwrite each other's +# UEFIPAYLOAD.fd or intermediate objects. +BUILD_DEFINES=(-D BOOTLOADER=SBL + -D TIMER_SUPPORT=LAPIC + -D CHAINLOAD_DEFAULTS=TRUE + -D VIRTIO_ENABLE=TRUE + -D BUILD_ARCH="Legacy${ARCH}") + +OUT_DIR="$EDK2_DIR/Build/UefiPayloadPkgLegacy${ARCH}/${BUILD_TARGET}_${TOOL_CHAIN}" + +echo "=== Building UniversalPayload ($BUILD_TARGET, $ARCH) ===" +build -p UefiPayloadPkg/UefiPayloadPkg.dsc \ + -b "$BUILD_TARGET" \ + -t "$TOOL_CHAIN" \ + -a "$ARCH" \ + "${BUILD_DEFINES[@]}" + +PAYLOAD_FD="$OUT_DIR/FV/UEFIPAYLOAD.fd" + +if [ ! -f "$PAYLOAD_FD" ]; then + echo "Error: Payload not found at $PAYLOAD_FD" + exit 1 +fi + +echo "=== Generating embedded payload header ===" +BUILD_DIR="$OUT_DIR/${ARCH}/UefiPayloadPkg/ChainloadApp/ChainloadApp/DEBUG" +mkdir -p "$BUILD_DIR" +PAYLOAD_HDR="$BUILD_DIR/EmbeddedPayload.h" +python3 "$SCRIPT_DIR/ChainloadApp/GenPayloadHdr.py" "$PAYLOAD_FD" "$PAYLOAD_HDR" + +# The pass-1 build compiled ChainloadApp.c against the stub header +# before EmbeddedPayload.h existed, and its .deps file does not name +# the header. Remove the pass-1 object so pass 2 recompiles it. This +# is per-arch and per-target, so a concurrent build for another ISA is +# not disturbed (touching the shared source file would be). +rm -f "$OUT_DIR/${ARCH}/UefiPayloadPkg/ChainloadApp/ChainloadApp/OUTPUT/ChainloadApp.obj" + +echo "=== Building ChainloadApp ===" +build -p UefiPayloadPkg/UefiPayloadPkg.dsc \ + -b "$BUILD_TARGET" \ + -t "$TOOL_CHAIN" \ + -a "$ARCH" \ + "${BUILD_DEFINES[@]}" \ + -m UefiPayloadPkg/ChainloadApp/ChainloadApp.inf + +CHAINLOAD_EFI="$OUT_DIR/${ARCH}/ChainloadApp.efi" + +echo "" +echo "=== Build complete ===" +echo "Payload: $PAYLOAD_FD" +echo "ChainloadApp: $CHAINLOAD_EFI" +echo "" +if [ "$ARCH" = "X64" ]; then + echo "Test with QEMU (X64):" + echo " qemu-system-x86_64 -bios /path/to/OVMF_CODE.fd -m 1G -nographic -enable-kvm \\" + echo " -net none -netdev user,tftp=Build/UefiPayloadPkgLegacy${ARCH}/${BUILD_TARGET}_${TOOL_CHAIN}/${ARCH}/,bootfile=ChainloadApp.efi,id=nd \\" + echo " -device virtio-net-pci,netdev=nd" +else + echo "Test with QEMU (AArch64):" + echo " qemu-system-aarch64 -M virt -cpu cortex-a57 -m 1G -nographic \\" + echo " -bios /path/to/AAVMF_CODE.fd \\" + echo " -net none -netdev user,tftp=Build/UefiPayloadPkgLegacy${ARCH}/${BUILD_TARGET}_${TOOL_CHAIN}/${ARCH}/,bootfile=ChainloadApp.efi,id=nd \\" + echo " -device virtio-net-pci,netdev=nd" +fi diff --git a/UefiPayloadPkg/ChainloadApp/GenPayloadHdr.py b/UefiPayloadPkg/ChainloadApp/GenPayloadHdr.py new file mode 100644 index 0000000000..b92c6e72c8 --- /dev/null +++ b/UefiPayloadPkg/ChainloadApp/GenPayloadHdr.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +## @file +# Generate C header with embedded payload binary +# +# Copyright (c) 2026, Amazon.com, Inc. or its affiliates. All Rights Reserved.<BR> +# SPDX-License-Identifier: BSD-2-Clause-Patent +## + +import sys +import os + +def main(): + if len(sys.argv) < 3: + print(f"Usage: {sys.argv[0]} <input.fd> <output.h>", file=sys.stderr) + return 1 + + input_file = sys.argv[1] + output_file = sys.argv[2] + + if not os.path.exists(input_file): + print(f"Error: Input file '{input_file}' not found", file=sys.stderr) + return 1 + + with open(input_file, 'rb') as f: + data = f.read() + + with open(output_file, 'w') as f: + f.write("// Auto-generated - do not edit\n") + f.write(f"// Source: {os.path.basename(input_file)}\n\n") + # + # FindFvInPayload() casts addresses inside this array to + # EFI_FIRMWARE_VOLUME_HEADER * and reads a UINT64 field from + # them, so the array needs a stated alignment rather than + # whatever a compiler happens to give a large object. + # + f.write("#if defined (_MSC_VER)\n") + f.write("#define CHAINLOAD_PAYLOAD_ALIGN __declspec (align (8))\n") + f.write("#else\n") + f.write("#define CHAINLOAD_PAYLOAD_ALIGN __attribute__ ((aligned (8)))\n") + f.write("#endif\n\n") + f.write("CHAINLOAD_PAYLOAD_ALIGN STATIC CONST UINT8 mPayloadData[] = {\n") + + for i, byte in enumerate(data): + if i % 16 == 0: + f.write(" ") + f.write(f"0x{byte:02X},") + if i % 16 == 15: + f.write("\n") + else: + f.write(" ") + + if len(data) % 16 != 0: + f.write("\n") + f.write("};\n\n") + f.write("STATIC CONST UINTN mPayloadSize = sizeof(mPayloadData);\n") + + print(f"Generated {output_file} ({len(data)} bytes)") + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/UefiPayloadPkg/ChainloadApp/README.md b/UefiPayloadPkg/ChainloadApp/README.md new file mode 100644 index 0000000000..77641a8477 --- /dev/null +++ b/UefiPayloadPkg/ChainloadApp/README.md @@ -0,0 +1,116 @@ +# ChainloadApp + +A UEFI application that chainloads a full UefiPayloadPkg firmware volume +from a running UEFI environment. + +## Overview + +ChainloadApp embeds `UEFIPAYLOAD.fd` and transfers control to it after +calling `ExitBootServices()`. This lets a fresh, self-contained UEFI +environment take over from a platform firmware image that cannot be +modified, e.g. for guest firmware development inside a VM whose outer +firmware is fixed. + +## How it works + +1. **Payload embedding.** `UEFIPAYLOAD.fd` is embedded as a C array in + `EmbeddedPayload.h`, generated at build time by + `ChainloadApp/GenPayloadHdr.py` from the compiled FV. + +2. **HOB construction.** ChainloadApp builds the Hand-Off Blocks the + payload's entry point expects: + - `gUniversalPayloadAcpiTableGuid` (RSDP from the UEFI configuration table) + - `gUniversalPayloadSmbiosTableGuid` (SMBIOS entry point, if present) + - `gUniversalPayloadExtraDataGuid` (payload FV location) + - `gUefiSerialPortInfoGuid` and `gUniversalPayloadSerialPortInfoGuid` + (serial console configuration, derived from the ACPI SPCR table) + - Memory map records converted from the UEFI memory map, with GCD + MMIO regions surfaced as Reserved with `MEM_MAP_FLAG_MMIO` set + + A `gUniversalPayloadPciRootBridgeInfoGuid` HOB is *not* emitted; the + payload's `PciHostBridgeLib` derives roots from ACPI MCFG. + +3. **Control transfer.** After `ExitBootServices()`, ChainloadApp jumps to + the payload's `_ModuleEntryPoint` with the HOB list address. + + On AArch64, ChainloadApp builds its own translation tables (in + `EfiReservedMemoryType` pages) via `ArmConfigureMmu()` while boot + services are still available, then installs them after + `ExitBootServices()` and branches with the MMU and caches enabled. + No data-cache maintenance is needed; the FV is invalidated from the + instruction cache for I/D coherency. The payload's `HandOffToDxeCore()` + adopts the live translation; CpuDxe later edits it in place. + +## Supported architectures + +- **X64** +- **AArch64** + +## Building + +`BuildChainloadEmbedded.sh` runs two build passes. The first builds +`UEFIPAYLOAD.fd`; the header generator then turns that FD into +`EmbeddedPayload.h`, and the second pass rebuilds ChainloadApp against +it. Both passes share `-D CHAINLOAD_DEFAULTS=TRUE`. + +```bash +cd /path/to/edk2 +source edksetup.sh +./UefiPayloadPkg/BuildChainloadEmbedded.sh +``` + +Output: +`Build/UefiPayloadPkgLegacy${ARCH}/${BUILD_TARGET}_${TOOL_CHAIN_TAG}/${ARCH}/ChainloadApp.efi` +— with the defaults below, that is +`Build/UefiPayloadPkgLegacyX64/RELEASE_GCC5/X64/ChainloadApp.efi`. + +Environment variables: + +| Variable | Default | Notes | +|---|---|---| +| `ARCH` | `X64` | `AARCH64` for arm64 | +| `BUILD_TARGET` | `RELEASE` | `DEBUG` / `NOOPT` | +| `TOOL_CHAIN_TAG` | `GCC5` | | +| `GCC5_AARCH64_PREFIX` | `aarch64-unknown-linux-gnu-` | AArch64 cross-toolchain prefix | + +Example (AArch64, DEBUG): + +```bash +ARCH=AARCH64 BUILD_TARGET=DEBUG \ +GCC5_AARCH64_PREFIX=aarch64-linux-gnu- \ +./UefiPayloadPkg/BuildChainloadEmbedded.sh +``` + +## Testing with QEMU + +### X64 (OVMF) + +```bash +qemu-system-x86_64 -m 1G -nographic -enable-kvm -bios OVMF_CODE.fd \ + -netdev user,tftp=Build/UefiPayloadPkgLegacyX64/RELEASE_GCC5/X64/,bootfile=ChainloadApp.efi,id=n \ + -device virtio-net-pci,netdev=n +``` + +### AArch64 (`-M virt`) + +```bash +dd if=/dev/zero of=disk.img bs=1M count=64 && mkfs.vfat disk.img +mmd -i disk.img ::/EFI ::/EFI/BOOT +mcopy -i disk.img Build/UefiPayloadPkgLegacyAARCH64/RELEASE_GCC5/AARCH64/ChainloadApp.efi ::/EFI/BOOT/BOOTAA64.EFI + +qemu-system-aarch64 -M virt -cpu cortex-a72 -m 1G -nographic \ + -bios QEMU_EFI.fd -drive file=disk.img,format=raw,if=virtio +``` + +## Files + +- `ChainloadApp.c` / `ChainloadApp.inf` — the application +- `AArch64/PayloadEntry.S`, `X64/PayloadEntry.nasm` — handoff trampolines +- `GenPayloadHdr.py` — FD-to-C-array generator +- `EmbeddedPayloadStub.h` — placeholder header for the first build pass +- `../BuildChainloadEmbedded.sh` — two-pass build script + +--- + +Copyright (c) 2026, Amazon.com, Inc. or its affiliates. All Rights Reserved.<BR> +SPDX-License-Identifier: BSD-2-Clause-Patent -- 2.47.3 -=-=-=-=-=-=-=-=-=-=-=- Groups.io Links: You receive all messages sent to this group. View/Reply Online (#122105): https://edk2.groups.io/g/devel/message/122105 Mute This Topic: https://groups.io/mt/120797312/21656 Group Owner: [email protected] Unsubscribe: https://edk2.groups.io/g/devel/unsub [[email protected]] -=-=-=-=-=-=-=-=-=-=-=-
