diff --git a/lib/riscv/asm-offsets.c b/lib/riscv/asm-offsets.c
index eb337b7547b8..7b88d16fd0e4 100644
--- a/lib/riscv/asm-offsets.c
+++ b/lib/riscv/asm-offsets.c
@@ -1,6 +1,7 @@
 // SPDX-License-Identifier: GPL-2.0-only
 #include <kbuild.h>
 #include <elf.h>
+#include <asm/ptrace.h>
 
 int main(void)
 {
@@ -13,5 +14,42 @@ int main(void)
 	OFFSET(ELF_RELA_ADDEND, elf64_rela, r_addend);
 	DEFINE(ELF_RELA_SIZE, sizeof(struct elf64_rela));
 #endif
+	OFFSET(PT_EPC, pt_regs, epc);
+	OFFSET(PT_RA, pt_regs, ra);
+	OFFSET(PT_SP, pt_regs, sp);
+	OFFSET(PT_GP, pt_regs, gp);
+	OFFSET(PT_TP, pt_regs, tp);
+	OFFSET(PT_T0, pt_regs, t0);
+	OFFSET(PT_T1, pt_regs, t1);
+	OFFSET(PT_T2, pt_regs, t2);
+	OFFSET(PT_S0, pt_regs, s0);
+	OFFSET(PT_S1, pt_regs, s1);
+	OFFSET(PT_A0, pt_regs, a0);
+	OFFSET(PT_A1, pt_regs, a1);
+	OFFSET(PT_A2, pt_regs, a2);
+	OFFSET(PT_A3, pt_regs, a3);
+	OFFSET(PT_A4, pt_regs, a4);
+	OFFSET(PT_A5, pt_regs, a5);
+	OFFSET(PT_A6, pt_regs, a6);
+	OFFSET(PT_A7, pt_regs, a7);
+	OFFSET(PT_S2, pt_regs, s2);
+	OFFSET(PT_S3, pt_regs, s3);
+	OFFSET(PT_S4, pt_regs, s4);
+	OFFSET(PT_S5, pt_regs, s5);
+	OFFSET(PT_S6, pt_regs, s6);
+	OFFSET(PT_S7, pt_regs, s7);
+	OFFSET(PT_S8, pt_regs, s8);
+	OFFSET(PT_S9, pt_regs, s9);
+	OFFSET(PT_S10, pt_regs, s10);
+	OFFSET(PT_S11, pt_regs, s11);
+	OFFSET(PT_T3, pt_regs, t3);
+	OFFSET(PT_T4, pt_regs, t4);
+	OFFSET(PT_T5, pt_regs, t5);
+	OFFSET(PT_T6, pt_regs, t6);
+	OFFSET(PT_STATUS, pt_regs, status);
+	OFFSET(PT_BADADDR, pt_regs, badaddr);
+	OFFSET(PT_CAUSE, pt_regs, cause);
+	OFFSET(PT_ORIG_A0, pt_regs, orig_a0);
+	DEFINE(PT_SIZE, sizeof(struct pt_regs));
 	return 0;
 }
diff --git a/lib/riscv/asm/bug.h b/lib/riscv/asm/bug.h
new file mode 100644
index 000000000000..a6f4136ba1b6
--- /dev/null
+++ b/lib/riscv/asm/bug.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _ASMRISCV_BUG_H_
+#define _ASMRISCV_BUG_H_
+
+#ifndef __ASSEMBLY__
+
+static inline void bug(void)
+{
+	asm volatile("ebreak");
+}
+
+#else
+
+.macro bug
+	ebreak
+.endm
+
+#endif
+
+#endif /* _ASMRISCV_BUG_H_ */
diff --git a/lib/riscv/asm/csr.h b/lib/riscv/asm/csr.h
index 356ae054bfff..eeddd1fb448a 100644
--- a/lib/riscv/asm/csr.h
+++ b/lib/riscv/asm/csr.h
@@ -3,7 +3,19 @@
 #define _ASMRISCV_CSR_H_
 #include <linux/const.h>
 
+#define CSR_SSTATUS		0x100
+#define CSR_STVEC		0x105
 #define CSR_SSCRATCH		0x140
