arch/x86: Add support for catching null dereferences through debug regs

This commit adds support for catching null dereferences and execution
through x86's debug registers. This is particularly useful when running
32-bit coreboot as paging is not enabled to catch these through page
faults. This commit adds three new configs to support this feature:
DEBUG_HW_BREAKPOINTS, DEBUG_NULL_DEREF_BREAKPOINTS and
DEBUG_NULL_DEREF_HALT.

BUG=b:223902046
TEST=Ran on nipperkin device, verifying that HW breakpoints work as
expected.

Change-Id: I113590689046a13c2a552741bbfe7668a834354a
Signed-off-by: Robert Zieba <robertzieba@google.com>
Reviewed-on: https://review.coreboot.org/c/coreboot/+/63657
Tested-by: build bot (Jenkins) <no-reply@coreboot.org>
Reviewed-by: Raul Rangel <rrangel@chromium.org>
diff --git a/src/arch/x86/Kconfig b/src/arch/x86/Kconfig
index e9fce50..993b1e6 100644
--- a/src/arch/x86/Kconfig
+++ b/src/arch/x86/Kconfig
@@ -320,6 +320,38 @@
 	string
 	default "src/arch/x86/memlayout.ld"
 
+config DEBUG_HW_BREAKPOINTS
+	bool
+	default y
+	help
+	  Enable support for hardware data and instruction breakpoints through
+	  the x86 debug registers
+
+config DEBUG_HW_BREAKPOINTS_IN_ALL_STAGES
+	bool
+	default y
+	depends on DEBUG_HW_BREAKPOINTS && IDT_IN_EVERY_STAGE
+
+config DEBUG_NULL_DEREF_BREAKPOINTS
+	bool
+	default y
+	depends on DEBUG_HW_BREAKPOINTS
+	help
+	  Enable support for catching null dereferences and instruction execution
+
+config DEBUG_NULL_DEREF_BREAKPOINTS_IN_ALL_STAGES
+	bool
+	default y
+	depends on DEBUG_NULL_DEREF_BREAKPOINTS && DEBUG_HW_BREAKPOINTS_IN_ALL_STAGES
+
+config DEBUG_NULL_DEREF_HALT
+	bool
+	default n
+	depends on DEBUG_NULL_DEREF_BREAKPOINTS
+	help
+	  When enabled null dereferences and instruction fetches will halt execution.
+	  Otherwise an error will be printed.
+
 # Some EC need an "EC firmware pointer" (a data structure hinting the address
 # of its firmware blobs) being put at a fixed position. Its space
 # (__section__(".ecfw_ptr")) should be reserved if it lies in the range of a
diff --git a/src/arch/x86/Makefile.inc b/src/arch/x86/Makefile.inc
index 80cd9cf..e5d52ef 100644
--- a/src/arch/x86/Makefile.inc
+++ b/src/arch/x86/Makefile.inc
@@ -78,6 +78,7 @@
 ifeq ($(CONFIG_ARCH_BOOTBLOCK_X86_32)$(CONFIG_ARCH_BOOTBLOCK_X86_64),y)
 
 bootblock-y += boot.c
+bootblock-$(CONFIG_DEBUG_HW_BREAKPOINTS_IN_ALL_STAGES) += breakpoint.c
 bootblock-y += post.c
 bootblock-y += cpu_common.c
 bootblock-$(CONFIG_IDT_IN_EVERY_STAGE) += exception.c
@@ -87,6 +88,7 @@
 bootblock-y += memmove.c
 bootblock-$(CONFIG_COLLECT_TIMESTAMPS_TSC) += timestamp.c
 bootblock-$(CONFIG_X86_TOP4G_BOOTMEDIA_MAP) += mmap_boot.c
+bootblock-$(CONFIG_DEBUG_NULL_DEREF_BREAKPOINTS_IN_ALL_STAGES) += null_breakpoint.c
 bootblock-$(CONFIG_BOOTBLOCK_NORMAL) += bootblock_normal.c
 bootblock-y += gdt_init.S
 bootblock-y += id.S
