In preparation for launching UEFIPAYLOAD.fd on AArch64 with
ChainloadApp, extend UefiPayloadEntry to support AArch64 alongside
IA32/X64 in the legacy (non-Universal) payload entry path.

AArch64/DxeHandoff.c: on the raw-bootloader path (MMU off at entry),
build ARM_MEMORY_REGION_DESCRIPTOR entries from the
resource-descriptor HOBs and call ArmConfigureMmu() to install page
tables owned by the payload. ResourceType is the primary
discriminator: system memory and firmware-reserved DRAM are mapped
write-back, EFI_RESOURCE_MEMORY_MAPPED_IO is mapped Device. A
descriptor that is unambiguously UNCACHEABLE-only overrides the type
switch, so ECAM published as MEMORY_RESERVED (see "Optionally publish
MCFG as Reserved" earlier in this series) is mapped Device instead of
cacheable.

On the ChainloadApp path the MMU is already on: a later change in this
series teaches ChainloadApp to install its own translation tables
before ExitBootServices() and to enter the payload with the MMU and
caches enabled. Those tables are pinned by an EfiBootServicesData
memory-allocation HOB emitted from the launcher's boot-time
reservation list, so DXE cannot allocate over them.
HandOffToDxeCore() adopts that live translation instead of building a
fresh one. ArmPkg's CpuDxe begins with ASSERT(ArmMmuEnabled()) and
manages memory attributes by editing the live tables via
ArmSetMemoryAttributes(); it never needs a freshly-built hierarchy.
Calling ArmConfigureMmu() with the MMU already enabled and an unknown
incoming TCR/MAIR is not safe, since it programs TCR and MAIR while
the outgoing TTBR0 is still live and performs no TLB invalidation, and
is not attempted here.

UefiPayloadEntry.c: guard the x86-only Local APIC MMIO HOB and legacy
8259 PIC mask writes with MDE_CPU_IA32/X64, and guard the x86
TOLUD-based reserved-vs-MMIO heuristic in MemInfoCallbackMmio() the
same way. On AArch64, DRAM starts at or above 1 GiB and the payload FV
is a reserved range in that window; classifying it as MMIO maps it
Device+XN and the payload faults on the first instruction after
ArmEnableMmu().

UefiPayloadPkg.dsc: set PcdArmArchTimerVirtIntrNum to 27 (the QEMU
virt default; a real platform overrides it).

UefiPayloadPkg.fdf: enlarge FD_SIZE for AArch64 to fit the ARM library
instances (~2 MiB over the X64 build).

Under CHAINLOAD_DEFAULTS switch AArch64 to the HOB-driven
BaseSerialPortLibHob: ChainloadApp derives the serial console from the
ACPI SPCR, so the fixed-address PL011SerialPortLib is only correct on
QEMU virt.

Library/AcpiGicPcdLib: NULL library plugged into ArmGicDxe under
CHAINLOAD_DEFAULTS. Its constructor walks the bootloader-supplied ACPI
MADT for the GIC Distributor, Redistributor and memory-mapped
CPU-interface bases, overrides PcdGicDistributorBase,
PcdGicRedistributorsBase and PcdGicInterruptInterfaceBase (all moved
to PcdsDynamicExDefault for this build), and adds the ranges to the
GCD memory space so the payload page tables cover them. The table
lookup goes through AcpiFindTableFromRsdp() from the shared
AcpiTableWalkLib, so the library carries no ACPI walk of its own.

Every base the MADT can supply is derived first, then sufficiency is
decided from the CPU: ArmHasGicSystemRegisters() present means
ArmGicDxe will most likely take the v3 path and a redistributor is
required; absent means it will take the v2 path and the CPU-interface
base is required. A MADT that is present but does not supply the
required base is fatal: we print the specific reason and call
CpuDeadLoop(). Falling back to the QEMU-virt defaults on a real
platform would let ArmGicDxe touch a foreign address, and returning an
error status alone would not stop that in a RELEASE build because the
AutoGen constructor wrapper only ASSERT_EFI_ERROR()s it.

The MADT GICD.GicVersion byte is not used as a gate: ArmGicDxe's own
GicV3Supported() explicitly distrusts a table-reported version ("this
does not seem to be very reliable as the implementation could easily
get it wrong") and dispatches on the CPU's system-register feature and
a runtime ICC_SRE_EL2.SRE probe instead, so a lazily-filled version
byte would fail a boot that would otherwise succeed. The byte is read
only for a diagnostic warning when it disagrees with what the CPU
implies. The CPU-interface base is derived and published whenever GICC
supplies a non-zero one, so the SRE-denied fallback to the v2 path
also has a correct value.

Several AcpiGicPcdLib paths are not exercised on the platform this was
developed against: the GICv2 distributor-size branch, the GICC
publish-and-map path (that platform reports
GICC.PhysicalBaseAddress = 0), the no-system-registers arm, and the
ICC_SRE_EL2.SRE-denied fallback. The GICv3 path runs on every boot. We
keep the unexercised branches deliberately: removing them would make a
valid GICv2 MADT hard-fail.

Override IoLib on AArch64 to BaseIoLibIntrinsicArmVirt, matching
ArmVirtPkg. BaseIoLibIntrinsic uses plain C volatile pointer
dereferences for MmioRead*/MmioWrite*, which the compiler may fuse
with adjacent pointer arithmetic into post-indexed loads/stores. On a
stage-2 data abort a post-indexed access has ESR_EL2.ISV=0, so KVM
cannot decode it as MMIO and delivers KVM_EXIT_ARM_NISV to the VMM
instead of KVM_EXIT_MMIO. The ArmVirt variant uses hand-written
register-offset accesses that always yield ISV=1.

Cc: Ard Biesheuvel <[email protected]>
Cc: Benjamin Doron <[email protected]>
Cc: Gua Guo <[email protected]>
Cc: Guo Dong <[email protected]>
Cc: James Lu <[email protected]>
Cc: Leif Lindholm <[email protected]>
Cc: Sami Mujawar <[email protected]>
Cc: Sean Rhodes <[email protected]>
Cc: Shuo Liu <[email protected]>
Cc: Vishal Oliyil Kunnnil <[email protected]>
Assisted-by: claude-opus-5
Signed-off-by: Alexander Graf <[email protected]>
---
 .../Library/AcpiGicPcdLib/AcpiGicPcdLib.c     | 664 ++++++++++++++++++
 .../Library/AcpiGicPcdLib/AcpiGicPcdLib.inf   |  50 ++
 .../UefiPayloadEntry/AArch64/DxeHandoff.c     | 398 ++++++++++-
 .../UefiPayloadEntry/UefiPayloadEntry.c       |  20 +-
 .../UefiPayloadEntry/UefiPayloadEntry.inf     |   4 +
 UefiPayloadPkg/UefiPayloadPkg.dsc             |  53 +-
 UefiPayloadPkg/UefiPayloadPkg.fdf             |   9 +
 7 files changed, 1190 insertions(+), 8 deletions(-)
 create mode 100644 UefiPayloadPkg/Library/AcpiGicPcdLib/AcpiGicPcdLib.c
 create mode 100644 UefiPayloadPkg/Library/AcpiGicPcdLib/AcpiGicPcdLib.inf

diff --git a/UefiPayloadPkg/Library/AcpiGicPcdLib/AcpiGicPcdLib.c 
b/UefiPayloadPkg/Library/AcpiGicPcdLib/AcpiGicPcdLib.c
new file mode 100644
index 0000000000..bc64975bff
--- /dev/null
+++ b/UefiPayloadPkg/Library/AcpiGicPcdLib/AcpiGicPcdLib.c
@@ -0,0 +1,664 @@
+/** @file

+  NULL library that discovers the GIC Distributor, Redistributor and

+  memory-mapped CPU-interface bases from the ACPI MADT supplied by

+  the bootloader and populates the corresponding PCDs before

+  ArmGicDxe consumes them.

+

+  UefiPayloadPkg carries QEMU-virt fixed defaults for the GIC PCDs.

+  When the outer firmware handed over ACPI tables (via ChainloadApp

+  or a Slim Bootloader), the actual GIC location is described by the

+  MADT GICD/GICR/GICC structures.  Read those, override the PCDs, and

+  add the ranges to the GCD memory space so ArmGicDxe can touch them.

+

+  With no ACPI handover at all the fixed defaults stay in place, which

+  is the existing behaviour for a plain QEMU-virt boot.

+

+  Which base ArmGicDxe actually reads is a partly-runtime decision

+  (see the file-scope commentary at ArmPkg/Drivers/ArmGicDxe/

+  ArmGicDxe.c: it dispatches on the CPU's GICv3 system-register

+  feature and on whether ICC_SRE_EL2.SRE can be enabled, or on the

+  CPU's GICv5 system-register feature, and drives the interrupt

+  controller as a v2 otherwise).  This library therefore

+  derives every base the MADT can supply, and only afterwards decides

+  whether what it has is sufficient for the path ArmGicDxe will take.

+  The MADT GICD's GicVersion byte is read purely as a diagnostic

+  cross-check: upstream deliberately does not trust a table-reported

+  GIC version, and neither does this code.

+

+  A MADT that is present but does not describe the base that

+  ArmGicDxe's chosen path needs is fatal, not a reason to fall back to

+  the QEMU-virt defaults: those addresses are not a GIC on any other

+  platform, so letting ArmGicDxe walk them yields either a bus abort

+  or a plausible-looking read followed by a mute hang with no

+  interrupt controller - and in a RELEASE build, no diagnostic either.

+  In every such case the specific reason is printed and CpuDeadLoop()

+  is called: the DXE AutoGen constructor wrapper only

+  ASSERT_EFI_ERROR()s a status returned from here, so in a RELEASE

+  build a returned error alone would be discarded and the entry point

+  would run anyway.  A payload without an interrupt controller cannot

+  boot, so halting loses nothing over the mute hang and makes the

+  failure diagnosable.

+

+  Copyright (c) 2026, Amazon.com, Inc. or its affiliates.<BR>

+  SPDX-License-Identifier: BSD-2-Clause-Patent

+**/

+

+#include <PiDxe.h>

+#include <IndustryStandard/Acpi.h>

+#include <UniversalPayload/AcpiTable.h>

+

+#include <Library/AcpiTableWalkLib.h>

+#include <Library/ArmLib.h>

+#include <Library/BaseLib.h>

+#include <Library/DebugLib.h>

+#include <Library/DxeServicesTableLib.h>

+#include <Library/HobLib.h>

+#include <Library/PcdLib.h>

+

+/**

+  Locate an ACPI table by signature via the RSDP the bootloader

+  handed over in the gUniversalPayloadAcpiTableGuid HOB.

+

+  @param[in]  Signature  4-byte ACPI table signature.

+

+  @return  Pointer to the table header, or NULL if not found.

+**/

+STATIC

+EFI_ACPI_DESCRIPTION_HEADER *

+LocateAcpiTable (

+  IN UINT32  Signature

+  )

+{

+  EFI_HOB_GUID_TYPE             *GuidHob;

+  UNIVERSAL_PAYLOAD_ACPI_TABLE  *AcpiHob;

+

+  GuidHob = GetFirstGuidHob (&gUniversalPayloadAcpiTableGuid);

+  if (GuidHob == NULL) {

+    return NULL;

+  }

+

+  AcpiHob = (UNIVERSAL_PAYLOAD_ACPI_TABLE *)GET_GUID_HOB_DATA (GuidHob);

+

+  return AcpiFindTableFromRsdp (AcpiHob->Rsdp, Signature);

+}

+

+/**

+  Add a device MMIO range to the GCD memory map and mark it uncacheable

+  and non-executable, so that ArmCpuDxe populates a page-table entry

+  for it.

+

+  If the range is already present in the GCD map, only proceed when the

+  existing descriptor is an unowned MMIO descriptor that covers the

+  whole request.  A bogus MADT can point a GIC base into DRAM, and

+  remapping live system memory uncacheable and non-executable is not a

+  warning-level event.  This is the same check the PL031 RTC library

+  performs for the same situation.

+

+  @param[in] Base    Base physical address.

+  @param[in] Length  Length in bytes.

+

+  @retval EFI_SUCCESS        The range is MMIO and is now mapped UC|XP.

+  @retval EFI_ACCESS_DENIED  The range is already described as something

+                             other than unowned MMIO covering it whole.

+  @retval other              GCD service failure.

+**/

+STATIC

+EFI_STATUS

+MapGicMmio (

+  IN EFI_PHYSICAL_ADDRESS  Base,

+  IN UINT64                Length

+  )

+{

+  EFI_STATUS                       Status;

+  EFI_GCD_MEMORY_SPACE_DESCRIPTOR  Desc;

+

+  Status = gDS->AddMemorySpace (

+                  EfiGcdMemoryTypeMemoryMappedIo,

+                  Base,

+                  Length,

+                  EFI_MEMORY_UC | EFI_MEMORY_XP

+                  );

+  if (Status == EFI_ACCESS_DENIED) {

+    //

+    // Something already describes part or all of the range, and

+    // EFI_ACCESS_DENIED does not say what.  Refuse to touch the

+    // attributes unless it is MMIO that no driver owns, and unless that

+    // single descriptor covers the whole request: AddMemorySpace() also

+    // returns EFI_ACCESS_DENIED for a partial overlap, while

+    // GetMemorySpaceDescriptor() only returns the descriptor containing

+    // Base.

+    //

+    Status = gDS->GetMemorySpaceDescriptor (Base, &Desc);

+    if (EFI_ERROR (Status)) {

+      return Status;

+    }

+

+    if ((Desc.GcdMemoryType != EfiGcdMemoryTypeMemoryMappedIo) ||

+        (Desc.ImageHandle != NULL) ||

+        (Desc.BaseAddress > Base) ||

+        ((Base + Length) > (Desc.BaseAddress + Desc.Length)))

+    {

+      DEBUG ((

+        DEBUG_ERROR,

+        "%a: 0x%Lx(0x%Lx) is already described as GCD type %u owned by %p "

+        "over 0x%Lx(0x%Lx); refusing to remap\n",

+        __func__,

+        Base,

+        Length,

+        (UINT32)Desc.GcdMemoryType,

+        Desc.ImageHandle,

+        Desc.BaseAddress,

+        Desc.Length

+        ));

+      return EFI_ACCESS_DENIED;

+    }

+

+    //

+    // A pre-existing MMIO descriptor need not carry the UC and XP

+    // capabilities, and CoreSetMemorySpaceAttributes() rejects any

+    // attribute that is absent from Capabilities.  Add them first.

+    //

+    if ((Desc.Capabilities & (EFI_MEMORY_UC | EFI_MEMORY_XP)) !=

+        (EFI_MEMORY_UC | EFI_MEMORY_XP))

+    {

+      Status = gDS->SetMemorySpaceCapabilities (

+                      Base,

+                      Length,

+                      Desc.Capabilities | EFI_MEMORY_UC | EFI_MEMORY_XP

+                      );

+      if (EFI_ERROR (Status)) {

+        DEBUG ((

+          DEBUG_ERROR,

+          "%a: SetMemorySpaceCapabilities(0x%Lx, 0x%Lx): %r\n",

+          __func__,

+          Base,

+          Length,

+          Status

+          ));

+        return Status;

+      }

+    }

+  } else if (EFI_ERROR (Status)) {

+    DEBUG ((

+      DEBUG_ERROR,

+      "%a: AddMemorySpace(0x%Lx, 0x%Lx): %r\n",

+      __func__,

+      Base,

+      Length,

+      Status

+      ));

+    return Status;

+  }

+

+  Status = gDS->SetMemorySpaceAttributes (

+                  Base,

+                  Length,

+                  EFI_MEMORY_UC | EFI_MEMORY_XP

+                  );

+  if (EFI_ERROR (Status)) {

+    DEBUG ((

+      DEBUG_ERROR,

+      "%a: SetMemorySpaceAttributes(0x%Lx, 0x%Lx): %r\n",

+      __func__,

+      Base,

+      Length,

+      Status

+      ));

+  }

+

+  return Status;

+}

+

+/**

+  Halt after the caller has printed why the MADT is unusable.

+

+  See the file header for why the QEMU-virt build-time defaults must

+  not survive an unusable MADT, and why returning an error status is

+  not sufficient in a RELEASE build.

+**/

+STATIC

+VOID

+GicMadtFatal (

+  VOID

+  )

+{

+  DEBUG ((

+    DEBUG_ERROR,

+    "AcpiGicPcdLib: no usable interrupt controller can be derived from "

+    "the MADT and the build-time PCD defaults are only correct on QEMU "

+    "virt; halting.\n"

+    ));

+  CpuDeadLoop ();

+}

+

+/**

+  Constructor: parse the ACPI MADT for the GICD/GICR/GICC bases,

+  override the GIC PCDs, and map the MMIO ranges.

+

+  @param  ImageHandle  Image handle (unused).

+  @param  SystemTable  System table (unused).

+

+  @retval EFI_SUCCESS  No ACPI handover, so the fixed PCD defaults

+                       stand; or the base(s) required by ArmGicDxe's

+                       chosen init path were derived and mapped.

+  @return              An error status only reachable in the caller if

+                       CpuDeadLoop() were to return; documents which

+                       failure was hit.

+**/

+EFI_STATUS

+EFIAPI

+AcpiGicPcdLibConstructor (

+  IN EFI_HANDLE        ImageHandle,

+  IN EFI_SYSTEM_TABLE  *SystemTable

+  )

+{

+  EFI_ACPI_DESCRIPTION_HEADER             *Madt;

+  EFI_ACPI_6_0_GIC_DISTRIBUTOR_STRUCTURE  *Gicd;

+  EFI_ACPI_6_0_GICR_STRUCTURE             *Gicr;

+  EFI_ACPI_6_0_GIC_STRUCTURE              *Gicc;

+  EFI_STATUS                              Status;

+  RETURN_STATUS                           PcdStatus;

+  UINT8                                   *Ptr;

+  UINT8                                   *End;

+  UINT64                                  DistBase;

+  UINT64                                  RedistBase;

+  UINT64                                  RedistLen;

+  UINT64                                  CpuIfBase;

+  UINT64                                  ThisCpuIfBase;

+  UINTN                                   GicrCount;

+  UINTN                                   GiccRedistCount;

+  UINTN                                   Length;

+  UINT8                                   GicVersion;

+  BOOLEAN                                 HaveSysRegs;

+  BOOLEAN                                 CpuIfMismatch;

+

+  Madt = LocateAcpiTable (

+           EFI_ACPI_6_0_MULTIPLE_APIC_DESCRIPTION_TABLE_SIGNATURE

+           );

+  if (Madt == NULL) {

+    DEBUG ((DEBUG_INFO, "%a: no MADT, keeping fixed GIC PCDs\n", __func__));

+    return EFI_SUCCESS;

+  }

+

+  if (Madt->Length < sizeof 
(EFI_ACPI_6_0_MULTIPLE_APIC_DESCRIPTION_TABLE_HEADER)) {

+    DEBUG ((

+      DEBUG_ERROR,

+      "%a: MADT length %u is shorter than its own header\n",

+      __func__,

+      Madt->Length

+      ));

+    GicMadtFatal ();

+    return EFI_VOLUME_CORRUPTED;

+  }

+

+  DistBase        = 0;

+  RedistBase      = 0;

+  RedistLen       = 0;

+  CpuIfBase       = 0;

+  GicrCount       = 0;

+  GiccRedistCount = 0;

+  GicVersion      = 0;

+  CpuIfMismatch   = FALSE;

+

+  Ptr = (UINT8 *)Madt +

+        sizeof (EFI_ACPI_6_0_MULTIPLE_APIC_DESCRIPTION_TABLE_HEADER);

+  End = (UINT8 *)Madt + Madt->Length;

+

+  //

+  // Walk the interrupt controller structures.  Each one is Type, Length,

+  // then a type-specific body, so both bytes must be present before

+  // Length can be read, Length must be large enough to advance, and it

+  // must not run past the end of the table.

+  //

+  while ((UINTN)(End - Ptr) >= 2) {

+    Length = Ptr[1];

+

+    if ((Length < 2) || (Length > (UINTN)(End - Ptr))) {

+      DEBUG ((

+        DEBUG_ERROR,

+        "%a: MADT structure type %u at offset 0x%Lx has bad length %u\n",

+        __func__,

+        (UINT32)Ptr[0],

+        (UINT64)(UINTN)(Ptr - (UINT8 *)Madt),

+        (UINT32)Length

+        ));

+      GicMadtFatal ();

+      return EFI_VOLUME_CORRUPTED;

+    }

+

+    switch (Ptr[0]) {

+      case EFI_ACPI_6_0_GICD:

+        if (Length < (OFFSET_OF (EFI_ACPI_6_0_GIC_DISTRIBUTOR_STRUCTURE, 
PhysicalBaseAddress) +

+                      sizeof (UINT64)))

+        {

+          DEBUG ((

+            DEBUG_WARN,

+            "%a: GICD structure is %u bytes, too short for a base address; 
ignoring\n",

+            __func__,

+            (UINT32)Length

+            ));

+          break;

+        }

+

+        Gicd     = (EFI_ACPI_6_0_GIC_DISTRIBUTOR_STRUCTURE *)Ptr;

+        DistBase = ReadUnaligned64 (&Gicd->PhysicalBaseAddress);

+

+        //

+        // GicVersion sits after SystemVectorBase and was added in

+        // ACPI 6.0.  A pre-6.0 GICD is 24 bytes and does not carry it,

+        // so bound it separately and leave GicVersion at 0

+        // ("unspecified") when absent.  It is read only for the

+        // cross-check warning below and is never a gate.

+        //

+        if (Length >= (OFFSET_OF (EFI_ACPI_6_0_GIC_DISTRIBUTOR_STRUCTURE, 
GicVersion) +

+                       sizeof (UINT8)))

+        {

+          GicVersion = Gicd->GicVersion;

+        }

+

+        break;

+

+      case EFI_ACPI_6_0_GICR:

+        if (Length < (OFFSET_OF (EFI_ACPI_6_0_GICR_STRUCTURE, 
DiscoveryRangeLength) +

+                      sizeof (UINT32)))

+        {

+          DEBUG ((

+            DEBUG_WARN,

+            "%a: GICR structure is %u bytes, too short for a discovery range; 
ignoring\n",

+            __func__,

+            (UINT32)Length

+            ));

+          break;

+        }

+

+        Gicr = (EFI_ACPI_6_0_GICR_STRUCTURE *)Ptr;

+        GicrCount++;

+

+        //

+        // Several GICR structures are legal: they describe several

+        // discovery ranges.  PcdGicRedistributorsBase carries one base,

+        // and ArmGicDxe walks frames from it until GICR_TYPER.Last, so

+        // it can never reach the others.  Keep the first and warn below,

+        // rather than silently keeping whichever came last.

+        //

+        if (GicrCount == 1) {

+          RedistBase = ReadUnaligned64 (&Gicr->DiscoveryRangeBaseAddress);

+          RedistLen  = ReadUnaligned32 (&Gicr->DiscoveryRangeLength);

+        }

+

+        break;

+

+      case EFI_ACPI_6_0_GIC:

+        //

+        // GICC.PhysicalBaseAddress is the memory-mapped CPU-interface

+        // address that GicV2DxeInitialize() reads via

+        // PcdGicInterruptInterfaceBase.  In GICv2 all PEs share one

+        // GICC MMIO window (banked per-PE), which is why the PCD is a

+        // single value; every enabled GICC entry should therefore

+        // report the same address.  The ACPI spec permits it to be 0

+        // on a platform without GICv2 compatibility support, so 0 is

+        // "not derivable" here rather than a value to publish.

+        //

+        // The field has been present since the GICC structure was

+        // introduced in ACPI 5.0, so bound it independently of

+        // GICRBaseAddress below.

+        //

+        if (Length >= (OFFSET_OF (EFI_ACPI_6_0_GIC_STRUCTURE, 
PhysicalBaseAddress) +

+                       sizeof (UINT64)))

+        {

+          Gicc          = (EFI_ACPI_6_0_GIC_STRUCTURE *)Ptr;

+          ThisCpuIfBase = ReadUnaligned64 (&Gicc->PhysicalBaseAddress);

+

+          if (ThisCpuIfBase != 0) {

+            if (CpuIfBase == 0) {

+              CpuIfBase = ThisCpuIfBase;

+            } else if (CpuIfBase != ThisCpuIfBase) {

+              CpuIfMismatch = TRUE;

+            }

+          }

+        }

+

+        //

+        // GICRBaseAddress was added to the GICC structure in ACPI 5.1;

+        // an older, shorter GICC simply does not describe it.

+        //

+        if (Length < (OFFSET_OF (EFI_ACPI_6_0_GIC_STRUCTURE, GICRBaseAddress) +

+                      sizeof (UINT64)))

+        {

+          break;

+        }

+

+        Gicc = (EFI_ACPI_6_0_GIC_STRUCTURE *)Ptr;

+        if (ReadUnaligned64 (&Gicc->GICRBaseAddress) != 0) {

+          GiccRedistCount++;

+        }

+

+        break;

+

+      default:

+        break;

+    }

+

+    Ptr += Length;

+  }

+

+  if (GicrCount > 1) {

+    DEBUG ((

+      DEBUG_WARN,

+      "%a: MADT has %u GICR structures; only the first discovery range is 
used\n",

+      __func__,

+      (UINT32)GicrCount

+      ));

+  }

+

+  if (CpuIfMismatch) {

+    DEBUG ((

+      DEBUG_WARN,

+      "%a: MADT GICC entries report differing PhysicalBaseAddress values; "

+      "using 0x%Lx for PcdGicInterruptInterfaceBase\n",

+      __func__,

+      CpuIfBase

+      ));

+  }

+

+  if (DistBase == 0) {

+    DEBUG ((DEBUG_ERROR, "%a: MADT describes no GIC distributor\n", __func__));

+    GicMadtFatal ();

+    return EFI_NOT_FOUND;

+  }

+

+  //

+  // ArmGicDxe (see ArmPkg/Drivers/ArmGicDxe/ArmGicDxe.c) chooses its

+  // init path from the CPU, not from the MADT: GicV3Supported() checks

+  // ArmHasGicSystemRegisters() and then whether ICC_SRE_EL2.SRE can be

+  // set, because "the GICC IIDR Architecture version [...] does not

+  // seem to be very reliable"; ArmHasGicV5SystemRegisters() also

+  // selects the v3 path (ArmGicDxe.c:77).  Only when neither is

+  // available does it drive the GIC as a v2.  Whether SRE sticks depends

+  // on the higher exception level and cannot be predicted here without

+  // repeating the write ArmGicDxe is about to perform.

+  //

+  // Use the same first-order predicate to decide which base is

+  // required.  When system registers are present the v3 path is the

+  // likely one and needs a redistributor; when they are absent the v2

+  // path is certain and needs the CPU interface.  In either case

+  // derive and publish the CPU-interface base whenever GICC supplies a

+  // non-zero one, so the SRE-denied fallback to v2 has a correct value

+  // rather than the QEMU-virt build-time default.

+  //

+  HaveSysRegs = ArmHasGicSystemRegisters ();

+

+  if (HaveSysRegs) {

+    if (GicrCount == 0) {

+      if (GiccRedistCount != 0) {

+        //

+        // ACPI 6.0 lets a platform describe the redistributors per PE in

+        // GICC.GICRBaseAddress instead of as one contiguous discovery

+        // range, and that is exactly the case where the frames are not

+        // contiguous.  PcdGicRedistributorsBase carries a single base and

+        // ArmGicDxe walks frames from it until GICR_TYPER.Last, so any

+        // range synthesised from the per-PE bases would be a guess about

+        // the platform's layout.  Report it instead of guessing.

+        //

+        DEBUG ((

+          DEBUG_ERROR,

+          "%a: MADT describes the redistributors per PE in 
GICC.GICRBaseAddress "

+          "(%u of them); PcdGicRedistributorsBase cannot express that\n",

+          __func__,

+          (UINT32)GiccRedistCount

+          ));

+        GicMadtFatal ();

+        return EFI_UNSUPPORTED;

+      }

+

+      DEBUG ((

+        DEBUG_ERROR,

+        "%a: CPU has GICv3 system registers but the MADT describes no "

+        "GIC redistributor in either form\n",

+        __func__

+        ));

+      GicMadtFatal ();

+      return EFI_NOT_FOUND;

+    }

+

+    if ((RedistBase == 0) || (RedistLen == 0)) {

+      //

+      // ArmGicDxe walks redistributor frames from the published base until

+      // one reports GICR_TYPER.Last, so the whole discovery range has to

+      // be mapped.  A GICv3 redistributor is 128 KiB and a GICv4 one is

+      // 256 KiB, so no fixed guess covers an SMP system: the walk would

+      // read past the mapping.  If the MADT gives no length, fail.

+      //

+      DEBUG ((

+        DEBUG_ERROR,

+        "%a: MADT GICR discovery range is unusable: base 0x%Lx, length 
0x%Lx\n",

+        __func__,

+        RedistBase,

+        RedistLen

+        ));

+      GicMadtFatal ();

+      return EFI_UNSUPPORTED;

+    }

+  } else {

+    if (CpuIfBase == 0) {

+      DEBUG ((

+        DEBUG_ERROR,

+        "%a: CPU has no GICv3 system registers so ArmGicDxe will take the "

+        "v2 path, but the MADT GICC entries describe no memory-mapped CPU "

+        "interface (PhysicalBaseAddress is zero or absent)\n",

+        __func__

+        ));

+      GicMadtFatal ();

+      return EFI_NOT_FOUND;

+    }

+  }

+

+  //

+  // Cross-check the GICD.GicVersion byte against what the structures

+  // and CPU imply, purely for diagnostics.  0 means the field is

+  // absent (pre-6.0 GICD) or the firmware left it unspecified.  This

+  // never gates anything for the reason quoted above.

+  //

+  if (GicVersion != 0) {

+    if (HaveSysRegs && (GicVersion < EFI_ACPI_6_0_GIC_V3)) {

+      DEBUG ((

+        DEBUG_WARN,

+        "%a: MADT GICD.GicVersion is %u but the CPU implements GICv3 "

+        "system registers; ignoring the reported version\n",

+        __func__,

+        (UINT32)GicVersion

+        ));

+    } else if (!HaveSysRegs && (GicVersion >= EFI_ACPI_6_0_GIC_V3)) {

+      DEBUG ((

+        DEBUG_WARN,

+        "%a: MADT GICD.GicVersion is %u but the CPU has no GICv3 system "

+        "registers; ignoring the reported version\n",

+        __func__,

+        (UINT32)GicVersion

+        ));

+    }

+  }

+

+  //

+  // The MADT carries no distributor length.  ArmGicDxe maps the

+  // distributor as GICD_V3_SIZE (64 KiB) on the v3 path and

+  // GICD_V2_SIZE (4 KiB) on the v2 path (see GicV3DxeInitialize()

+  // and GicV2DxeInitialize() respectively), so map at least what the

+  // path that will run needs.  Mapping the larger unconditionally is

+  // not safe on a genuine GICv2 platform: the 60 KiB beyond the

+  // distributor is not the GIC's, and if any of it is already in the

+  // GCD map MapGicMmio() refuses to touch it and this constructor

+  // halts a boot that would otherwise succeed.  HaveSysRegs is the

+  // same first-order predicate GicV3Supported() uses to choose the

+  // path, and the same predicate this constructor already used above

+  // to decide sufficiency.  In the SRE-denied corner case (HaveSysRegs

+  // TRUE, ArmGicDxe falls back to v2) 64 KiB is mapped where the v2

+  // path uses only 4; that is safe because on such a platform the

+  // distributor block is architecturally 64 KiB regardless of which

+  // interface the driver chooses.

+  //

+  Status = MapGicMmio (DistBase, HaveSysRegs ? SIZE_64KB : SIZE_4KB);

+  if (EFI_ERROR (Status)) {

+    GicMadtFatal ();

+    return Status;

+  }

+

+  PcdStatus = PcdSet64S (PcdGicDistributorBase, DistBase);

+  ASSERT_RETURN_ERROR (PcdStatus);

+

+  if ((GicrCount != 0) && (RedistBase != 0) && (RedistLen != 0)) {

+    Status = MapGicMmio (RedistBase, RedistLen);

+    if (EFI_ERROR (Status)) {

+      GicMadtFatal ();

+      return Status;

+    }

+

+    PcdStatus = PcdSet64S (PcdGicRedistributorsBase, RedistBase);

+    ASSERT_RETURN_ERROR (PcdStatus);

+  } else {

+    DEBUG ((

+      DEBUG_WARN,

+      "%a: PcdGicRedistributorsBase left at its build-time default 0x%Lx\n",

+      __func__,

+      PcdGet64 (PcdGicRedistributorsBase)

+      ));

+  }

+

+  if (CpuIfBase != 0) {

+    //

+    // GicV2DxeInitialize() maps the CPU interface itself as

+    // GICC_V2_SIZE, i.e. 8 KiB.  Use the same size here.

+    //

+    Status = MapGicMmio (CpuIfBase, SIZE_8KB);

+    if (EFI_ERROR (Status)) {

+      GicMadtFatal ();

+      return Status;

+    }

+

+    PcdStatus = PcdSet64S (PcdGicInterruptInterfaceBase, CpuIfBase);

+    ASSERT_RETURN_ERROR (PcdStatus);

+  } else {

+    DEBUG ((

+      DEBUG_WARN,

+      "%a: PcdGicInterruptInterfaceBase left at its build-time default "

+      "0x%Lx; the GICv2 fallback would use it if ICC_SRE_EL2.SRE were "

+      "denied\n",

+      __func__,

+      PcdGet64 (PcdGicInterruptInterfaceBase)

+      ));

+  }

+

+  DEBUG ((

+    DEBUG_INFO,

+    "%a: GICD 0x%Lx, GICR 0x%Lx (len 0x%Lx), GICC 0x%Lx from MADT; "

+    "CPU %a GICv3 sysregs\n",

+    __func__,

+    DistBase,

+    RedistBase,

+    RedistLen,

+    CpuIfBase,

+    HaveSysRegs ? "has" : "lacks"

+    ));

+

+  return EFI_SUCCESS;

+}

diff --git a/UefiPayloadPkg/Library/AcpiGicPcdLib/AcpiGicPcdLib.inf 
b/UefiPayloadPkg/Library/AcpiGicPcdLib/AcpiGicPcdLib.inf
new file mode 100644
index 0000000000..1f84a8fb61
--- /dev/null
+++ b/UefiPayloadPkg/Library/AcpiGicPcdLib/AcpiGicPcdLib.inf
@@ -0,0 +1,50 @@
+## @file

+#  Discover the GIC Distributor, Redistributor and CPU-interface bases

+#  from the ACPI MADT supplied by the bootloader and populate the

+#  ArmGic PCDs.

+#

+#  Plugged into ArmGicDxe as a NULL library so that the constructor

+#  runs before InterruptDxeInitialize() reads PcdGicDistributorBase,

+#  PcdGicRedistributorsBase and PcdGicInterruptInterfaceBase.

+#

+#  Copyright (c) 2026, Amazon.com, Inc. or its affiliates.<BR>

+#  SPDX-License-Identifier: BSD-2-Clause-Patent

+##

+

+[Defines]

+  INF_VERSION                    = 0x00010005

+  BASE_NAME                      = AcpiGicPcdLib

+  FILE_GUID                      = 4E7CB4BF-DF23-4AA1-8B1F-5A47DA0EC1E6

+  MODULE_TYPE                    = DXE_DRIVER

+  VERSION_STRING                 = 1.0

+  LIBRARY_CLASS                  = NULL|DXE_DRIVER

+  CONSTRUCTOR                    = AcpiGicPcdLibConstructor

+

+[Sources]

+  AcpiGicPcdLib.c

+

+[Packages]

+  ArmPkg/ArmPkg.dec

+  MdePkg/MdePkg.dec

+  MdeModulePkg/MdeModulePkg.dec

+  UefiPayloadPkg/UefiPayloadPkg.dec

+

+[LibraryClasses]

+  AcpiTableWalkLib

+  ArmLib

+  BaseLib

+  DebugLib

+  DxeServicesTableLib

+  HobLib

+  PcdLib

+

+[Guids]

+  gUniversalPayloadAcpiTableGuid

+

+[Pcd]

+  gArmTokenSpaceGuid.PcdGicDistributorBase

+  gArmTokenSpaceGuid.PcdGicRedistributorsBase

+  gArmTokenSpaceGuid.PcdGicInterruptInterfaceBase

+

+[Depex]

+  gEfiCpuArchProtocolGuid

diff --git a/UefiPayloadPkg/UefiPayloadEntry/AArch64/DxeHandoff.c 
b/UefiPayloadPkg/UefiPayloadEntry/AArch64/DxeHandoff.c
index f54cc82109..7205c3cd4d 100644
--- a/UefiPayloadPkg/UefiPayloadEntry/AArch64/DxeHandoff.c
+++ b/UefiPayloadPkg/UefiPayloadEntry/AArch64/DxeHandoff.c
@@ -13,11 +13,376 @@
 #include <Library/MemoryAllocationLib.h>

 #include <Library/PcdLib.h>

 #include <Library/HobLib.h>

+#include <Library/ArmMmuLib.h>

+#include <Library/ArmLib.h>

 

 #include "UefiPayloadEntry.h"

 

 #define STACK_SIZE  0x20000

 

+//

+// Upper bound on the number of mappable resource descriptor HOBs the

+// bootloader may hand over.  Exceeding it is an error rather than a

+// reason to map a subset of the address space; see

+// ConfigureMmuFromHobs().

+//

+#define MAX_RESOURCE_HOBS  256

+

+//

+// Splitting N input ranges at every distinct boundary yields at most

+// 2 * N - 1 elementary intervals, so an output table of 2 * N entries

+// plus the zero-Length terminator can never overflow.

+//

+#define MAX_DESCRIPTORS  (2 * MAX_RESOURCE_HOBS)

+

+//

+// One mappable range collected from a resource descriptor HOB, before

+// overlaps between HOBs are resolved.

+//

+typedef struct {

+  UINT64                          Start;

+  UINT64                          End;

+  ARM_MEMORY_REGION_ATTRIBUTES    Attributes;

+  UINTN                           Priority;

+} MMU_INPUT_REGION;

+

+STATIC ARM_MEMORY_REGION_DESCRIPTOR  mVirtualMemoryTable[MAX_DESCRIPTORS + 1];

+STATIC MMU_INPUT_REGION              mInputRegions[MAX_RESOURCE_HOBS];

+STATIC UINT64                        mBoundaries[2 * MAX_RESOURCE_HOBS];

+

+/**

+  Return the ARM memory attributes for a HOB resource descriptor.

+

+  ResourceType is the primary discriminator: MEMORY_RESERVED covers

+  firmware-reserved DRAM (ACPI NVS, the payload FV/HOB/stack), which

+  must be write-back so that unaligned accesses do not fault and no

+  mismatched-attribute alias of DRAM is created; MEMORY_MAPPED_IO is

+  Device.  The cacheability bits in ResourceAttribute state which

+  types the range supports, not which type is wanted (the payload's

+  MemInfoCallbackMmio() advertises UC|WC|WT|WB on everything), so

+  only an unambiguous UNCACHEABLE-only attribute overrides the type

+  switch: that is how ECAM published as MEMORY_RESERVED under

+  PcdPublishMcfgAsReservedMemory is mapped Device rather than cacheable.

+

+  @param[in]  Resource  The HOB resource descriptor.

+  @param[out] Attr      Returned ARM memory attributes.

+

+  @retval TRUE   Descriptor should be mapped with the returned Attr.

+  @retval FALSE  Descriptor should be skipped.

+**/

+STATIC

+BOOLEAN

+ArmAttributesForResourceHob (

+  IN  EFI_HOB_RESOURCE_DESCRIPTOR   *Resource,

+  OUT ARM_MEMORY_REGION_ATTRIBUTES  *Attr

+  )

+{

+  EFI_RESOURCE_ATTRIBUTE_TYPE  Ra;

+

+  Ra = Resource->ResourceAttribute;

+

+  if (((Ra & EFI_RESOURCE_ATTRIBUTE_UNCACHEABLE) != 0) &&

+      ((Ra & (EFI_RESOURCE_ATTRIBUTE_WRITE_BACK_CACHEABLE |

+              EFI_RESOURCE_ATTRIBUTE_WRITE_THROUGH_CACHEABLE |

+              EFI_RESOURCE_ATTRIBUTE_WRITE_COMBINEABLE)) == 0))

+  {

+    *Attr = ARM_MEMORY_REGION_ATTRIBUTE_DEVICE;

+    return TRUE;

+  }

+

+  switch (Resource->ResourceType) {

+    case EFI_RESOURCE_SYSTEM_MEMORY:

+    case EFI_RESOURCE_MEMORY_RESERVED:

+      *Attr = ARM_MEMORY_REGION_ATTRIBUTE_WRITE_BACK;

+      return TRUE;

+    case EFI_RESOURCE_MEMORY_MAPPED_IO:

+      *Attr = ARM_MEMORY_REGION_ATTRIBUTE_DEVICE;

+      return TRUE;

+    default:

+      break;

+  }

+

+  return FALSE;

+}

+

+/**

+  Return a priority for an ARM memory attribute, used to resolve

+  overlaps between resource descriptor HOBs.

+

+  A cacheable alias of a device aperture is a mismatched-attribute

+  alias, and its failure mode is an abort or silent data corruption.

+  Mapping a few pages of DRAM as Device only costs performance.  So

+  where two descriptors overlap, the device/uncached attribute wins.

+

+  @param[in] Attributes  ARM memory region attributes.

+

+  @return  Priority.  The higher value wins where two ranges overlap.

+**/

+STATIC

+UINTN

+ArmAttributePriority (

+  IN ARM_MEMORY_REGION_ATTRIBUTES  Attributes

+  )

+{

+  switch (Attributes) {

+    case ARM_MEMORY_REGION_ATTRIBUTE_DEVICE:

+    case ARM_MEMORY_REGION_ATTRIBUTE_UNCACHED_UNBUFFERED:

+      return 1;

+    default:

+      return 0;

+  }

+}

+

+/**

+  Configure the MMU from the HOB resource descriptors.

+

+  Only called when the payload is entered with the MMU off, i.e.

+  from a raw bootloader.  ArmConfigureMmu() then populates a fresh

+  translation table, installs it in TTBR0 and enables the MMU.

+

+  ChainloadApp instead installs its own tables (in Reserved pages)

+  before ExitBootServices() and enters the payload with the MMU and

+  caches on; HandOffToDxeCore() adopts that live translation and

+  never reaches this function.  Calling ArmConfigureMmu() with the

+  MMU already enabled and an unknown incoming TCR/MAIR is not safe:

+  it programs TCR and MAIR while the outgoing TTBR0 is still live,

+  and ArmSetTTBR0() performs no TLB invalidation, so stale entries

+  from the previous tables stay usable afterwards.

+

+  The bootloader's descriptors may overlap.  The payload emits a

+  SYSTEM_MEMORY descriptor covering all of DRAM together with

+  MEMORY_RESERVED carve-outs inside it, and some of those carve-outs

+  have to be mapped Device rather than cacheable - ECAM published

+  under PcdPublishMcfgAsReservedMemory, for one.  ArmConfigureMmu()

+  applies the descriptor array in order and a later descriptor

+  overwrites an earlier one, so handing it the HOB list as-is would

+  make the attribute of an ECAM aperture depend on the order the

+  bootloader happened to emit its HOBs in.  Instead the ranges are

+  collected, split at every distinct boundary, and each resulting

+  interval is given the highest-priority attribute among the ranges

+  covering it.  The result has no overlapping entries at all, so it

+  does not depend on HOB order.

+

+  @retval EFI_SUCCESS           MMU configured.

+  @retval EFI_OUT_OF_RESOURCES  More mappable resource descriptor HOBs

+                                than MAX_RESOURCE_HOBS; the address

+                                space cannot be mapped in full.

+  @retval EFI_NOT_FOUND         No mappable resource descriptor HOB.

+  @retval other                 ArmConfigureMmu() failure.

+**/

+STATIC

+EFI_STATUS

+ConfigureMmuFromHobs (

+  VOID

+  )

+{

+  EFI_PEI_HOB_POINTERS          Hob;

+  EFI_HOB_RESOURCE_DESCRIPTOR   *Resource;

+  ARM_MEMORY_REGION_ATTRIBUTES  Attr;

+  ARM_MEMORY_REGION_ATTRIBUTES  IntervalAttr;

+  VOID                          *TranslationTableBase;

+  UINTN                         TranslationTableSize;

+  UINTN                         RegionCount;

+  UINTN                         BoundaryCount;

+  UINTN                         Count;

+  UINTN                         Index;

+  UINTN                         Inner;

+  UINTN                         Best;

+  UINT64                        Base;

+  UINT64                        End;

+  UINT64                        Value;

+  BOOLEAN                       Truncated;

+  BOOLEAN                       Found;

+

+  RegionCount = 0;

+  Truncated   = FALSE;

+

+  //

+  // Collect every mappable range.  Overlaps are resolved below, so the

+  // order the HOBs arrive in does not matter here.

+  //

+  Hob.Raw = GetFirstHob (EFI_HOB_TYPE_RESOURCE_DESCRIPTOR);

+  while (Hob.Raw != NULL) {

+    Resource = (EFI_HOB_RESOURCE_DESCRIPTOR *)Hob.Raw;

+

+    if (ArmAttributesForResourceHob (Resource, &Attr)) {

+      if (RegionCount == MAX_RESOURCE_HOBS) {

+        Truncated = TRUE;

+        break;

+      }

+

+      Base = Resource->PhysicalStart & ~(UINT64)EFI_PAGE_MASK;

+      End  = ALIGN_VALUE (

+               Resource->PhysicalStart + Resource->ResourceLength,

+               EFI_PAGE_SIZE

+               );

+

+      //

+      // A sub-page-aligned MMIO region adjacent to RAM would otherwise

+      // produce two entries covering the same page with different

+      // attributes.  Warn on bootloader-supplied misalignment and align

+      // outward (the end is already ALIGN_VALUE()'d up), rather than

+      // asserting on data this code does not control.

+      //

+      if (Resource->PhysicalStart != Base) {

+        DEBUG ((

+          DEBUG_WARN,

+          "%a: resource at 0x%Lx not page-aligned; aligning outward\n",

+          __func__,

+          Resource->PhysicalStart

+          ));

+      }

+

+      if (End > Base) {

+        mInputRegions[RegionCount].Start      = Base;

+        mInputRegions[RegionCount].End        = End;

+        mInputRegions[RegionCount].Attributes = Attr;

+        mInputRegions[RegionCount].Priority   = ArmAttributePriority (Attr);

+        RegionCount++;

+      }

+    }

+

+    Hob.Raw = GET_NEXT_HOB (Hob);

+    Hob.Raw = GetNextHob (EFI_HOB_TYPE_RESOURCE_DESCRIPTOR, Hob.Raw);

+  }

+

+  //

+  // Truncating the HOB walk would leave regions the payload needs out

+  // of the page tables, and the failure would surface much later as an

+  // unrelated abort.  Fail here instead, so that HandOffToDxeCore()

+  // reports the cause.

+  //

+  if (Truncated) {

+    DEBUG ((

+      DEBUG_ERROR,

+      "%a: more than %u mappable resource descriptor HOBs; "

+      "refusing to map a truncated address space\n",

+      __func__,

+      (UINT32)MAX_RESOURCE_HOBS

+      ));

+    return EFI_OUT_OF_RESOURCES;

+  }

+

+  if (RegionCount == 0) {

+    DEBUG ((

+      DEBUG_ERROR,

+      "%a: no mappable resource descriptor HOB\n",

+      __func__

+      ));

+    return EFI_NOT_FOUND;

+  }

+

+  //

+  // Collect, sort and deduplicate the range boundaries.  Every interval

+  // between two adjacent boundaries is covered by a constant set of

+  // input ranges and so has one unambiguous attribute.

+  //

+  BoundaryCount = 0;

+  for (Index = 0; Index < RegionCount; Index++) {

+    mBoundaries[BoundaryCount++] = mInputRegions[Index].Start;

+    mBoundaries[BoundaryCount++] = mInputRegions[Index].End;

+  }

+

+  for (Index = 1; Index < BoundaryCount; Index++) {

+    Value = mBoundaries[Index];

+    for (Inner = Index; (Inner > 0) && (mBoundaries[Inner - 1] > Value); 
Inner--) {

+      mBoundaries[Inner] = mBoundaries[Inner - 1];

+    }

+

+    mBoundaries[Inner] = Value;

+  }

+

+  Count = 0;

+  for (Index = 0; Index < BoundaryCount; Index++) {

+    if ((Count == 0) || (mBoundaries[Count - 1] != mBoundaries[Index])) {

+      mBoundaries[Count++] = mBoundaries[Index];

+    }

+  }

+

+  BoundaryCount = Count;

+

+  //

+  // Emit one descriptor per interval, giving it the highest-priority

+  // attribute among the ranges covering it, and coalescing adjacent

+  // intervals that resolved to the same attribute.

+  //

+  Count = 0;

+  for (Index = 0; (Index + 1) < BoundaryCount; Index++) {

+    Base         = mBoundaries[Index];

+    End          = mBoundaries[Index + 1];

+    Found        = FALSE;

+    Best         = 0;

+    IntervalAttr = ARM_MEMORY_REGION_ATTRIBUTE_WRITE_BACK;

+

+    for (Inner = 0; Inner < RegionCount; Inner++) {

+      if ((mInputRegions[Inner].Start > Base) || (mInputRegions[Inner].End < 
End)) {

+        continue;

+      }

+

+      if (!Found || (mInputRegions[Inner].Priority > Best)) {

+        Best         = mInputRegions[Inner].Priority;

+        IntervalAttr = mInputRegions[Inner].Attributes;

+        Found        = TRUE;

+      }

+    }

+

+    //

+    // A gap between two input ranges stays unmapped.

+    //

+    if (!Found) {

+      continue;

+    }

+

+    if ((Count > 0) &&

+        (mVirtualMemoryTable[Count - 1].Attributes == IntervalAttr) &&

+        (mVirtualMemoryTable[Count - 1].PhysicalBase +

+         mVirtualMemoryTable[Count - 1].Length == Base))

+    {

+      mVirtualMemoryTable[Count - 1].Length += End - Base;

+      continue;

+    }

+

+    ASSERT (Count < MAX_DESCRIPTORS);

+    mVirtualMemoryTable[Count].PhysicalBase = Base;

+    mVirtualMemoryTable[Count].VirtualBase  = Base;

+    mVirtualMemoryTable[Count].Length       = End - Base;

+    mVirtualMemoryTable[Count].Attributes   = IntervalAttr;

+    Count++;

+  }

+

+  //

+  // The table is built from a sorted, deduplicated boundary list, so no

+  // two entries can overlap.  Assert it rather than assume it: a

+  // violation would mean two descriptors with different attributes

+  // cover the same page, which is the mismatched-attribute alias this

+  // flattening exists to eliminate, and whose outcome would once again

+  // depend on the order the descriptors are applied in.

+  //

+  for (Index = 1; Index < Count; Index++) {

+    ASSERT (

+      mVirtualMemoryTable[Index - 1].PhysicalBase +

+      mVirtualMemoryTable[Index - 1].Length <=

+      mVirtualMemoryTable[Index].PhysicalBase

+      );

+  }

+

+  ZeroMem (&mVirtualMemoryTable[Count], sizeof (mVirtualMemoryTable[Count]));

+

+  DEBUG ((

+    DEBUG_INFO,

+    "%a: mapping %u regions from %u resource HOBs\n",

+    __func__,

+    (UINT32)Count,

+    (UINT32)RegionCount

+    ));

+

+  return ArmConfigureMmu (

+           mVirtualMemoryTable,

+           &TranslationTableBase,

+           &TranslationTableSize

+           );

+}

+

 /**

    Transfers control to DxeCore.

 

@@ -35,8 +400,37 @@ HandOffToDxeCore (
   IN EFI_PEI_HOB_POINTERS  HobList

   )

 {

-  VOID  *BaseOfStack;

-  VOID  *TopOfStack;

+  VOID        *BaseOfStack;

+  VOID        *TopOfStack;

+  EFI_STATUS  Status;

+

+  if (ArmMmuEnabled ()) {

+    //

+    // ChainloadApp entered the payload with the MMU and caches on and

+    // its own translation tables live: those tables are pinned by an

+    // EfiBootServicesData memory-allocation HOB emitted from the

+    // launcher's boot-time reservation list, so DXE cannot allocate

+    // over them and the OS reclaims them once it has installed its

+    // own translation.  ArmPkg's CpuDxe

+    // (ArmPkg/Drivers/CpuDxe/AArch64/Mmu.c) begins with

+    // ASSERT(ArmMmuEnabled()) and manages memory attributes by editing

+    // the live tables via ArmSetMemoryAttributes(); it never needs a

+    // freshly-built hierarchy.  Adopt the incoming translation as-is.

+    //

+    DEBUG ((DEBUG_INFO, "HandOffToDxeCore: MMU already enabled, adopting live 
translation\n"));

+  } else {

+    //

+    // Raw-bootloader path: build our own page tables from the

+    // resource-descriptor HOBs.  ArmConfigureMmu() populates a table

+    // in payload-owned pages, installs it in TTBR0 and enables the

+    // MMU.

+    //

+    Status = ConfigureMmuFromHobs ();

+    if (EFI_ERROR (Status)) {

+      DEBUG ((DEBUG_ERROR, "HandOffToDxeCore: Failed to enable MMU: %r\n", 
Status));

+      CpuDeadLoop ();

+    }

+  }

 

   //

   // Allocate 128KB for the Stack

diff --git a/UefiPayloadPkg/UefiPayloadEntry/UefiPayloadEntry.c 
b/UefiPayloadPkg/UefiPayloadEntry/UefiPayloadEntry.c
index fe66243280..a58dfb4516 100644
--- a/UefiPayloadPkg/UefiPayloadEntry/UefiPayloadEntry.c
+++ b/UefiPayloadPkg/UefiPayloadEntry/UefiPayloadEntry.c
@@ -133,11 +133,16 @@ MemInfoCallbackMmio (
     // It's in DRAM and thus must be reserved

     //

     Type = EFI_RESOURCE_MEMORY_RESERVED;

+ #if defined (MDE_CPU_IA32) || defined (MDE_CPU_X64)

   } else if ((MemoryMapEntry->Base < 0x100000000ULL) && (MemoryMapEntry->Base 
>= mTopOfLowerUsableDram)) {

     //

-    // It's not in DRAM, must be MMIO

+    // On x86, reserved ranges above TOLUD and below 4 GiB are the

+    // MMIO hole.  This heuristic does not apply on AArch64 where

+    // DRAM commonly starts at or above 1 GiB and the payload FV

+    // itself is a reserved range in that window.

     //

     Type = EFI_RESOURCE_MEMORY_MAPPED_IO;

+ #endif

   } else {

     Type = EFI_RESOURCE_MEMORY_RESERVED;

   }

@@ -811,9 +816,12 @@ BuildGenericHob (
   IN UINTN  PayloadFvSize

   )

 {

-  UINT8                        PhysicalAddressBits;

-  UINTN                        Index;

+  UINT8  PhysicalAddressBits;

+  UINTN  Index;

+

+ #if defined (MDE_CPU_IA32) || defined (MDE_CPU_X64)

   EFI_RESOURCE_ATTRIBUTE_TYPE  ResourceAttribute;

+ #endif

 

   // The UEFI payload FV

   BuildMemoryAllocationHob (PayloadFvBase, PayloadFvSize, EfiBootServicesData);

@@ -847,8 +855,9 @@ BuildGenericHob (
   PhysicalAddressBits = ArchGetPhysicalAddressBits ();

   BuildCpuHob (PhysicalAddressBits, 16);

 

+ #if defined (MDE_CPU_IA32) || defined (MDE_CPU_X64)

   //

-  // Report Local APIC range, cause sbl HOB to be NULL, comment now

+  // Report Local APIC range (x86-only)

   //

   ResourceAttribute = (

                        EFI_RESOURCE_ATTRIBUTE_PRESENT |

@@ -858,6 +867,7 @@ BuildGenericHob (
                        );

   BuildResourceDescriptorHob (EFI_RESOURCE_MEMORY_MAPPED_IO, 
ResourceAttribute, 0xFEC80000, SIZE_512KB);

   BuildMemoryAllocationHob (0xFEC80000, SIZE_512KB, EfiMemoryMappedIO);

+ #endif

 }

 

 /**

@@ -1026,11 +1036,13 @@ _ModuleEntryPoint (
     HobInfo->BootMode = BOOT_ON_FLASH_UPDATE;

   }

 

+ #if defined (MDE_CPU_IA32) || defined (MDE_CPU_X64)

   //

   // Mask off all legacy 8259 interrupt sources

   //

   IoWrite8 (LEGACY_8259_MASK_REGISTER_MASTER, 0xFF);

   IoWrite8 (LEGACY_8259_MASK_REGISTER_SLAVE, 0xFF);

+ #endif

 

   Hob.HandoffInformationTable = (EFI_HOB_HANDOFF_INFO_TABLE *)GetFirstHob 
(EFI_HOB_TYPE_HANDOFF);

   HandOffToDxeCore (DxeCoreEntryPoint, Hob);

diff --git a/UefiPayloadPkg/UefiPayloadEntry/UefiPayloadEntry.inf 
b/UefiPayloadPkg/UefiPayloadEntry/UefiPayloadEntry.inf
index 7e345a9349..9590ccf077 100644
--- a/UefiPayloadPkg/UefiPayloadEntry/UefiPayloadEntry.inf
+++ b/UefiPayloadPkg/UefiPayloadEntry/UefiPayloadEntry.inf
@@ -62,6 +62,10 @@
   UefiCpuBaseArchSupportLib

   MemoryAllocationLib

 

+[LibraryClasses.AARCH64]

+  ArmMmuLib

+  ArmLib

+

 [Guids]

   gEfiMemoryTypeInformationGuid

   gEfiFirmwareFileSystem2Guid

diff --git a/UefiPayloadPkg/UefiPayloadPkg.dsc 
b/UefiPayloadPkg/UefiPayloadPkg.dsc
index ec1bee7ad2..cffb44f8bc 100644
--- a/UefiPayloadPkg/UefiPayloadPkg.dsc
+++ b/UefiPayloadPkg/UefiPayloadPkg.dsc
@@ -450,6 +450,16 @@
 !endif

 

 [LibraryClasses.AARCH64]

+  #

+  # BaseIoLibIntrinsic.inf uses plain C volatile pointer dereferences for

+  # MmioRead*/MmioWrite*, which the compiler may fuse with adjacent pointer

+  # arithmetic into post-indexed AArch64 loads/stores. Post-indexed accesses

+  # produce a stage-2 data abort with ESR_EL2.ISV=0, so KVM cannot decode the

+  # access as MMIO and returns KVM_EXIT_ARM_NISV instead of KVM_EXIT_MMIO.

+  # Use the ArmVirt variant, which emits plain register-offset accesses in

+  # hand-written assembly, matching what ArmVirtPkg does.

+  #

+  IoLib|MdePkg/Library/BaseIoLibIntrinsic/BaseIoLibIntrinsicArmVirt.inf

   ArmHvcLib|ArmPkg/Library/ArmHvcLib/ArmHvcLib.inf

   ArmLib|MdePkg/Library/ArmLib/ArmBaseLib.inf

   ArmMmuLib|UefiCpuPkg/Library/ArmMmuLib/ArmMmuBaseLib.inf

@@ -461,7 +471,19 @@
   ResetSystemLib|ArmPkg/Library/ArmPsciResetSystemLib/ArmPsciResetSystemLib.inf

   PL011UartLib|ArmPlatformPkg/Library/PL011UartLib/PL011UartLib.inf

   
PL011UartClockLib|ArmPlatformPkg/Library/PL011UartClockLib/PL011UartClockLib.inf

+!if $(CHAINLOAD_DEFAULTS) == TRUE

+  #

+  # ChainloadApp emits a gUniversalPayloadSerialPortInfoGuid HOB from

+  # the ACPI SPCR, so use the HOB-driven 16550 SerialPortLib.  The

+  # PL011 instance below is fixed to PcdSerialRegisterBase, which is

+  # only correct on QEMU virt.  DXE-phase modules use the DxeHobLib-

+  # backed instance; SEC (below) uses the PayloadEntryHobLib-backed

+  # one.

+  #

+  
SerialPortLib|UefiPayloadPkg/Library/BaseSerialPortLibHob/DxeBaseSerialPortLibHob.inf

+!else

   
SerialPortLib|ArmPlatformPkg/Library/PL011SerialPortLib/PL011SerialPortLib.inf

+!endif

 

   QemuFwCfgLib|OvmfPkg/Library/QemuFwCfgLib/QemuFwCfgMmioDxeLib.inf

   QemuFwCfgS3Lib|OvmfPkg/Library/QemuFwCfgS3Lib/BaseQemuFwCfgS3LibNull.inf

@@ -499,6 +521,11 @@
   
VirtioMmioDeviceLib|OvmfPkg/Library/VirtioMmioDeviceLib/VirtioMmioDeviceLib.inf

   VirtioLib|OvmfPkg/Library/VirtioLib/VirtioLib.inf

 

+[LibraryClasses.AARCH64.SEC]

+!if $(CHAINLOAD_DEFAULTS) == TRUE

+  
SerialPortLib|UefiPayloadPkg/Library/BaseSerialPortLibHob/BaseSerialPortLibHob.inf

+!endif

+

 [LibraryClasses.common.SEC]

   HobLib|UefiPayloadPkg/Library/PayloadEntryHobLib/HobLib.inf

   PcdLib|MdePkg/Library/BasePcdLibNull/BasePcdLibNull.inf

@@ -717,9 +744,11 @@
   gArmPlatformTokenSpaceGuid.PcdSystemMemoryUefiRegionSize|0x04000000

 

   # ARM General Interrupt Controller

+!if $(CHAINLOAD_DEFAULTS) == FALSE

   gArmTokenSpaceGuid.PcdGicDistributorBase|0x8000000

   gArmTokenSpaceGuid.PcdGicRedistributorsBase|0x80a0000

   gArmTokenSpaceGuid.PcdGicInterruptInterfaceBase|0x8080000

+!endif

 

   # Enable NX memory protection for all non-code regions, including OEM and OS

   # reserved ones, with the exception of LoaderData regions, of which OS 
loaders

@@ -888,6 +917,19 @@
 

 [PcdsDynamicExDefault.AARCH64]

 

+!if $(CHAINLOAD_DEFAULTS) == TRUE

+  #

+  # ChainloadApp hands over ACPI tables from the outer firmware.

+  # AcpiGicPcdLib parses the MADT for the GICD/GICR/GICC bases and

+  # overrides these before ArmGicDxe reads them.  QEMU-virt

+  # defaults are kept as the fallback for a bootloader that does

+  # not supply a MADT.

+  #

+  gArmTokenSpaceGuid.PcdGicDistributorBase|0x8000000

+  gArmTokenSpaceGuid.PcdGicRedistributorsBase|0x80a0000

+  gArmTokenSpaceGuid.PcdGicInterruptInterfaceBase|0x8080000

+!endif

+

   gEfiMdeModulePkgTokenSpaceGuid.PcdFlashNvStorageFtwSpareBase     | 0

   gEfiMdeModulePkgTokenSpaceGuid.PcdFlashNvStorageFtwSpareBase64   | 0

   gEfiMdeModulePkgTokenSpaceGuid.PcdFlashNvStorageVariableBase64   | 0

@@ -898,8 +940,8 @@
   # Timer IRQs

   gArmTokenSpaceGuid.PcdArmArchTimerSecIntrNum|29

   gArmTokenSpaceGuid.PcdArmArchTimerIntrNum|30

-  # Not used in QEMU platform

-  gArmTokenSpaceGuid.PcdArmArchTimerVirtIntrNum|0

+  # QEMU virt: EL1 virtual timer PPI 27

+  gArmTokenSpaceGuid.PcdArmArchTimerVirtIntrNum|27

   gArmTokenSpaceGuid.PcdArmArchTimerHypIntrNum|26

   gArmTokenSpaceGuid.PcdArmArchTimerHypVirtIntrNum|0x0

 

@@ -1272,7 +1314,14 @@
   EmbeddedPkg/RealTimeClockRuntimeDxe/RealTimeClockRuntimeDxe.inf

   EmbeddedPkg/MetronomeDxe/MetronomeDxe.inf

 

+!if $(CHAINLOAD_DEFAULTS) == TRUE

+  ArmPkg/Drivers/ArmGicDxe/ArmGicDxe.inf {

+    <LibraryClasses>

+      NULL|UefiPayloadPkg/Library/AcpiGicPcdLib/AcpiGicPcdLib.inf

+  }

+!else

   ArmPkg/Drivers/ArmGicDxe/ArmGicDxe.inf

+!endif

   ArmPkg/Drivers/TimerDxe/TimerDxe.inf

   OvmfPkg/VirtNorFlashDxe/VirtNorFlashDxe.inf {

     <LibraryClasses>

diff --git a/UefiPayloadPkg/UefiPayloadPkg.fdf 
b/UefiPayloadPkg/UefiPayloadPkg.fdf
index 054227e3ea..1a229d2b74 100644
--- a/UefiPayloadPkg/UefiPayloadPkg.fdf
+++ b/UefiPayloadPkg/UefiPayloadPkg.fdf
@@ -13,7 +13,16 @@
 DEFINE FD_BASE       = 0x00800000

 DEFINE FD_BLOCK_SIZE = 0x00001000

 

+!if $(ARCH) == "AARCH64"

+# ArmMmuLib and friends add ~2 MiB over the X64 build.

 !if $(TARGET) == "NOOPT"

+DEFINE FD_SIZE     = 0x00950000

+DEFINE NUM_BLOCKS  = 0x950

+!else

+DEFINE FD_SIZE     = 0x00900000

+DEFINE NUM_BLOCKS  = 0x900

+!endif

+!elseif $(TARGET) == "NOOPT"

 DEFINE FD_SIZE     = 0x00850000

 DEFINE NUM_BLOCKS  = 0x850

 !else

-- 
2.47.3



-=-=-=-=-=-=-=-=-=-=-=-
Groups.io Links: You receive all messages sent to this group.
View/Reply Online (#122103): https://edk2.groups.io/g/devel/message/122103
Mute This Topic: https://groups.io/mt/120797306/21656
Group Owner: [email protected]
Unsubscribe: https://edk2.groups.io/g/devel/unsub [[email protected]]
-=-=-=-=-=-=-=-=-=-=-=-


Reply via email to