+#define CSR_SEPC		0x141
+#define CSR_SCAUSE		0x142
+#define CSR_STVAL		0x143
+
+/* Exception cause high bit - is an interrupt if set */
+#define CAUSE_IRQ_FLAG		(_AC(1, UL) << (__riscv_xlen - 1))
+
+/* Exception causes */
+#define EXC_INST_ILLEGAL	2
+#define EXC_BREAKPOINT		3
 
 #ifndef __ASSEMBLY__
 
diff --git a/lib/riscv/asm/processor.h b/lib/riscv/asm/processor.h
new file mode 100644
index 000000000000..d8b7018c9102
--- /dev/null
+++ b/lib/riscv/asm/processor.h
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _ASMRISCV_PROCESSOR_H_
+#define _ASMRISCV_PROCESSOR_H_
+#include <asm/csr.h>
+#include <asm/ptrace.h>
+
+#define EXCEPTION_CAUSE_MAX	16
+
+typedef void (*exception_fn)(struct pt_regs *);
+
+struct thread_info {
+	int cpu;
+	unsigned long hartid;
+	exception_fn exception_handlers[EXCEPTION_CAUSE_MAX];
+};
+
+static inline struct thread_info *current_thread_info(void)
+{
+	return (struct thread_info *)csr_read(CSR_SSCRATCH);
+}
+
+void install_exception_handler(unsigned long cause, void (*handler)(struct pt_regs *));
+void do_handle_exception(struct pt_regs *regs);
+void thread_info_init(void);
+
+#endif /* _ASMRISCV_PROCESSOR_H_ */
diff --git a/lib/riscv/asm/ptrace.h b/lib/riscv/asm/ptrace.h
new file mode 100644
index 000000000000..0873a8ae749f
--- /dev/null
+++ b/lib/riscv/asm/ptrace.h
@@ -0,0 +1,46 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _ASMRISCV_PTRACE_H_
+#define _ASMRISCV_PTRACE_H_
+
+struct pt_regs {
+	unsigned long epc;
+	unsigned long ra;
+	unsigned long sp;
+	unsigned long gp;
+	unsigned long tp;
+	unsigned long t0;
+	unsigned long t1;
+	unsigned long t2;
+	unsigned long s0;
+	unsigned long s1;
+	unsigned long a0;
+	unsigned long a1;
+	unsigned long a2;
+	unsigned long a3;
+	unsigned long a4;
+	unsigned long a5;
+	unsigned long a6;
+	unsigned long a7;
+	unsigned long s2;
+	unsigned long s3;
+	unsigned long s4;
+	unsigned long s5;
+	unsigned long s6;
+	unsigned long s7;
+	unsigned long s8;
+	unsigned long s9;
+	unsigned long s10;
+	unsigned long s11;
+	unsigned long t3;
+	unsigned long t4;
+	unsigned long t5;
+	unsigned long t6;
+	/* Supervisor/Machine CSRs */
+	unsigned long status;
+	unsigned long badaddr;
+	unsigned long cause;
+	/* a0 value before the syscall */
+	unsigned long orig_a0;
+};
+
+#endif /* _ASMRISCV_PTRACE_H_ */
diff --git a/lib/riscv/asm/setup.h b/lib/riscv/asm/setup.h
index c8cfebb4f2c1..e58dd53071ae 100644
--- a/lib/riscv/asm/setup.h
+++ b/lib/riscv/asm/setup.h
@@ -2,9 +2,10 @@
 #ifndef _ASMRISCV_SETUP_H_
 #define _ASMRISCV_SETUP_H_
 #include <libcflat.h>
+#include <asm/processor.h>
 
 #define NR_CPUS 16
-extern unsigned long cpus[NR_CPUS];       /* per-cpu IDs (hartids) */
+extern struct thread_info cpus[NR_CPUS];
 extern int nr_cpus;
 int hartid_to_cpu(unsigned long hartid);
 