@@ -122,6 +124,7 @@
 
 verstage-$(CONFIG_VBOOT_SEPARATE_VERSTAGE) += assembly_entry.S
 verstage-y += boot.c
+verstage-$(CONFIG_DEBUG_HW_BREAKPOINTS_IN_ALL_STAGES) += breakpoint.c
 verstage-y += post.c
 verstage-$(CONFIG_VBOOT_SEPARATE_VERSTAGE) += gdt_init.S
 verstage-$(CONFIG_IDT_IN_EVERY_STAGE) += exception.c
@@ -133,6 +136,7 @@
 verstage-y += memcpy.c
 verstage-y += memmove.c
 verstage-$(CONFIG_X86_TOP4G_BOOTMEDIA_MAP) += mmap_boot.c
+verstage-$(CONFIG_DEBUG_NULL_DEREF_BREAKPOINTS_IN_ALL_STAGES) += null_breakpoint.c
 # If verstage is a separate stage it means there's no need
 # for a chipset-specific car_stage_entry() so use the generic one
 # which just calls verstage().
@@ -158,6 +162,7 @@
 
 romstage-y += assembly_entry.S
 romstage-y += boot.c
+romstage-$(CONFIG_DEBUG_HW_BREAKPOINTS_IN_ALL_STAGES) += breakpoint.c
 romstage-y += post.c
 romstage-y += gdt_init.S
 romstage-y += cpu_common.c
@@ -167,6 +172,7 @@
 romstage-y += memmove.c
 romstage-y += memset.c
 romstage-$(CONFIG_X86_TOP4G_BOOTMEDIA_MAP) += mmap_boot.c
+romstage-$(CONFIG_DEBUG_NULL_DEREF_BREAKPOINTS_IN_ALL_STAGES) += null_breakpoint.c
 romstage-y += postcar_loader.c
 romstage-$(CONFIG_COLLECT_TIMESTAMPS_TSC) += timestamp.c
 romstage-$(CONFIG_HAVE_CF9_RESET) += cf9_reset.c
@@ -199,6 +205,7 @@
 postcar-generic-ccopts += -D__POSTCAR__
 
 postcar-y += boot.c
+postcar-$(CONFIG_DEBUG_HW_BREAKPOINTS_IN_ALL_STAGES) += breakpoint.c
 postcar-y += post.c
 postcar-y += gdt_init.S
 postcar-y += cpu_common.c
@@ -209,6 +216,7 @@
 postcar-y += memmove.c
 postcar-y += memset.c
 postcar-$(CONFIG_X86_TOP4G_BOOTMEDIA_MAP) += mmap_boot.c
+postcar-$(CONFIG_DEBUG_NULL_DEREF_BREAKPOINTS_IN_ALL_STAGES) += null_breakpoint.c
 postcar-y += postcar.c
 postcar-$(CONFIG_COLLECT_TIMESTAMPS_TSC) += timestamp.c
 postcar-$(CONFIG_HAVE_CF9_RESET) += cf9_reset.c
@@ -243,6 +251,7 @@
 ramstage-y += c_exit.S
 ramstage-y += cpu.c
 ramstage-y += cpu_common.c
+ramstage-$(CONFIG_DEBUG_HW_BREAKPOINTS) += breakpoint.c
 ramstage-y += ebda.c
 ramstage-y += exception.c
 ramstage-y += idt.S
@@ -252,6 +261,7 @@
 ramstage-y += memset.c
 ramstage-$(CONFIG_X86_TOP4G_BOOTMEDIA_MAP) += mmap_boot.c
 ramstage-$(CONFIG_GENERATE_MP_TABLE) += mpspec.c
+ramstage-$(CONFIG_DEBUG_NULL_DEREF_BREAKPOINTS) += null_breakpoint.c
 ramstage-$(CONFIG_GENERATE_PIRQ_TABLE) += pirq_routing.c
 ramstage-y += rdrand.c
 ramstage-$(CONFIG_GENERATE_SMBIOS_TABLES) += smbios.c
