diff --git a/xen/arch/ppc/Kconfig b/xen/arch/ppc/Kconfig
index a2ade2ecf4..b32dce39b8 100644
--- a/xen/arch/ppc/Kconfig
+++ b/xen/arch/ppc/Kconfig
@@ -1,5 +1,6 @@
 config PPC
 	def_bool y
+	select HAS_DEVICE_TREE
 
 config PPC64
 	def_bool y
diff --git a/xen/arch/ppc/Makefile b/xen/arch/ppc/Makefile
index 098a4dd0a9..0c0a7884a1 100644
--- a/xen/arch/ppc/Makefile
+++ b/xen/arch/ppc/Makefile
@@ -2,6 +2,7 @@ obj-$(CONFIG_PPC64) += ppc64/
 
 obj-y += boot-of.init.o
 obj-$(CONFIG_EARLY_PRINTK) += early_printk.init.o
+obj-y += opal.o
 obj-y += setup.o
 
 $(TARGET): $(TARGET)-syms
diff --git a/xen/arch/ppc/arch.mk b/xen/arch/ppc/arch.mk
index 0183b9ac6a..9f66f7a57b 100644
--- a/xen/arch/ppc/arch.mk
+++ b/xen/arch/ppc/arch.mk
@@ -10,5 +10,5 @@ CFLAGS += -mstrict-align -mcmodel=medium -mabi=elfv2 -fPIC -mno-altivec -mno-vsx
 LDFLAGS += -m elf64lppc
 
 # TODO: Drop override when more of the build is working
-override ALL_OBJS-y = arch/$(SRCARCH)/built_in.o
-override ALL_LIBS-y =
+override ALL_OBJS-y = arch/$(SRCARCH)/built_in.o common/libfdt/built_in.o lib/built_in.o
+override ALL_LIBS-y = lib/lib.a
diff --git a/xen/arch/ppc/include/asm/boot.h b/xen/arch/ppc/include/asm/boot.h
index 9b8a7c43c2..296539cd9e 100644
--- a/xen/arch/ppc/include/asm/boot.h
+++ b/xen/arch/ppc/include/asm/boot.h
@@ -4,7 +4,10 @@
 
 #include <xen/types.h>
 