diff --git a/lib/riscv/processor.c b/lib/riscv/processor.c
new file mode 100644
index 000000000000..3790349aadd1
--- /dev/null
+++ b/lib/riscv/processor.c
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2023, Ventana Micro Systems Inc., Andrew Jones <ajones@ventanamicro.com>
+ */
+#include <libcflat.h>
+#include <asm/csr.h>
+#include <asm/processor.h>
+#include <asm/setup.h>
+
+extern unsigned long _text;
+
+static void show_regs(struct pt_regs *regs)
+{
+	uintptr_t text = (uintptr_t)&_text;
+	unsigned int w = __riscv_xlen / 4;
+
+	printf("Load address: %" PRIxPTR "\n", text);
+	printf("status : %.*lx\n", w, regs->status);
+	printf("cause  : %.*lx\n", w, regs->cause);
+	printf("badaddr: %.*lx\n", w, regs->badaddr);
+	printf("pc: %.*lx ra: %.*lx\n", w, regs->epc, w, regs->ra);
+	printf("sp: %.*lx gp: %.*lx tp : %.*lx\n", w, regs->sp, w, regs->gp, w, regs->tp);
+	printf("a0: %.*lx a1: %.*lx a2 : %.*lx a3 : %.*lx\n", w, regs->a0, w, regs->a1, w, regs->a2, w, regs->a3);
+	printf("a4: %.*lx a5: %.*lx a6 : %.*lx a7 : %.*lx\n", w, regs->a4, w, regs->a5, w, regs->a6, w, regs->a7);
+	printf("t0: %.*lx t1: %.*lx t2 : %.*lx t3 : %.*lx\n", w, regs->t0, w, regs->t1, w, regs->t2, w, regs->t3);
+	printf("t4: %.*lx t5: %.*lx t6 : %.*lx\n", w, regs->t4, w, regs->t5, w, regs->t6);
+	printf("s0: %.*lx s1: %.*lx s2 : %.*lx s3 : %.*lx\n", w, regs->s0, w, regs->s1, w, regs->s2, w, regs->s3);
+	printf("s4: %.*lx s5: %.*lx s6 : %.*lx s7 : %.*lx\n", w, regs->s4, w, regs->s5, w, regs->s6, w, regs->s7);
+	printf("s8: %.*lx s9: %.*lx s10: %.*lx s11: %.*lx\n", w, regs->s8, w, regs->s9, w, regs->s10, w, regs->s11);
+}
+
+void do_handle_exception(struct pt_regs *regs)
+{
+	struct thread_info *info = current_thread_info();
+
+	assert(regs->cause < EXCEPTION_CAUSE_MAX);
+	if (info->exception_handlers[regs->cause]) {
+		info->exception_handlers[regs->cause](regs);
+		return;
+	}
+
+	show_regs(regs);
+	assert(0);
+}
+
+void install_exception_handler(unsigned long cause, void (*handler)(struct pt_regs *))
+{
+	struct thread_info *info = current_thread_info();
+
+	assert(cause < EXCEPTION_CAUSE_MAX);
+	info->exception_handlers[cause] = handler;
+}
+
+void thread_info_init(void)
+{
+	unsigned long hartid = csr_read(CSR_SSCRATCH);
+	int cpu = hartid_to_cpu(hartid);
+
+	csr_write(CSR_SSCRATCH, &cpus[cpu]);
+}
diff --git a/lib/riscv/setup.c b/lib/riscv/setup.c
index 44c26b125a27..57eb4797f798 100644
--- a/lib/riscv/setup.c
+++ b/lib/riscv/setup.c
@@ -12,12 +12,13 @@
 #include <devicetree.h>
 #include <asm/csr.h>
 #include <asm/page.h>
+#include <asm/processor.h>
 #include <asm/setup.h>
 
 char *initrd;
 u32 initrd_size;
 
-unsigned long cpus[NR_CPUS] = { [0 ... NR_CPUS - 1] = ~0UL };
+struct thread_info cpus[NR_CPUS];
 int nr_cpus;
 
 int hartid_to_cpu(unsigned long hartid)
@@ -25,7 +26,7 @@ int hartid_to_cpu(unsigned long hartid)
 	int cpu;
 
 	for_each_present_cpu(cpu)
-		if (cpus[cpu] == hartid)
+		if (cpus[cpu].hartid == hartid)
 			return cpu;
 	return -1;
 }
@@ -36,7 +37,8 @@ static void cpu_set_fdt(int fdtnode __unused, u64 regval, void *info __unused)
 
 	assert_msg(cpu < NR_CPUS, "Number cpus exceeds maximum supported (%d).", NR_CPUS);
 
-	cpus[cpu] = regval;
+	cpus[cpu].cpu = cpu;
+	cpus[cpu].hartid = regval;
 	set_cpu_present(cpu, true);
 }
 