@@ -306,11 +316,13 @@
 
 endif # CONFIG_ARCH_RAMSTAGE_X86_32 / CONFIG_ARCH_RAMSTAGE_X86_64
 
+smm-$(CONFIG_DEBUG_HW_BREAKPOINTS_IN_ALL_STAGES) += breakpoint.c
 smm-$(CONFIG_IDT_IN_EVERY_STAGE) += exception.c
 smm-$(CONFIG_IDT_IN_EVERY_STAGE) += idt.S
 smm-y += memcpy.c
 smm-y += memmove.c
 smm-y += memset.c
 smm-$(CONFIG_X86_TOP4G_BOOTMEDIA_MAP) += mmap_boot.c
+smm-$(CONFIG_DEBUG_NULL_DEREF_BREAKPOINTS_IN_ALL_STAGES) += null_breakpoint.c
 
 smm-srcs += $(wildcard src/mainboard/$(MAINBOARDDIR)/smihandler.c)
diff --git a/src/arch/x86/breakpoint.c b/src/arch/x86/breakpoint.c
new file mode 100644
index 0000000..4b21e32
--- /dev/null
+++ b/src/arch/x86/breakpoint.c
@@ -0,0 +1,300 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#include <arch/registers.h>
+#include <arch/breakpoint.h>
+#include <console/console.h>
+#include <stdint.h>
+#include <types.h>
+
+#define DEBUG_REGISTER_COUNT 4
+
+/* Each enable field is 2 bits and starts at bit 0 */
+#define DEBUG_CTRL_ENABLE_SHIFT(index) (2 * (index))
+#define DEBUG_CTRL_ENABLE_MASK(index) (0x3 << DEBUG_CTRL_ENABLE_SHIFT(index))
+#define DEBUG_CTRL_ENABLE(index, enable) ((enable) << DEBUG_CTRL_ENABLE_SHIFT(index))
+
+/* Each breakpoint has a length and type, each is two bits and start at bit 16 */
+#define DEBUG_CTRL_LT_SHIFT(index) (4 * (index) + 16)
+#define DEBUG_CTRL_LT_MASK(index) (0xf << DEBUG_CTRL_LT_SHIFT(index))
+#define DEBUG_CTRL_LT(index, len, type) ((((len) << 2 | (type))) << DEBUG_CTRL_LT_SHIFT(index))
+
+/* Each field is one bit, starting at bit 0 */
+#define DEBUG_STATUS_BP_HIT_MASK(index) (1 << (index))
+#define DEBUG_STATUS_GET_BP_HIT(index, value) \
+	(((value) & DEBUG_STATUS_BP_HIT_MASK(index)) >> (index))
+
+/* Breakpoint lengths values */
+#define DEBUG_CTRL_LEN_1 0x0
+#define DEBUG_CTRL_LEN_2 0x1
+#define DEBUG_CTRL_LEN_8 0x2
+#define DEBUG_CTRL_LEN_4 0x3
+
+/* Breakpoint enable values */
+#define DEBUG_CTRL_ENABLE_LOCAL 0x1
+#define DEBUG_CTRL_ENABLE_GLOBAL 0x2
+
+/* eflags/rflags bit to continue execution after hitting an instruction breakpoint */
+#define FLAGS_RESUME (1 << 16)
+
+struct breakpoint {
+	bool allocated;
+	enum breakpoint_type type;
+	breakpoint_handler handler;
+};
+
+static struct breakpoint breakpoints[DEBUG_REGISTER_COUNT];
+
+static inline bool debug_write_addr_reg(int index, uintptr_t value)
+{
+	switch (index) {
+	case 0:
+		asm("mov %0, %%dr0" ::"r"(value));
+		break;
+
+	case 1:
+		asm("mov %0, %%dr1" ::"r"(value));
+		break;
+
+	case 2:
+		asm("mov %0, %%dr2" ::"r"(value));
+		break;
+
+	case 3:
+		asm("mov %0, %%dr3" ::"r"(value));
+		break;
+
+	default:
+		return false;
+	}
+
+	return true;
+}
+
+static inline uintptr_t debug_read_status(void)
+{
+	uintptr_t ret = 0;
+
+	asm("mov %%dr6, %0" : "=r"(ret));
+	return ret;
+}
+
+static inline void debug_write_status(uintptr_t value)
+{
+	asm("mov %0, %%dr6" ::"r"(value));
+}
+
+static inline uintptr_t debug_read_control(void)
+{
+	uintptr_t ret = 0;
+
+	asm("mov %%dr7, %0" : "=r"(ret));
+	return ret;
+}
+
+static inline void debug_write_control(uintptr_t value)
+{
+	asm("mov %0, %%dr7" ::"r"(value));
+}
+
+static enum breakpoint_result allocate_breakpoint(struct breakpoint_handle *out_handle,
+						  enum breakpoint_type type)
+{
+	for (int i = 0; i < DEBUG_REGISTER_COUNT; i++) {
+		if (breakpoints[i].allocated)
+			continue;
+
+		breakpoints[i].allocated = true;
+		breakpoints[i].handler = NULL;
+		breakpoints[i].type = type;
+		out_handle->bp = i;
+		return BREAKPOINT_RES_OK;
+	}
+
+	return BREAKPOINT_RES_NONE_AVAILABLE;
+}
+
+static enum breakpoint_result validate_handle(struct breakpoint_handle handle)
+{
+	int bp = handle.bp;
+
+	if (bp < 0 || bp >= DEBUG_REGISTER_COUNT || !breakpoints[bp].allocated)
+		return BREAKPOINT_RES_INVALID_HANDLE;
+
+	return BREAKPOINT_RES_OK;
+}
+
+enum breakpoint_result breakpoint_create_instruction(struct breakpoint_handle *out_handle,
+						     void *virt_addr)
+{
+	enum breakpoint_result res =
+		allocate_breakpoint(out_handle, BREAKPOINT_TYPE_INSTRUCTION);
+
+	if (res != BREAKPOINT_RES_OK)
+		return res;
+
+	int bp = out_handle->bp;
+	if (!debug_write_addr_reg(bp, (uintptr_t)virt_addr))
+		return BREAKPOINT_RES_INVALID_HANDLE;
+
+	uintptr_t control = debug_read_control();
+	control &= ~DEBUG_CTRL_LT_MASK(bp);
+	control |= DEBUG_CTRL_LT(bp, DEBUG_CTRL_LEN_1, BREAKPOINT_TYPE_INSTRUCTION);
+	debug_write_control(control);
+	return BREAKPOINT_RES_OK;
+}
+
+enum breakpoint_result breakpoint_create_data(struct breakpoint_handle *out_handle,
+					      void *virt_addr, size_t len, bool write_only)
+{
+	uintptr_t len_value = 0;
+
+	switch (len) {
+	case 1:
+		len_value = DEBUG_CTRL_LEN_1;
+		break;
+
+	case 2:
+		len_value = DEBUG_CTRL_LEN_2;
+		break;
+
+	case 4:
+		len_value = DEBUG_CTRL_LEN_4;
+		break;
+
+	case 8:
+		/* Only supported on 64-bit CPUs */
+		if (!ENV_X86_64)
+			return BREAKPOINT_RES_INVALID_LENGTH;
+		len_value = DEBUG_CTRL_LEN_8;
+		break;
+
+	default:
+		return BREAKPOINT_RES_INVALID_LENGTH;
+	}
+
+	enum breakpoint_type type =
+		write_only ? BREAKPOINT_TYPE_DATA_WRITE : BREAKPOINT_TYPE_DATA_RW;
+	enum breakpoint_result res = allocate_breakpoint(out_handle, type);
+	if (res != BREAKPOINT_RES_OK)
+		return res;
+
+	int bp = out_handle->bp;
+	if (!debug_write_addr_reg(bp, (uintptr_t)virt_addr))
+		return BREAKPOINT_RES_INVALID_HANDLE;
+
+	uintptr_t control = debug_read_control();
+	control &= ~DEBUG_CTRL_LT_MASK(bp);
+	control |= DEBUG_CTRL_LT(bp, len_value, type);
+	debug_write_control(control);
+	return BREAKPOINT_RES_OK;
+}
+
+enum breakpoint_result breakpoint_remove(struct breakpoint_handle handle)
+{
+	enum breakpoint_result res = validate_handle(handle);
+
+	if (res != BREAKPOINT_RES_OK)
+		return res;
+	breakpoint_enable(handle, false);
+
+	int bp = handle.bp;
+	breakpoints[bp].allocated = false;
+	return BREAKPOINT_RES_OK;
+}
+
+enum breakpoint_result breakpoint_enable(struct breakpoint_handle handle, bool enabled)
+{
+	enum breakpoint_result res = validate_handle(handle);
+
+	if (res != BREAKPOINT_RES_OK)
+		return res;
+
+	uintptr_t control = debug_read_control();
+	int bp = handle.bp;
+	control &= ~DEBUG_CTRL_ENABLE_MASK(bp);
+	if (enabled)
+		control |= DEBUG_CTRL_ENABLE(bp, DEBUG_CTRL_ENABLE_GLOBAL);
+	debug_write_control(control);
+	return BREAKPOINT_RES_OK;
+}
+
+enum breakpoint_result breakpoint_get_type(struct breakpoint_handle handle,
+					   enum breakpoint_type *type)
+{
+	enum breakpoint_result res = validate_handle(handle);
+
+	if (res != BREAKPOINT_RES_OK)
+		return res;
+
+	*type = breakpoints[handle.bp].type;
+	return BREAKPOINT_RES_OK;
+}
+
+enum breakpoint_result breakpoint_set_handler(struct breakpoint_handle handle,
+					      breakpoint_handler handler)
+{
+	enum breakpoint_result res = validate_handle(handle);
+
+	if (res != BREAKPOINT_RES_OK)
+		return res;
+
+	breakpoints[handle.bp].handler = handler;
+	return BREAKPOINT_RES_OK;
+}
+
+static enum breakpoint_result is_breakpoint_hit(struct breakpoint_handle handle, bool *out_hit)
+{
+	enum breakpoint_result res = validate_handle(handle);
+
+	if (res != BREAKPOINT_RES_OK)
+		return res;
+
+	uintptr_t status = debug_read_status();
+	*out_hit = DEBUG_STATUS_GET_BP_HIT(handle.bp, status);
+
+	return BREAKPOINT_RES_OK;
+}
+
+int breakpoint_dispatch_handler(struct eregs *info)
+{
+	bool instr_bp_hit = 0;
+
+	for (int i = 0; i < DEBUG_REGISTER_COUNT; i++) {
+		struct breakpoint_handle handle = { i };
+		bool hit = false;
+		enum breakpoint_type type;
+
+		if (is_breakpoint_hit(handle, &hit) != BREAKPOINT_RES_OK || !hit)
+			continue;
+
+		if (breakpoint_get_type(handle, &type) != BREAKPOINT_RES_OK)
+			continue;
+
+		instr_bp_hit |= type == BREAKPOINT_TYPE_INSTRUCTION;
+
+		/* Call the breakpoint handler. */
+		if (breakpoints[handle.bp].handler) {
+			int ret = breakpoints[handle.bp].handler(handle, info);
+			/* A non-zero return value indicates a fatal error. */
+			if (ret)
+				return ret;
+		}
+	}
+
+	/* Clear hit breakpoints. */
+	uintptr_t status = debug_read_status();
+	for (int i = 0; i < DEBUG_REGISTER_COUNT; i++) {
+		status &= ~DEBUG_STATUS_BP_HIT_MASK(i);
+	}
+	debug_write_status(status);
+
+	if (instr_bp_hit) {
+		/* Set the resume flag so the same breakpoint won't be hit immediately. */
+#if ENV_X86_64
+		info->rflags |= FLAGS_RESUME;
+#else
+		info->eflags |= FLAGS_RESUME;
+#endif
+	}
+
+	return 0;
+}
diff --git a/src/arch/x86/exception.c b/src/arch/x86/exception.c
index 624226e3..5f6c0fc 100644
--- a/src/arch/x86/exception.c
+++ b/src/arch/x86/exception.c
@@ -1,7 +1,9 @@
 /* SPDX-License-Identifier: GPL-2.0-only */