-/* a collection of interfaces used during boot. */
+/*
+ * OpenFirmware boot interfaces
+ */
+
 enum {
     OF_FAILURE = -1,
     OF_SUCCESS = 0,
@@ -20,4 +23,15 @@ struct of_service {
 int enter_of(struct of_service *args, unsigned long entry);
 void boot_of_init(unsigned long vec);
 
+/*
+ * OPAL boot interfaces
+ */
+
+struct opal {
+    uint64_t base;
+    uint64_t entry;
+};
+
+void boot_opal_init(const void *fdt);
+
 #endif /* _ASM_PPC_BOOT_H */
diff --git a/xen/arch/ppc/include/asm/bug.h b/xen/arch/ppc/include/asm/bug.h
new file mode 100644
index 0000000000..e5e874b31c
--- /dev/null
+++ b/xen/arch/ppc/include/asm/bug.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+#ifndef _ASM_PPC_BUG_H
+#define _ASM_PPC_BUG_H
+
+#include <xen/stringify.h>
+
+/*
+ * Power ISA guarantees that an instruction consisting of all zeroes is
+ * illegal.
+ */
+#define BUG_OPCODE  0x00000000
+
+#define BUG_INSTR ".long " __stringify(BUG_OPCODE)
+
+#define BUG_FN_REG r0
+
+#endif /* _ASM_PPC_BUG_H */
diff --git a/xen/arch/ppc/include/asm/cache.h b/xen/arch/ppc/include/asm/cache.h
new file mode 100644
index 0000000000..8a0a6b7b17
--- /dev/null
+++ b/xen/arch/ppc/include/asm/cache.h
@@ -0,0 +1,6 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+#ifndef _ASM_PPC_CACHE_H
+#define _ASM_PPC_CACHE_H
+
+#endif /* _ASM_PPC_CACHE_H */
diff --git a/xen/arch/ppc/include/asm/processor.h b/xen/arch/ppc/include/asm/processor.h
index 838d279508..2300640787 100644
--- a/xen/arch/ppc/include/asm/processor.h
+++ b/xen/arch/ppc/include/asm/processor.h
@@ -133,6 +133,17 @@ struct cpu_user_regs
     uint32_t entry_vector;
 };
 
-#endif
+/*
+ * panic() isn't available at the moment so an infinite loop will be
+ * used temporarily.
+ * TODO: change it to panic()
+ */
+static inline void noreturn die(void)
+{
+    for ( ; ; )
+        HMT_very_low();
+}
+
+#endif /* __ASSEMBLY__ */
 
 #endif /* _ASM_PPC_PROCESSOR_H */
diff --git a/xen/arch/ppc/include/asm/string.h b/xen/arch/ppc/include/asm/string.h
new file mode 100644
index 0000000000..7a420e05e4
--- /dev/null
+++ b/xen/arch/ppc/include/asm/string.h
@@ -0,0 +1,6 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+#ifndef _ASM_PPC_STRING_H
+#define _ASM_PPC_STRING_H
+
+#endif /* _ASM_PPC_STRING_H */
diff --git a/xen/arch/ppc/include/asm/system.h b/xen/arch/ppc/include/asm/system.h
new file mode 100644
index 0000000000..94091df644
--- /dev/null
+++ b/xen/arch/ppc/include/asm/system.h
@@ -0,0 +1,6 @@
+#ifndef _ASM_SYSTEM_H_
+#define _ASM_SYSTEM_H_
+
+#define smp_wmb() __asm__ __volatile__ ( "lwsync" : : : "memory" )
+
+#endif /* _ASM_SYSTEM_H */
diff --git a/xen/arch/ppc/opal.c b/xen/arch/ppc/opal.c
new file mode 100644
index 0000000000..251de8ac23
--- /dev/null
+++ b/xen/arch/ppc/opal.c
@@ -0,0 +1,48 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#include <asm/boot.h>
+#include <asm/early_printk.h>
+#include <asm/opal-api.h>
+#include <asm/processor.h>
+#include <xen/types.h>
+#include <xen/libfdt/libfdt.h>
+#include <xen/init.h>
+#include <xen/lib.h>
+
+/* Global OPAL struct containing entrypoint and base */
+struct opal opal;
+
+void __init boot_opal_init(const void *fdt)
+{
+    int opal_node;
+    const __be64 *opal_base;
+    const __be64 *opal_entry;
+
+    if ( fdt_check_header(fdt) < 0 )
+    {
+        /*
+         * NOTE: This won't actually print since the early serial
+         * console isn't set up yet.
+         */
+        early_printk("Booted without valid FDT pointer in r3!\n");
+        die();
+    }
+
+    opal_node = fdt_path_offset(fdt, "/ibm,opal");
+    if ( opal_node < 0 )
+    {
+        early_printk("Unable to find ibm,opal node!\n");
+        die();
+    }
+
+    opal_base = fdt_getprop(fdt, opal_node, "opal-base-address", NULL);
+    opal_entry = fdt_getprop(fdt, opal_node, "opal-entry-address", NULL);
+    if ( !opal_base || !opal_entry )
+    {
+        early_printk("Failed to get opal-base-address/opal-entry-address "
+                     "property from DT!\n");
+        die();
+    }
+
+    opal.base = be64_to_cpu(*opal_base);
+    opal.entry = be64_to_cpu(*opal_entry);
+}
diff --git a/xen/arch/ppc/setup.c b/xen/arch/ppc/setup.c
index 7c623a49f5..bacd6d1acd 100644
--- a/xen/arch/ppc/setup.c
+++ b/xen/arch/ppc/setup.c
@@ -18,8 +18,13 @@ void __init noreturn start_xen(unsigned long r3, unsigned long r4,
     }
     else
     {
-        /* kexec boot: Unimplemented */
-        __builtin_trap();
+        /*
+         * kexec boot protocol
+         *
+         * TODO: This currently assumes an OPAL/PowerNV system, but it's also
+         * possible to be kexec'd on an OF system.
+         */
+        boot_opal_init((void *) r3);
     }
 
     early_printk("Hello, ppc64le!\n");