@@ -104,6 +106,7 @@ void setup(const void *fdt, phys_addr_t freemem_start)
 
 	mem_init(PAGE_ALIGN((unsigned long)freemem));
 	cpu_init();
+	thread_info_init();
 	io_init();
 
 	ret = dt_get_bootargs(&bootargs);
diff --git a/riscv/Makefile b/riscv/Makefile
index fb97e678a456..1243be125c00 100644
--- a/riscv/Makefile
+++ b/riscv/Makefile
@@ -26,6 +26,7 @@ cflatobjs += lib/alloc_phys.o
 cflatobjs += lib/devicetree.o
 cflatobjs += lib/riscv/bitops.o
 cflatobjs += lib/riscv/io.o
+cflatobjs += lib/riscv/processor.o
 cflatobjs += lib/riscv/sbi.o
 cflatobjs += lib/riscv/setup.o
 cflatobjs += lib/riscv/smp.o
diff --git a/riscv/cstart.S b/riscv/cstart.S
index 6ec2231e5812..ac7858ef398f 100644
--- a/riscv/cstart.S
+++ b/riscv/cstart.S
@@ -37,8 +37,9 @@
 .global start
 start:
 	/*
-	 * Stash the hartid in scratch and shift the dtb
-	 * address into a0
+	 * Stash the hartid in scratch and shift the dtb address into a0.
+	 * thread_info_init() will later promote scratch to point at thread
+	 * local storage.
 	 */
 	csrw	CSR_SSCRATCH, a0
 	mv	a0, a1
@@ -74,7 +75,8 @@ start:
 	zero_range a1, sp
 
 	/* set up exception handling */
-	//TODO
+	la	a1, exception_vectors
+	csrw	CSR_STVEC, a1
 
 	/* complete setup */
 	la	a1, stacktop			// a1 is the base of free memory
@@ -97,3 +99,111 @@ start:
 halt:
 1:	wfi
 	j	1b
