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]]
-=-=-=-=-=-=-=-=-=-=-=-


Reply via email to