-
 #include <arch/cpu.h>
+#include <arch/breakpoint.h>
+#include <arch/null_breakpoint.h>
 #include <arch/exception.h>
+#include <arch/registers.h>
 #include <commonlib/helpers.h>
 #include <console/console.h>
 #include <console/streams.h>
@@ -371,7 +373,7 @@
 }
 #endif /* CONFIG_GDB_STUB */
 
-#include <arch/registers.h>
+#define DEBUG_VECTOR 1
 
 void x86_exception(struct eregs *info);
 
@@ -488,6 +490,11 @@
 	int logical_processor = 0;
 	u32 apic_id = CONFIG(SMP) ? lapicid() : 0;
 
+	if (info->vector == DEBUG_VECTOR) {
+		if (breakpoint_dispatch_handler(info) == 0)
+			return;
+	}
+
 #if ENV_RAMSTAGE
 	logical_processor = cpu_index();
 #endif
@@ -657,4 +664,6 @@
 	}
 
 	load_idt(idt, sizeof(idt));
+
+	null_breakpoint_init();
 }
diff --git a/src/arch/x86/include/arch/breakpoint.h b/src/arch/x86/include/arch/breakpoint.h
new file mode 100644
index 0000000..32e9f48
--- /dev/null
+++ b/src/arch/x86/include/arch/breakpoint.h
@@ -0,0 +1,58 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _BREAKPOINT_H_
+#define _BREAKPOINT_H_
+
+#include <arch/registers.h>
+#include <types.h>
+
+#if CONFIG(DEBUG_HW_BREAKPOINTS) && \
+	(CONFIG(DEBUG_HW_BREAKPOINTS_IN_ALL_STAGES) || ENV_RAMSTAGE)
+struct breakpoint_handle {
+	int bp;
+};
+
+typedef int (*breakpoint_handler)(struct breakpoint_handle, struct eregs *info);
+
+enum breakpoint_result {
+	BREAKPOINT_RES_OK = 0,
+	BREAKPOINT_RES_NONE_AVAILABLE = -1,
+	BREAKPOINT_RES_INVALID_HANDLE = -2,
+	BREAKPOINT_RES_INVALID_LENGTH = -3
+};
+
+enum breakpoint_type {
+	BREAKPOINT_TYPE_INSTRUCTION = 0x0,
+	BREAKPOINT_TYPE_DATA_WRITE = 0x1,
+	BREAKPOINT_TYPE_IO = 0x2,
+	BREAKPOINT_TYPE_DATA_RW = 0x3,
+};
+
+/* Creates an instruction breakpoint at the given address. */
+enum breakpoint_result breakpoint_create_instruction(struct breakpoint_handle *out_handle,
+						     void *virt_addr);
+/* Creates a data breakpoint at the given address for len bytes. */
+enum breakpoint_result breakpoint_create_data(struct breakpoint_handle *out_handle,
+					      void *virt_addr, size_t len, bool write_only);
+/* Removes a given breakpoint. */
+enum breakpoint_result breakpoint_remove(struct breakpoint_handle handle);
+/* Enables or disables a given breakpoint. */
+enum breakpoint_result breakpoint_enable(struct breakpoint_handle handle, bool enabled);
+/* Returns the type of a breakpoint. */
+enum breakpoint_result breakpoint_get_type(struct breakpoint_handle handle,
+					   enum breakpoint_type *type);
+/*
+ * Sets a handler function to be called when the breakpoint is hit. The handler should return 0
+ * to continue or any other value to halt execution as a fatal error.
+ */
+enum breakpoint_result breakpoint_set_handler(struct breakpoint_handle handle,
+					      breakpoint_handler handler);
+/* Called by x86_exception to dispatch breakpoint exceptions to the correct handler. */
+int breakpoint_dispatch_handler(struct eregs *info);
+#else
+static inline int breakpoint_dispatch_handler(struct eregs *info)
+{
+	/* Not implemented */
+	return 0;
+}
+#endif
+#endif /* _BREAKPOINT_H_ */
diff --git a/src/arch/x86/include/arch/null_breakpoint.h b/src/arch/x86/include/arch/null_breakpoint.h
new file mode 100644
index 0000000..bc86dc0
--- /dev/null
+++ b/src/arch/x86/include/arch/null_breakpoint.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _NULL_BREAKPOINT_H_
+#define _NULL_BREAKPOINT_H_
+
+#if CONFIG(DEBUG_NULL_DEREF_BREAKPOINTS) && \
+    (CONFIG(DEBUG_NULL_DEREF_BREAKPOINTS_IN_ALL_STAGES) || ENV_RAMSTAGE)
+
+/* Places data and instructions breakpoints at address zero. */
+void null_breakpoint_init(void);
+#else
+static inline void null_breakpoint_init(void)
+{
+    /* Not implemented */
+}
+#endif
+#endif /* _NULL_BREAKPOINT_H_ */
diff --git a/src/arch/x86/null_breakpoint.c b/src/arch/x86/null_breakpoint.c
new file mode 100644
index 0000000..8b21a77
--- /dev/null
+++ b/src/arch/x86/null_breakpoint.c
@@ -0,0 +1,57 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#include <arch/breakpoint.h>
+#include <arch/null_breakpoint.h>
+#include <console/console.h>
+#include <stdint.h>
+
+static struct breakpoint_handle null_deref_bp;
+static struct breakpoint_handle null_fetch_bp;
+
+static int handle_fetch_breakpoint(struct breakpoint_handle handle, struct eregs *regs)
+{
+	printk(BIOS_ERR, "Instruction fetch from address zero\n");
+	return CONFIG(DEBUG_NULL_DEREF_HALT);
+}
+
+static int handle_deref_breakpoint(struct breakpoint_handle handle, struct eregs *regs)
+{
+#if ENV_X86_64
+	printk(BIOS_ERR, "Null dereference at rip: 0x%llx \n", regs->rip);
+#else
+	printk(BIOS_ERR, "Null dereference at eip: 0x%x \n", regs->eip);
+#endif
+	return CONFIG(DEBUG_NULL_DEREF_HALT);
+}
+
+static void create_deref_breakpoint(void)
+{
+	enum breakpoint_result res =
+		breakpoint_create_data(&null_deref_bp, NULL, sizeof(uintptr_t), false);
+
+	if (res != BREAKPOINT_RES_OK) {
+		printk(BIOS_ERR, "Failed to create NULL dereference breakpoint\n");
+		return;
+	}
+
+	breakpoint_set_handler(null_deref_bp, &handle_deref_breakpoint);
+	breakpoint_enable(null_deref_bp, true);
+}
+
+static void create_instruction_breakpoint(void)
+{
+	enum breakpoint_result res = breakpoint_create_instruction(&null_fetch_bp, NULL);
+
+	if (res != BREAKPOINT_RES_OK) {
+		printk(BIOS_ERR, "Failed to create address zero instruction fetch breakpoint\n");
+		return;
+	}
+
+	breakpoint_set_handler(null_fetch_bp, &handle_fetch_breakpoint);
+	breakpoint_enable(null_fetch_bp, true);
+}
+
+void null_breakpoint_init(void)
+{
+	create_deref_breakpoint();
+	create_instruction_breakpoint();
+}