+
+/*
+ * Save context to address in a0.
+ * For a0, sets PT_A0(a0) to the contents of PT_ORIG_A0(a0).
+ * Clobbers a1.
+ */
+.macro save_context
+	REG_S	ra, PT_RA(a0)			// x1
+	REG_S	sp, PT_SP(a0)			// x2
+	REG_S	gp, PT_GP(a0)			// x3
+	REG_S	tp, PT_TP(a0)			// x4
+	REG_S	t0, PT_T0(a0)			// x5
+	REG_S	t1, PT_T1(a0)			// x6
+	REG_S	t2, PT_T2(a0)			// x7
+	REG_S	s0, PT_S0(a0)			// x8 / fp
+	REG_S	s1, PT_S1(a0)			// x9
+	/* a0 */				// x10
+	REG_S   a1, PT_A1(a0)			// x11
+	REG_S	a2, PT_A2(a0)			// x12
+	REG_S	a3, PT_A3(a0)			// x13
+	REG_S	a4, PT_A4(a0)			// x14
+	REG_S	a5, PT_A5(a0)			// x15
+	REG_S	a6, PT_A6(a0)			// x16
+	REG_S	a7, PT_A7(a0)			// x17
+	REG_S	s2, PT_S2(a0)			// x18
+	REG_S	s3, PT_S3(a0)			// x19
+	REG_S	s4, PT_S4(a0)			// x20
+	REG_S	s5, PT_S5(a0)			// x21
+	REG_S	s6, PT_S6(a0)			// x22
+	REG_S	s7, PT_S7(a0)			// x23
+	REG_S	s8, PT_S8(a0)			// x24
+	REG_S	s9, PT_S9(a0)			// x25
+	REG_S	s10, PT_S10(a0)			// x26
+	REG_S	s11, PT_S11(a0)			// x27
+	REG_S	t3, PT_T3(a0)			// x28
+	REG_S	t4, PT_T4(a0)			// x29
+	REG_S	t5, PT_T5(a0)			// x30
+	REG_S	t6, PT_T6(a0)			// x31
+	csrr	a1, CSR_SEPC
+	REG_S	a1, PT_EPC(a0)
+	csrr	a1, CSR_SSTATUS
+	REG_S	a1, PT_STATUS(a0)
+	csrr	a1, CSR_STVAL
+	REG_S	a1, PT_BADADDR(a0)
+	csrr	a1, CSR_SCAUSE
+	REG_S	a1, PT_CAUSE(a0)
+	REG_L	a1, PT_ORIG_A0(a0)
+	REG_S	a1, PT_A0(a0)
+.endm
+
+/*
+ * Restore context from address in a0.
+ * Also restores a0.
+ */
+.macro restore_context
+	REG_L	ra, PT_RA(a0)			// x1
+	REG_L	sp, PT_SP(a0)			// x2
+	REG_L	gp, PT_GP(a0)			// x3
+	REG_L	tp, PT_TP(a0)			// x4
+	REG_L	t0, PT_T0(a0)			// x5
+	REG_L	t1, PT_T1(a0)			// x6
+	REG_L	t2, PT_T2(a0)			// x7
+	REG_L	s0, PT_S0(a0)			// x8 / fp
+	REG_L	s1, PT_S1(a0)			// x9
+	/* a0 */				// x10
+	/* a1 */				// x11
+	REG_L	a2, PT_A2(a0)			// x12
+	REG_L	a3, PT_A3(a0)			// x13
+	REG_L	a4, PT_A4(a0)			// x14
+	REG_L	a5, PT_A5(a0)			// x15
+	REG_L	a6, PT_A6(a0)			// x16
+	REG_L	a7, PT_A7(a0)			// x17
+	REG_L	s2, PT_S2(a0)			// x18
+	REG_L	s3, PT_S3(a0)			// x19
+	REG_L	s4, PT_S4(a0)			// x20
+	REG_L	s5, PT_S5(a0)			// x21
+	REG_L	s6, PT_S6(a0)			// x22
+	REG_L	s7, PT_S7(a0)			// x23
+	REG_L	s8, PT_S8(a0)			// x24
+	REG_L	s9, PT_S9(a0)			// x25
+	REG_L	s10, PT_S10(a0)			// x26
+	REG_L	s11, PT_S11(a0)			// x27
+	REG_L	t3, PT_T3(a0)			// x28
+	REG_L	t4, PT_T4(a0)			// x29
+	REG_L	t5, PT_T5(a0)			// x30
+	REG_L	t6, PT_T6(a0)			// x31
+	REG_L	a1, PT_EPC(a0)
+	csrw	CSR_SEPC, a1
+	REG_L	a1, PT_STATUS(a0)
+	csrw	CSR_SSTATUS, a1
+	REG_L	a1, PT_BADADDR(a0)
+	csrw	CSR_STVAL, a1
+	REG_L	a1, PT_CAUSE(a0)
+	csrw	CSR_SCAUSE, a1
+	REG_L	a1, PT_A1(a0)
+	REG_L	a0, PT_A0(a0)
+.endm
+
+.balign 4
+.global exception_vectors
+exception_vectors:
+	REG_S	a0, (-PT_SIZE + PT_ORIG_A0)(sp)
+	addi	a0, sp, -PT_SIZE
+	save_context
+	mv	sp, a0
+	call	do_handle_exception
+	restore_context
+	sret
diff --git a/riscv/selftest.c b/riscv/selftest.c
index d3b269cf6255..219093489b62 100644
--- a/riscv/selftest.c
+++ b/riscv/selftest.c
@@ -6,6 +6,7 @@
  */
 #include <libcflat.h>
 #include <cpumask.h>
+#include <asm/processor.h>
 #include <asm/setup.h>
 
 static void check_cpus(void)
@@ -13,7 +14,23 @@ static void check_cpus(void)
 	int cpu;
 
 	for_each_present_cpu(cpu)
-		report_info("CPU%3d: hartid=%08lx", cpu, cpus[cpu]);
+		report_info("CPU%3d: hartid=%08lx", cpu, cpus[cpu].hartid);
+}
+
+static bool exceptions_work;
+
+static void handler(struct pt_regs *regs)
+{
+	exceptions_work = true;
+	regs->epc += 2;
+}
+
+static void check_exceptions(void)
+{
+	install_exception_handler(EXC_INST_ILLEGAL, handler);
+	asm volatile(".4byte 0");
+	install_exception_handler(EXC_INST_ILLEGAL, NULL);
+	report(exceptions_work, "exceptions");
 }
 
 int main(int argc, char **argv)
@@ -45,6 +62,7 @@ int main(int argc, char **argv)
 		report_skip("environ parsing");
 	}
 
+	check_exceptions();
 	check_cpus();
 
 	return report_summary();
