diff --git a/epicardium/l0der/elf.h b/epicardium/l0der/elf.h
new file mode 100644
index 0000000000000000000000000000000000000000..9deb88ea8faae7487bb2e943e8bdb89b81329fde
--- /dev/null
+++ b/epicardium/l0der/elf.h
@@ -0,0 +1,111 @@
+#pragma once
+
+/*
+ * 32-bit ELF structures.
+ *
+ * ref: Tool Interface Standard (TIS) Executable and Linking Format (ELF) Specification
+ *      Version 1.2, May 1995
+ *      http://refspecs.linuxbase.org/elf/elf.pdf
+ *
+ * ref: ELF for the ARM Architecture
+ *      ARM IHI 0044F, current through ABI release 2.10, 24th November 2015
+ *      http://infocenter.arm.com/help/topic/com.arm.doc.ihi0044f/IHI0044F_aaelf.pdf
+ *
+ */
+
+#include <stdint.h>
+
+typedef uint32_t Elf32_Addr;
+typedef uint16_t Elf32_Half;
+typedef uint32_t Elf32_Off;
+typedef int32_t Elf32_Sword;
+typedef uint32_t Elf32_Word;
+
+#define EI_NIDENT 16
+
+typedef struct {
+	unsigned char 	e_ident[EI_NIDENT];
+	Elf32_Half		e_type;
+	Elf32_Half		e_machine;
+	Elf32_Word		e_version;
+	Elf32_Addr		e_entry;
+	Elf32_Off		e_phoff;
+	Elf32_Off		e_shoff;
+	Elf32_Word		e_flags;
+	Elf32_Half		e_ehsize;
+	Elf32_Half		e_phentsize;
+	Elf32_Half		e_phnum;
+	Elf32_Half		e_shentsize;
+	Elf32_Half		e_shnum;
+	Elf32_Half		e_shstrndx;
+} Elf32_Ehdr;
+
+#define ET_DYN 3 // Shared object file or PIE binary
+
+#define EM_ARM 40
+
+#define EV_CURRENT 1
+
+#define ELFMAG0 0x7f
+#define ELFMAG1 'E'
+#define ELFMAG2 'L'
+#define ELFMAG3 'F'
+
+#define ELFCLASS32 1
+
+#define ELFDATA2LSB 1
+
+typedef struct {
+	Elf32_Word	sh_name;
+	Elf32_Word	sh_type;
+	Elf32_Word	sh_flags;
+	Elf32_Addr	sh_addr;
+	Elf32_Off	sh_offset;
+	Elf32_Word	sh_size;
+	Elf32_Word	sh_link;
+	Elf32_Word	sh_info;
+	Elf32_Word	sh_addralign;
+	Elf32_Word	sh_entsize;
+} Elf32_Shdr;
+
+#define SHT_RELA 4
+#define SHT_REL 9
+#define SHT_DYNSYM 11
+
+typedef struct {
+	Elf32_Word		st_name;
+	Elf32_Addr		st_value;
+	Elf32_Word		st_size;
+	unsigned char	st_info;
+	unsigned char	st_other;
+	Elf32_Half		st_shndx;
+} Elf32_Sym;
+
+#define ELF32_ST_BIND(i) ((i)>>4)
+#define ELF32_ST_TYPE(i) ((i)&0xf)
+
+#define STB_WEAK 2
+
+typedef struct {
+	Elf32_Addr	r_offset;
+	Elf32_Word	r_info;
+} Elf32_Rel;
+
+#define ELF32_R_SYM(i) ((i)>>8)
+#define ELF32_R_TYPE(i) ((unsigned char)(i))
+
+#define R_ARM_RELATIVE  0x17
+
+typedef struct {
+	Elf32_Word	p_type;
+	Elf32_Off	p_offset;
+	Elf32_Addr	p_vaddr;
+	Elf32_Addr	p_paddr;
+	Elf32_Word	p_filesz;
+	Elf32_Word	p_memsz;
+	Elf32_Word	p_flags;
+	Elf32_Word	p_align;
+} Elf32_Phdr;
+
+#define PT_LOAD 1
+#define PT_INTERP 3
diff --git a/epicardium/l0der/l0der.c b/epicardium/l0der/l0der.c
new file mode 100644
index 0000000000000000000000000000000000000000..bf02ad2dcfcb4cd18c7edc7d7510b2927afc5bb6
--- /dev/null
+++ b/epicardium/l0der/l0der.c
@@ -0,0 +1,724 @@
+#include "l0der/l0der.h"
+
+#include <alloca.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "epicardium.h"
+#include "l0der/elf.h"
+#include "modules/log.h"
+
+/*
+ * l0der is, in reality, a boneless operating-system style ELF loader.
+ *
+ * To implement it, we parse an ELF file somewhat defensively, trying to
+ * not DoS ourselves by overallocating RAM (no heap allocations, no recursion).
+ *
+ * Currently we support only relocatable, PIE binaries. Adding support for
+ * static ELFs would be trivial, however we want to keep the possibility to
+ * shuffle around memory areas in future versions of card10 (possibly giving
+ * l0dables more RAM than 256k) without having to recompile all l0dables. We
+ * are also keeping the opportunity to have separate loading schemes in the
+ * future, for instance:
+ *  - l0dables running next to pycardium, without unloading it
+ *  - multiple l0dables running next to each other (TSR-style)
+ *
+ * Thus, we use PIE l0dables to keep these possibilities open and not write down
+ * a memory map in stone.
+ */
+
+#define WEAK_SYMBOL_MAX 128
+
+struct _pie_load_info {
+	/// Populated by _load_pie
+	// Addresses within ELF file.
+	uint32_t image_start;
+	uint32_t image_limit;
+	// Highest alignment request for a segment.
+	uint32_t strictest_alignment;
+
+	/// Populated by _parse_dynamic_symbols
+	// List of weak symbols for which relocations can be ignored.
+	uint32_t weak_symbols[WEAK_SYMBOL_MAX];
+	uint32_t weak_symbol_count;
+
+	/// Populated by _get_load_addr
+	// Load address of ELF file.
+	uint32_t load_address;
+	// Addresses within memory of ELF file.
+	uint32_t image_load_start;
+	uint32_t image_load_limit;
+	// Stack top.
+	uint32_t stack_top;
+};
+
+/*
+ * Read an ELF header, check E_IDENT.
+ */
+static int _read_elf_header(int fd, Elf32_Ehdr *hdr)
+{
+	int res;
+
+	epic_file_seek(fd, 0, SEEK_SET);
+
+	if ((res = epic_file_read(fd, hdr, sizeof(Elf32_Ehdr))) !=
+	    sizeof(Elf32_Ehdr)) {
+		LOG_ERR("l0der", "_read_elf_header: read failed: %d", res);
+		return res;
+	}
+
+	if (hdr->e_ident[0] != ELFMAG0 || hdr->e_ident[1] != ELFMAG1 ||
+	    hdr->e_ident[2] != ELFMAG2 || hdr->e_ident[3] != ELFMAG3) {
+		LOG_ERR("l0der", "_read_elf_header: not an ELF file");
+		return -ENOEXEC;
+	}
+
+	if (hdr->e_ident[4] != ELFCLASS32) {
+		LOG_ERR("l0der", "_read_elf_header: not a 32-bit ELF");
+		return -ENOEXEC;
+	}
+
+	if (hdr->e_ident[5] != ELFDATA2LSB) {
+		LOG_ERR("l0der", "_read_elf_header: not a little-endian ELF");
+		return -ENOEXEC;
+	}
+
+	if (hdr->e_ident[6] != EV_CURRENT) {
+		LOG_ERR("l0der", "_read_elf_header: not a v1 ELF");
+		return -ENOEXEC;
+	}
+
+	if (hdr->e_ehsize < sizeof(Elf32_Ehdr)) {
+		LOG_ERR("l0der", "_raed_elf_header: header too small");
+		return -ENOEXEC;
+	}
+
+	return 0;
+}
+
+/*
+ * Read bytes from file at a given offset.
+ *
+ * :param int fd: file pointer to read from
+ * :param uint32_t address: address from which to read
+ * :param void *data: buffer into which to read
+ * :param size_t count: amount of bytes to read
+ * :returns: ``0`` on success or a negative value on error.  Possible errors:
+ *
+ *	- ``-EIO``: Could not read from FAT - address out of bounds of not enough bytes available.
+ */
+static int _seek_and_read(int fd, uint32_t address, void *data, size_t count)
+{
+	int res;
+
+	if ((res = epic_file_seek(fd, address, SEEK_SET)) != 0) {
+		LOG_ERR("l0der",
+			"_seek_and_read: could not seek to 0x%lx: %d",
+			address,
+			res);
+		return res;
+	}
+
+	if ((res = epic_file_read(fd, data, count)) != count) {
+		LOG_ERR("l0der", "_seek_and_read: could not read: %d", res);
+		return res;
+	}
+
+	return 0;
+}
+
+/*
+ * Read an ELF program header header.
+ */
+static int _read_program_header(int fd, uint32_t phdr_addr, Elf32_Phdr *phdr)
+{
+	return _seek_and_read(fd, phdr_addr, phdr, sizeof(Elf32_Phdr));
+}
+
+/*
+ * Read an ELF section header header.
+ */
+static int _read_section_header(int fd, uint32_t shdr_addr, Elf32_Shdr *shdr)
+{
+	return _seek_and_read(fd, shdr_addr, shdr, sizeof(Elf32_Shdr));
+}
+
+/*
+ * Check an ELF program header.
+ *
+ * This function ensures basic memory sanity of a program header / segment.
+ * It ensures that it points to a file region that is contained within the file fully.
+ */
+static int _check_program_header(int fd, int size, Elf32_Phdr *phdr)
+{
+	// Check file size/offset.
+	uint32_t file_start = phdr->p_offset;
+	uint32_t file_limit = phdr->p_offset + phdr->p_filesz;
+	if (file_limit < file_start) {
+		LOG_ERR("l0der", "_check_program_header: file size overflow");
+		return -ENOEXEC;
+	}
+	if (file_limit > size) {
+		LOG_ERR("l0der",
+			"_check_program_header: extends past end of file");
+		return -ENOEXEC;
+	}
+
+	if (phdr->p_type == PT_LOAD) {
+		// Check mem/file size.
+		if (phdr->p_filesz > phdr->p_memsz) {
+			LOG_ERR("l0der",
+				"_check_program_header: file size larger than memory size");
+			return -ENOEXEC;
+		}
+
+		uint32_t mem_start = phdr->p_vaddr;
+		uint32_t mem_limit = phdr->p_vaddr + phdr->p_memsz;
+
+		if (mem_limit < mem_start) {
+			LOG_ERR("l0der",
+				"_check_program_header: mem size overflow");
+			return -ENOEXEC;
+		}
+	}
+
+	return 0;
+}
+
+/*
+ * Check an ELF section header.
+ *
+ * This function ensures basic memory sanity of a section header.
+ * It ensures that it points to a file region that is contained within the file fully.
+ */
+static int _check_section_header(int fd, int size, Elf32_Shdr *shdr)
+{
+	// Check file size/offset.
+	uint32_t file_start = shdr->sh_offset;
+	uint32_t file_limit = shdr->sh_offset + shdr->sh_size;
+	if (file_limit < file_start) {
+		LOG_ERR("l0der", "_check_section_header: file size overflow");
+		return -ENOEXEC;
+	}
+	if (file_limit > size) {
+		LOG_ERR("l0der",
+			"_check_section_header: extends past end of file");
+		return -ENOEXEC;
+	}
+
+	return 0;
+}
+
+/*
+ * Interpreter expected in l0dable PIE binaries.
+ */
+static const char *_interpreter = "card10-l0dable";
+
+/*
+ * Check that the given INTERP program header contains the correct interpreter string.
+ */
+static int _check_interp(int fd, Elf32_Phdr *phdr)
+{
+	int res;
+	uint32_t buffer_size = strlen(_interpreter) + 1;
+	char *interp         = alloca(buffer_size);
+	memset(interp, 0, buffer_size);
+
+	if ((res = _seek_and_read(fd, phdr->p_offset, interp, buffer_size)) !=
+	    0) {
+		return res;
+	}
+
+	if (strncmp(interp, _interpreter, strlen(_interpreter)) != 0) {
+		LOG_ERR("l0der",
+			"_check_interp: invalid interpreter, want card10-l0dable");
+		return -1;
+	}
+
+	return 0;
+}
+
+/*
+ * Calculate address at which binary should be loaded.
+ *
+ * Currently this means trying to fit it into core1 RAM.
+ */
+static int _get_load_addr(struct _pie_load_info *li)
+{
+	uint32_t image_size = li->image_limit - li->image_start;
+
+	// ref: Documentation/memorymap.rst
+	uint32_t core1_mem_start = 0x20040000;
+	uint32_t core1_mem_limit = 0x20080000;
+	uint32_t core1_mem_size  = core1_mem_limit - core1_mem_start;
+
+	if (image_size > core1_mem_size) {
+		LOG_ERR("l0der",
+			"_get_load_addr: image too large (need 0x%08lx bytes, have %08lx",
+			image_size,
+			core1_mem_size);
+		return -ENOMEM;
+	}
+
+	// Place image at bottom of core1 memory range.
+	li->load_address     = core1_mem_start;
+	li->image_load_start = li->load_address + li->image_start;
+	li->image_load_limit = li->load_address + li->image_limit;
+
+	// Ensure within alignment requests.
+	if ((li->load_address % li->strictest_alignment) != 0) {
+		LOG_ERR("l0der",
+			"_get_load_addr: too strict alignment request for %ld bytes",
+			li->strictest_alignment);
+		return -ENOEXEC;
+	}
+
+	// Place stack at top of core1 memory range.
+	li->stack_top = core1_mem_limit;
+
+	// Check that there is enough stack space.
+	uint32_t stack_space = li->stack_top - li->image_load_limit;
+	if (stack_space < 8192) {
+		LOG_WARN(
+			"l0der",
+			"_get_load_addr: low stack space (%ld bytes)",
+			stack_space
+		);
+	} else if (stack_space < 256) {
+		LOG_ERR("l0der",
+			"_get_load_addr: low stack space (%ld bytes), cannot continue",
+			stack_space);
+		return -ENOMEM;
+	}
+
+	LOG_INFO(
+		"l0der",
+		"Stack at %08lx, %ld bytes available",
+		li->stack_top,
+		stack_space
+	);
+
+	return 0;
+}
+
+/*
+ * Load a program segment into memory.
+ *
+ * Segment must be a LOAD segment.
+ */
+static int _load_segment(int fd, struct _pie_load_info *li, Elf32_Phdr *phdr)
+{
+	uint32_t segment_start = li->load_address + phdr->p_vaddr;
+	uint32_t segment_limit = segment_start + phdr->p_memsz;
+
+	LOG_INFO(
+		"l0der",
+		"Segment %08lx-%08lx: 0x%lx bytes from file",
+		segment_start,
+		segment_limit,
+		phdr->p_filesz
+	);
+	memset((void *)segment_start, 0, phdr->p_memsz);
+
+	return _seek_and_read(
+		fd, phdr->p_offset, (void *)segment_start, phdr->p_filesz
+	);
+}
+
+/*
+ * Parse dynamic symbol sections.
+ */
+static int _parse_dynamic_symbols(
+	int fd, int size, struct _pie_load_info *li, Elf32_Ehdr *hdr
+) {
+	int res;
+	Elf32_Shdr shdr;
+	Elf32_Sym sym;
+
+	// Go through all dynamic symbol sections.
+	for (int i = 0; i < hdr->e_shnum; i++) {
+		uint32_t shdr_addr = hdr->e_shoff + (i * hdr->e_shentsize);
+		if ((res = _read_section_header(fd, shdr_addr, &shdr)) != 0) {
+			return res;
+		}
+
+		if (shdr.sh_type != SHT_DYNSYM) {
+			continue;
+		}
+
+		if ((res = _check_section_header(fd, size, &shdr)) != 0) {
+			return res;
+		}
+
+		if ((shdr.sh_size % sizeof(Elf32_Sym)) != 0) {
+			LOG_ERR("l0der",
+				"_parse_dynamic_symbols: SHT_DYN section with invalid size: %ld",
+				shdr.sh_size);
+			return -EIO;
+		}
+		uint32_t sym_count = shdr.sh_size / sizeof(Elf32_Sym);
+
+		// Read symbols one by one.
+		if ((res = epic_file_seek(fd, shdr.sh_offset, SEEK_SET)) != 0) {
+			LOG_ERR("l0der",
+				"_parse_dynamic_symbols: seek to first relocation (at 0x%lx) failed",
+				shdr.sh_offset);
+			return res;
+		}
+
+		for (int j = 0; j < sym_count; j++) {
+			if ((res = epic_file_read(
+				     fd, &sym, sizeof(Elf32_Sym))) !=
+			    sizeof(Elf32_Sym)) {
+				LOG_ERR("l0der",
+					"__parse_dynamic_symbols: symbol read failed: %d",
+					res);
+				return res;
+			}
+
+			uint32_t bind = ELF32_ST_BIND(sym.st_info);
+			if (bind != STB_WEAK) {
+				continue;
+			}
+
+			if (li->weak_symbol_count >= WEAK_SYMBOL_MAX) {
+				LOG_ERR("l0der",
+					"__parse_dynamic_symbols: too many weak symbols (limit: %d)",
+					WEAK_SYMBOL_MAX);
+				return -ENOMEM;
+			}
+
+			li->weak_symbols[li->weak_symbol_count++] = j;
+		}
+	}
+
+	return 0;
+}
+
+/*
+ * Apply dynamic relocations from ELF.
+ *
+ * Currently, we only support R_ARM_RELATIVE relocations. These seem to be
+ * the only one used when making 'standard' PIE binaries on RAM. However, other
+ * kinds might have to be implemented in the future.
+ */
+static int
+_run_relocations(int fd, int size, struct _pie_load_info *li, Elf32_Ehdr *hdr)
+{
+	int res;
+	Elf32_Shdr shdr;
+	Elf32_Rel rel;
+
+	// Go through all relocation sections.
+	for (int i = 0; i < hdr->e_shnum; i++) {
+		uint32_t shdr_addr = hdr->e_shoff + (i * hdr->e_shentsize);
+		if ((res = _read_section_header(fd, shdr_addr, &shdr)) != 0) {
+			return res;
+		}
+
+		// We don't support RELA (relocation with addend) sections (yet?).
+		if (shdr.sh_type == SHT_RELA) {
+			LOG_ERR("l0der",
+				"_run_relocations: found unsupported SHT_RELA section, bailing");
+			return -ENOEXEC;
+		}
+
+		if (shdr.sh_type != SHT_REL) {
+			continue;
+		}
+
+		if ((res = _check_section_header(fd, size, &shdr)) != 0) {
+			return res;
+		}
+
+		if ((shdr.sh_size % sizeof(Elf32_Rel)) != 0) {
+			LOG_ERR("l0der",
+				"_run_relocations: SHT_REL section with invalid size: %ld",
+				shdr.sh_size);
+			return -ENOEXEC;
+		}
+		uint32_t reloc_count = shdr.sh_size / sizeof(Elf32_Rel);
+
+		// Read relocations one by one.
+		if ((res = epic_file_seek(fd, shdr.sh_offset, SEEK_SET)) != 0) {
+			LOG_ERR("l0der",
+				"_run_relocations: seek to first relocation (at 0x%lx) failed",
+				shdr.sh_offset);
+			return res;
+		}
+
+		for (int j = 0; j < reloc_count; j++) {
+			if ((res = epic_file_read(
+				     fd, &rel, sizeof(Elf32_Rel))) !=
+			    sizeof(Elf32_Rel)) {
+				LOG_ERR("l0der",
+					"_run_relocations: relocation read failed: %d",
+					res);
+				return res;
+			}
+
+			uint32_t sym = ELF32_R_SYM(rel.r_info);
+			uint8_t type = ELF32_R_TYPE(rel.r_info);
+
+			// Skip relocations that are for weak symbols.
+			// (ie., do not resolve relocation - they default to a safe NULL)
+			uint8_t skip = 0;
+			if (sym != 0) {
+				for (int k = 0; k < li->weak_symbol_count;
+				     k++) {
+					if (li->weak_symbols[k] == sym) {
+						skip = 1;
+						break;
+					}
+				}
+			}
+			if (skip) {
+				continue;
+			}
+
+			switch (type) {
+			case R_ARM_RELATIVE:
+				// Relocate.
+				if ((rel.r_offset % 4) != 0) {
+					LOG_ERR("l0der",
+						"_run_relocations: R_ARM_RELATIVE address must be 4-byte aligned");
+					return -ENOEXEC;
+				}
+				volatile uint32_t *addr =
+					(uint32_t
+						 *)(rel.r_offset + li->load_address);
+				if ((uint32_t)addr < li->image_load_start ||
+				    (uint32_t)addr >= li->image_load_limit) {
+					LOG_ERR("l0der",
+						"_run_relocations: R_ARM_RELATIVE address (%08lx) is outside image boundaries",
+						(uint32_t)addr);
+					return -ENOEXEC;
+				}
+
+				*addr += li->load_address;
+				break;
+			default:
+				LOG_ERR("l0der",
+					"_run_relocations: unsupported relocation type %d",
+					type);
+				return -ENOEXEC;
+			}
+		}
+	}
+
+	return 0;
+}
+
+/*
+ * Load a l0dable PIE binary.
+ */
+static int
+_load_pie(int fd, int size, Elf32_Ehdr *hdr, struct l0dable_info *info)
+{
+	int res;
+	struct _pie_load_info li = { 0 };
+
+	// First pass over program headers: sanity check sizes, calculate image
+	// size bounds, check alignment.
+
+	li.image_start = 0xffffffff;
+	li.image_limit = 0x0;
+
+	Elf32_Phdr phdr;
+
+	int status_interp = -1;
+
+	for (int i = 0; i < hdr->e_phnum; i++) {
+		uint32_t phdr_addr = hdr->e_phoff + (i * hdr->e_phentsize);
+		if ((res = _read_program_header(fd, phdr_addr, &phdr)) != 0) {
+			return res;
+		}
+
+		if ((res = _check_program_header(fd, size, &phdr)) != 0) {
+			return res;
+		}
+
+		if (phdr.p_type == PT_INTERP) {
+			status_interp = _check_interp(fd, &phdr);
+			continue;
+		}
+
+		if (phdr.p_type == PT_LOAD) {
+			// Check alignment request.
+			if ((phdr.p_vaddr % phdr.p_align) != 0) {
+				LOG_ERR("l0der",
+					"_load_pie: phdr %d alignment too strict",
+					i);
+				return -ENOEXEC;
+			}
+			if (phdr.p_align > li.strictest_alignment) {
+				li.strictest_alignment = phdr.p_align;
+			}
+
+			uint32_t mem_start = phdr.p_vaddr;
+			uint32_t mem_limit = phdr.p_vaddr + phdr.p_memsz;
+
+			// Record memory usage.
+			if (mem_start < li.image_start) {
+				li.image_start = mem_start;
+			}
+			if (mem_limit > li.image_limit) {
+				li.image_limit = mem_limit;
+			}
+		}
+	}
+
+	if (status_interp != 0) {
+		// Expected interpreter string was not found.
+		LOG_ERR("l0der", "_load_pie: not a card10 l0dable");
+		return -ENOEXEC;
+	}
+
+	if (li.image_limit < li.image_start) {
+		// We didn't find any LOAD segment.
+		LOG_ERR("l0der", "_load_pie: no loadable segments");
+		return -ENOEXEC;
+	}
+
+	LOG_INFO(
+		"l0der",
+		"Image bounds %08lx - %08lx",
+		li.image_start,
+		li.image_limit
+	);
+
+	if ((res = _get_load_addr(&li)) != 0) {
+		return res;
+	}
+
+	LOG_INFO("l0der", "Loading at %08lx", li.load_address);
+
+	// Second pass through program headers: load all LOAD segments.
+
+	for (int i = 0; i < hdr->e_phnum; i++) {
+		uint32_t phdr_addr = hdr->e_phoff + (i * hdr->e_phentsize);
+		if ((res = _read_program_header(fd, phdr_addr, &phdr)) != 0) {
+			return res;
+		}
+
+		if (phdr.p_type != PT_LOAD) {
+			continue;
+		}
+
+		if ((res = _load_segment(fd, &li, &phdr)) != 0) {
+			return res;
+		}
+	}
+
+	// Load dynamic symbols.
+	if ((res = _parse_dynamic_symbols(fd, size, &li, hdr)) != 0) {
+		return res;
+	}
+
+	// Run relocations.
+	if ((res = _run_relocations(fd, size, &li, hdr)) != 0) {
+		return res;
+	}
+
+	uint32_t image_entrypoint = li.load_address + hdr->e_entry;
+	LOG_INFO("l0der", "Entrypoint (ISR Vector) at %08lx", image_entrypoint);
+
+	// Setup stack
+	uint32_t *isr = (uint32_t *)image_entrypoint;
+	isr[0]        = li.stack_top;
+
+	info->isr_vector = (void *)image_entrypoint;
+	return 0;
+}
+
+int l0der_load_path(const char *path, struct l0dable_info *info)
+{
+	int fd, res;
+	if ((fd = epic_file_open(path, "rb")) < 0) {
+		LOG_ERR("l0der",
+			"l0der_load_path: could not open ELF file %s: %d",
+			path,
+			fd);
+		return fd;
+	}
+
+	if ((res = epic_file_seek(fd, 0, SEEK_END)) != 0) {
+		return res;
+	}
+
+	int size = epic_file_tell(fd);
+
+	if ((res = epic_file_seek(fd, 0, SEEK_SET)) != 0) {
+		return res;
+	}
+
+	// Load ELF header and ensure it's somewhat sane.
+
+	Elf32_Ehdr hdr;
+	if ((res = _read_elf_header(fd, &hdr)) != 0) {
+		goto done;
+	}
+
+	// Sanitize segments.
+
+	uint32_t ph_start = hdr.e_phoff;
+	uint32_t ph_limit = hdr.e_phoff + (hdr.e_phnum * hdr.e_phentsize);
+	if (ph_limit < ph_start) {
+		LOG_ERR("l0der",
+			"l0der_load_path: invalid program header count/size: overflow");
+		return -ENOEXEC;
+	}
+	if (ph_limit - ph_start == 0) {
+		LOG_ERR("l0der", "l0der_load_path: no segments");
+		return -ENOEXEC;
+	}
+	if (ph_limit > size) {
+		LOG_ERR("l0der",
+			"l0der_load_path: program header table extends past end of file");
+		return -ENOEXEC;
+	}
+	if (hdr.e_phentsize < sizeof(Elf32_Phdr)) {
+		LOG_ERR("l0der",
+			"l0der_load_path: invalid program header table entry size");
+		return -ENOEXEC;
+	}
+
+	// Sanitize sections.
+
+	uint32_t sh_start = hdr.e_shoff;
+	uint32_t sh_limit = hdr.e_shoff + (hdr.e_shnum + hdr.e_shentsize);
+	if (sh_limit < sh_start) {
+		LOG_ERR("l0der",
+			"l0der_load_path: invalid section header count/size: overflow");
+		return -ENOEXEC;
+	}
+	if (sh_limit > size) {
+		LOG_ERR("l0der",
+			"l0der_load_path: section header table extends past end of file");
+		return -ENOEXEC;
+	}
+	if (hdr.e_shentsize < sizeof(Elf32_Shdr)) {
+		LOG_ERR("l0der",
+			"l0der_load_path: invalid section header table entry size");
+		return -ENOEXEC;
+	}
+
+	// Check whether it's something that we can load.
+
+	if (hdr.e_type == ET_DYN && hdr.e_machine == EM_ARM &&
+	    hdr.e_version == EV_CURRENT) {
+		LOG_INFO("l0der", "Loading PIE l0dable %s ...", path);
+		res = _load_pie(fd, size, &hdr, info);
+		goto done;
+	} else {
+		LOG_ERR("l0der",
+			"l0der_load_path: %s: not an ARM PIE, cannot load.",
+			path);
+		res = -ENOEXEC;
+		goto done;
+	}
+
+done:
+	epic_file_close(fd);
+	return res;
+}
diff --git a/epicardium/l0der/l0der.h b/epicardium/l0der/l0der.h
new file mode 100644
index 0000000000000000000000000000000000000000..d7f6b462c80694bc4304fa4baab68a206b21760a
--- /dev/null
+++ b/epicardium/l0der/l0der.h
@@ -0,0 +1,32 @@
+#pragma once
+
+/*
+ * l0der, the l0dable loader.
+ *
+ * l0der is the ELF loader responsible for retrieving a l0dable from FAT and
+ * into memory for core1 to execute.
+ *
+ * l0dables are PIE ELF binaries. They can be loaded anywhere into memory,
+ * although for now we load them at a static address (but that might change
+ * with address space evolution and/or multi-app / resident app support.
+ *
+ */
+
+struct l0dable_info {
+	/** The address of the entry ISR vector. */
+	void *isr_vector;
+};
+
+/**
+ * Load a l0dable into memory.
+ *
+ * :param const char *path: Path of l0dable on FAT filesystem.
+ * :param l0dable_info l0dable: Information about loaded l0dable.
+ * :returns: ``0`` on success or a negative value on error.  Possible errors:
+ *
+ *    - ``-ENOENT``: l0dable not present at given path.
+ *    - ``-EIO``: Read failed: l0dable corrupted or truncated.
+ *    - ``-ENOEXEC``: Corrupted/invalid l0dable.
+ *    - ``-ENOMEM``: l0dable too large to fit in RAM.
+ */
+int l0der_load_path(const char *path, struct l0dable_info *l0dable);
diff --git a/epicardium/l0der/meson.build b/epicardium/l0der/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..e727bcfa223a0a6b2fd4d9dd0bc1f1edc780d269
--- /dev/null
+++ b/epicardium/l0der/meson.build
@@ -0,0 +1,3 @@
+l0der_sources = files(
+  'l0der.c',
+)
diff --git a/epicardium/main.c b/epicardium/main.c
index 75bb9c79537a82de067d2618d1f3071348d2b032..c90d008248e0562c184701099be9ff59d7639b60 100644
--- a/epicardium/main.c
+++ b/epicardium/main.c
@@ -1,6 +1,8 @@
 #include <stdio.h>
 #include <stdlib.h>
 
+#include <ff.h>
+
 #include "max32665.h"
 #include "uart.h"
 #include "cdcacm.h"
@@ -9,6 +11,7 @@
 #include "pmic.h"
 #include "leds.h"
 #include "api/dispatcher.h"
+#include "l0der/l0der.h"
 #include "modules/modules.h"
 #include "modules/log.h"
 #include "modules/stream.h"
@@ -92,15 +95,38 @@ int main(void)
 		    NULL,
 		    tskIDLE_PRIORITY + 2,
 		    &dispatcher_task_id) != pdPASS) {
-		LOG_CRIT("startup", "Failed to create %s task!", "API Dispatcher");
+		LOG_CRIT(
+			"startup",
+			"Failed to create %s task!",
+			"API Dispatcher"
+		);
 		abort();
 	}
 
 	LOG_INFO("startup", "Initializing dispatcher ...");
 	api_dispatcher_init();
 
-	LOG_INFO("startup", "Starting core1 payload ...");
-	core1_start();
+	/*
+	 * See if there's a l0dable.elf to run. If not, run pycardium.
+	 * This is temporary until epicardium gets a l0dable API from pycardium.
+	 */
+	const char *l0dable = "l0dable.elf";
+	if (f_stat(l0dable, NULL) == FR_OK) {
+		LOG_INFO("startup", "Running %s ...", l0dable);
+		struct l0dable_info info;
+		int res = l0der_load_path(l0dable, &info);
+		if (res != 0) {
+			LOG_ERR("startup", "l0der failed: %d\n", res);
+		} else {
+			LOG_INFO(
+				"startup", "Starting %s on core1 ...", l0dable
+			);
+			core1_start(info.isr_vector);
+		}
+	} else {
+		LOG_INFO("startup", "Starting pycardium on core1 ...");
+		core1_start((void *)0x10080000);
+	}
 
 	LOG_INFO("startup", "Starting FreeRTOS ...");
 	vTaskStartScheduler();
diff --git a/epicardium/meson.build b/epicardium/meson.build
index ce584e64f6b54b6ae4f32ec19e81e52b98f42be4..c5dc701d6164c79246e9a6aa8bc761c01626b32e 100644
--- a/epicardium/meson.build
+++ b/epicardium/meson.build
@@ -66,12 +66,15 @@ freertos = static_library(
 
 subdir('modules/')
 
+subdir('l0der/')
+
 elf = executable(
   name + '.elf',
   'cdcacm.c',
   'main.c',
   'support.c',
   module_sources,
+  l0der_sources,
   dependencies: [libcard10, max32665_startup_core0, maxusb, libff13],
   link_with: [api_dispatcher_lib, freertos],
   link_whole: [max32665_startup_core0_lib, board_card10_lib, newlib_heap_lib],
diff --git a/hw-tests/dual-core/main.c b/hw-tests/dual-core/main.c
index adc1076c6b0ff468da44cdf2fc98efb7a44efeb1..ca91f9d63161412f52271c7797e172c5e7be20be 100644
--- a/hw-tests/dual-core/main.c
+++ b/hw-tests/dual-core/main.c
@@ -18,40 +18,47 @@
 
 int main(void)
 {
-    card10_init();
-    card10_diag();
-
-    Paint_DrawImage(Heart, 0, 0, 160, 80);
-    LCD_Update();
-
-    for(int i=0; i<11; i++) {
-        leds_set_dim(i, 1);
-    }
-
-    int h = 0;
-
-    // Release core1
-    core1_start();
-
-    while (1) {
-        #define NUM     15
-        for(int i=0; i<NUM; i++) {
-            if(i < 12) {
-                leds_set_hsv(i, (h + 360/NUM * i) % 360, 1., 1./8);
-            } else {
-                leds_set_hsv(i, (h + 360/NUM * i) % 360, 1., 1.);
-            }
-        }
-
-        leds_update();
-        TMR_Delay(MXC_TMR0, MSEC(10), 0);
-        h++;
-
-        // Send a txev using `sev` every once in a while to wake up core1
-        // and let it do something
-        if (h % 100 == 0) {
-            printf("core0: Triggering core1 using SEV ...\n");
-            __asm volatile("sev");
-        }
-    }
+	card10_init();
+	card10_diag();
+
+	Paint_DrawImage(Heart, 0, 0, 160, 80);
+	LCD_Update();
+
+	for (int i = 0; i < 11; i++) {
+		leds_set_dim(i, 1);
+	}
+
+	int h = 0;
+
+	// Release core1
+	core1_start((void *)0x10080000);
+
+	while (1) {
+#define NUM 15
+		for (int i = 0; i < NUM; i++) {
+			if (i < 12) {
+				leds_set_hsv(
+					i,
+					(h + 360 / NUM * i) % 360,
+					1.,
+					1. / 8
+				);
+			} else {
+				leds_set_hsv(
+					i, (h + 360 / NUM * i) % 360, 1., 1.
+				);
+			}
+		}
+
+		leds_update();
+		TMR_Delay(MXC_TMR0, MSEC(10), 0);
+		h++;
+
+		// Send a txev using `sev` every once in a while to wake up core1
+		// and let it do something
+		if (h % 100 == 0) {
+			printf("core0: Triggering core1 using SEV ...\n");
+			__asm volatile("sev");
+		}
+	}
 }
diff --git a/l0dables/blinky/README.md b/l0dables/blinky/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..9b13b5104a1d717acef4f3af5592deb7b182948b
--- /dev/null
+++ b/l0dables/blinky/README.md
@@ -0,0 +1,6 @@
+blinky
+======
+
+It works! Blink it!
+
+This is a battery-hungry l0dable that shows you how to blink some LEDs while burning CPU time and battery.
diff --git a/l0dables/blinky/main.c b/l0dables/blinky/main.c
new file mode 100644
index 0000000000000000000000000000000000000000..14237da94cf93c587aad585e19b7a83126b211a9
--- /dev/null
+++ b/l0dables/blinky/main.c
@@ -0,0 +1,71 @@
+#include "epicardium.h"
+
+#include <math.h>
+
+int levels[11]         = { 0 };
+int levels_display[11] = { 0 };
+
+// From https://learn.adafruit.com/led-tricks-gamma-correction/the-quick-fix
+const uint8_t gamma8[] = {
+	0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+	0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+	1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   2,
+	2,   2,   2,   2,   2,   2,   2,   3,   3,   3,   3,   3,   3,   3,
+	4,   4,   4,   4,   4,   5,   5,   5,   5,   6,   6,   6,   6,   7,
+	7,   7,   7,   8,   8,   8,   9,   9,   9,   10,  10,  10,  11,  11,
+	11,  12,  12,  13,  13,  13,  14,  14,  15,  15,  16,  16,  17,  17,
+	18,  18,  19,  19,  20,  20,  21,  21,  22,  22,  23,  24,  24,  25,
+	25,  26,  27,  27,  28,  29,  29,  30,  31,  32,  32,  33,  34,  35,
+	35,  36,  37,  38,  39,  39,  40,  41,  42,  43,  44,  45,  46,  47,
+	48,  49,  50,  50,  51,  52,  54,  55,  56,  57,  58,  59,  60,  61,
+	62,  63,  64,  66,  67,  68,  69,  70,  72,  73,  74,  75,  77,  78,
+	79,  81,  82,  83,  85,  86,  87,  89,  90,  92,  93,  95,  96,  98,
+	99,  101, 102, 104, 105, 107, 109, 110, 112, 114, 115, 117, 119, 120,
+	122, 124, 126, 127, 129, 131, 133, 135, 137, 138, 140, 142, 144, 146,
+	148, 150, 152, 154, 156, 158, 160, 162, 164, 167, 169, 171, 173, 175,
+	177, 180, 182, 184, 186, 189, 191, 193, 196, 198, 200, 203, 205, 208,
+	210, 213, 215, 218, 220, 223, 225, 228, 231, 233, 236, 239, 241, 244,
+	247, 249, 252, 255
+};
+
+void fade()
+{
+	for (int i = 0; i < 11; i++) {
+		int level = gamma8[levels[i]];
+		if (levels_display[i] > 0) {
+			epic_leds_set(i, level, 0, 0);
+			if (level == 0) {
+				levels_display[i] = 0;
+			}
+		}
+		if (levels[i] > 0) {
+			levels[i]--;
+		}
+	}
+}
+
+/*
+ * main() is called when l0dable is loaded and executed.
+ */
+int main(void)
+{
+	// l0dables are running on a separate, exclusive-to-l0dables core.
+	// Busy-waiting will not block the main operating system on core0 from
+	// running - but it will drain batteries.
+	for (;;) {
+		for (int i = 0; i < 11; i++) {
+			levels[i]         = 128;
+			levels_display[i] = 1;
+			for (int j = 0; j < 32; j++) {
+				fade();
+			}
+		}
+		for (int i = 9; i > 0; i--) {
+			levels[i]         = 128;
+			levels_display[i] = 1;
+			for (int j = 0; j < 32; j++) {
+				fade();
+			}
+		}
+	}
+}
diff --git a/l0dables/blinky/meson.build b/l0dables/blinky/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..9a5c199dcaf503aafb2b966256b56938ae22be4e
--- /dev/null
+++ b/l0dables/blinky/meson.build
@@ -0,0 +1,13 @@
+name = 'blinky'
+
+elf = executable(
+  name + '.elf',
+  'main.c',
+  build_by_default: true,
+  dependencies: [l0dable_startup, api_caller],
+  link_whole: [l0dable_startup_lib],
+  link_args: [
+    '-Wl,-Map=' + meson.current_build_dir() + '/' + name + '.map',
+  ],
+  pie: true,
+)
diff --git a/l0dables/lib/crt.s b/l0dables/lib/crt.s
new file mode 100644
index 0000000000000000000000000000000000000000..b811c4e6cb0c732b0de7091e596da94fce9bd1ca
--- /dev/null
+++ b/l0dables/lib/crt.s
@@ -0,0 +1,295 @@
+		/*
+		 * C Runtime for l0dable.
+		 *
+		 * Also known as a startup file.
+		 *
+		 * We provide the following to l0dables:
+		 *  - calling GCC initializers.
+		 *  - an ISR vector.
+		 *
+		 * The stack is provided by l0der.
+		 */
+
+		.syntax unified
+		.arch armv7-m
+
+		/*
+		 * ISR Vector.
+		 *
+		 * All of the following (apart from Reset_Handler, which calls main())
+		 * are backed by weak referenced symbols, which you can override just
+         * by defining them in C code.
+		 */
+		.section .data
+		.align 2
+		.globl __isr_vector
+__isr_vector:
+		.long    0                             /* Top of Stack, overriden by l0der at load time */
+		.long    Reset_Handler                 /* Reset Handler */
+		.long    NMI_Handler                   /* NMI Handler */
+		.long    HardFault_Handler             /* Hard Fault Handler */
+		.long    MemManage_Handler             /* MPU Fault Handler */
+		.long    BusFault_Handler              /* Bus Fault Handler */
+		.long    UsageFault_Handler            /* Usage Fault Handler */
+		.long    0                             /* Reserved */
+		.long    0                             /* Reserved */
+		.long    0                             /* Reserved */
+		.long    0                             /* Reserved */
+		.long    SVC_Handler                   /* SVCall Handler */
+		.long    0                             /* Reserved */ /* @TODO: Is this the Debug Montior Interrupt? */
+		.long    0                             /* Reserved */
+		.long    PendSV_Handler                /* PendSV Handler */
+		.long    SysTick_Handler               /* SysTick Handler */
+
+		/* Device-specific Interrupts */
+		.long    PF_IRQHandler                 /* 0x10  0x0040  16: Power Fail */
+		.long    WDT0_IRQHandler               /* 0x11  0x0044  17: Watchdog 0 */
+		.long    DefaultHandler                /* 0x12  0x0048  18: USB, used by core0, unoverridable */
+		.long    RTC_IRQHandler                /* 0x13  0x004C  19: RTC */
+		.long    TRNG_IRQHandler               /* 0x14  0x0050  20: True Random Number Generator */
+		.long    TMR0_IRQHandler               /* 0x15  0x0054  21: Timer 0 */
+		.long    TMR1_IRQHandler               /* 0x16  0x0058  22: Timer 1 */
+		.long    TMR2_IRQHandler               /* 0x17  0x005C  23: Timer 2 */
+		.long    TMR3_IRQHandler               /* 0x18  0x0060  24: Timer 3*/
+		.long    TMR4_IRQHandler               /* 0x19  0x0064  25: Timer 4*/
+		.long    TMR5_IRQHandler               /* 0x1A  0x0068  26: Timer 5 */
+		.long    RSV11_IRQHandler              /* 0x1B  0x006C  27: Reserved */
+		.long    RSV12_IRQHandler              /* 0x1C  0x0070  28: Reserved */
+		.long    I2C0_IRQHandler               /* 0x1D  0x0074  29: I2C0 */
+		.long    DefaultHandler                /* 0x1E  0x0078  30: UART 0, used by core0, unoverridable */
+		.long    UART1_IRQHandler              /* 0x1F  0x007C  31: UART 1 */
+		.long    SPI1_IRQHandler               /* 0x20  0x0080  32: SPI1 */
+		.long    SPI2_IRQHandler               /* 0x21  0x0084  33: SPI2 */
+		.long    RSV18_IRQHandler              /* 0x22  0x0088  34: Reserved */
+		.long    RSV19_IRQHandler              /* 0x23  0x008C  35: Reserved */
+		.long    ADC_IRQHandler                /* 0x24  0x0090  36: ADC */
+		.long    RSV21_IRQHandler              /* 0x25  0x0094  37: Reserved */
+		.long    RSV22_IRQHandler              /* 0x26  0x0098  38: Reserved */
+		.long    FLC0_IRQHandler               /* 0x27  0x009C  39: Flash Controller */
+		.long    DefaultHandler                /* 0x28  0x00A0  40: GPIO0, used by core0, unoverridable */
+		.long    DefaultHandler                /* 0x29  0x00A4  41: GPIO2, used by core0, unoverridable */
+		.long    RSV26_IRQHandler              /* 0x2A  0x00A8  42: GPIO3 */
+		.long    TPU_IRQHandler                /* 0x2B  0x00AC  43: Crypto */
+		.long    DMA0_IRQHandler               /* 0x2C  0x00B0  44: DMA0 */
+		.long    DMA1_IRQHandler               /* 0x2D  0x00B4  45: DMA1 */
+		.long    DMA2_IRQHandler               /* 0x2E  0x00B8  46: DMA2 */
+		.long    DMA3_IRQHandler               /* 0x2F  0x00BC  47: DMA3 */
+		.long    RSV32_IRQHandler              /* 0x30  0x00C0  48: Reserved */
+		.long    RSV33_IRQHandler              /* 0x31  0x00C4  49: Reserved */
+		.long    UART2_IRQHandler              /* 0x32  0x00C8  50: UART 2 */
+		.long    RSV35_IRQHandler              /* 0x33  0x00CC  51: Reserved */
+		.long    I2C1_IRQHandler               /* 0x34  0x00D0  52: I2C1 */
+		.long    RSV37_IRQHandler              /* 0x35  0x00D4  53: Reserved */
+		.long    SPIXFC_IRQHandler             /* 0x36  0x00D8  54: SPI execute in place */
+		.long    BTLE_TX_DONE_IRQHandler       /* 0x37  0x00DC  55: BTLE TX Done */
+		.long    BTLE_RX_RCVD_IRQHandler       /* 0x38  0x00E0  56: BTLE RX Recived */
+		.long    BTLE_RX_ENG_DET_IRQHandler    /* 0x39  0x00E4  57: BTLE RX Energy Dectected */
+		.long    BTLE_SFD_DET_IRQHandler       /* 0x3A  0x00E8  58: BTLE SFD Detected */
+		.long    BTLE_SFD_TO_IRQHandler        /* 0x3B  0x00EC  59: BTLE SFD Timeout*/
+		.long    BTLE_GP_EVENT_IRQHandler      /* 0x3C  0x00F0  60: BTLE Timestamp*/
+		.long    BTLE_CFO_IRQHandler           /* 0x3D  0x00F4  61: BTLE CFO Done */
+		.long    BTLE_SIG_DET_IRQHandler       /* 0x3E  0x00F8  62: BTLE Signal Detected */
+		.long    BTLE_AGC_EVENT_IRQHandler     /* 0x3F  0x00FC  63: BTLE AGC Event */
+		.long    BTLE_RFFE_SPIM_IRQHandler     /* 0x40  0x0100  64: BTLE RFFE SPIM Done */
+		.long    BTLE_TX_AES_IRQHandler        /* 0x41  0x0104  65: BTLE TX AES Done */
+		.long    BTLE_RX_AES_IRQHandler        /* 0x42  0x0108  66: BTLE RX AES Done */
+		.long    BTLE_INV_APB_ADDR_IRQHandler  /* 0x43  0x010C  67: BTLE Invalid APB Address*/
+		.long    BTLE_IQ_DATA_VALID_IRQHandler /* 0x44  0x0110  68: BTLE IQ Data Valid */
+		.long    WUT_IRQHandler                /* 0x45  0x0114  69: WUT Wakeup */
+		.long    GPIOWAKE_IRQHandler           /* 0x46  0x0118  70: GPIO Wakeup */
+		.long    RSV55_IRQHandler              /* 0x47  0x011C  71: Reserved */
+		.long    SPI0_IRQHandler               /* 0x48  0x0120  72: SPI AHB */
+		.long    WDT1_IRQHandler               /* 0x49  0x0124  73: Watchdog 1 */
+		.long    RSV58_IRQHandler              /* 0x4A  0x0128  74: Reserved */
+		.long    PT_IRQHandler                 /* 0x4B  0x012C  75: Pulse train */
+		.long    SDMA0_IRQHandler              /* 0x4C  0x0130  76: Smart DMA 0 */
+		.long    RSV61_IRQHandler              /* 0x4D  0x0134  77: Reserved */
+		.long    I2C2_IRQHandler               /* 0x4E  0x0138  78: I2C 2 */
+		.long    RSV63_IRQHandler              /* 0x4F  0x013C  79: Reserved */
+		.long    RSV64_IRQHandler              /* 0x50  0x0140  80: Reserved */
+		.long    RSV65_IRQHandler              /* 0x51  0x0144  81: Reserved */
+		.long    SDHC_IRQHandler               /* 0x52  0x0148  82: SDIO/SDHC */
+		.long    OWM_IRQHandler                /* 0x53  0x014C  83: One Wire Master */
+		.long    DMA4_IRQHandler               /* 0x54  0x0150  84: DMA4 */
+		.long    DMA5_IRQHandler               /* 0x55  0x0154  85: DMA5 */
+		.long    DMA6_IRQHandler               /* 0x56  0x0158  86: DMA6 */
+		.long    DMA7_IRQHandler               /* 0x57  0x015C  87: DMA7 */
+		.long    DMA8_IRQHandler               /* 0x58  0x0160  88: DMA8 */
+		.long    DMA9_IRQHandler               /* 0x59  0x0164  89: DMA9 */
+		.long    DMA10_IRQHandler              /* 0x5A  0x0168  90: DMA10 */
+		.long    DMA11_IRQHandler              /* 0x5B  0x016C  91: DMA11 */
+		.long    DMA12_IRQHandler              /* 0x5C  0x0170  92: DMA12 */
+		.long    DMA13_IRQHandler              /* 0x5D  0x0174  93: DMA13 */
+		.long    DMA14_IRQHandler              /* 0x5E  0x0178  94: DMA14 */
+		.long    DMA15_IRQHandler              /* 0x5F  0x017C  95: DMA15 */
+		.long    USBDMA_IRQHandler             /* 0x60  0x0180  96: USB DMA */
+		.long    WDT2_IRQHandler               /* 0x61  0x0184  97: Watchdog Timer 2 */
+		.long    ECC_IRQHandler                /* 0x62  0x0188  98: Error Correction */
+		.long    DVS_IRQHandler                /* 0x63  0x018C  99: DVS Controller */
+		.long    SIMO_IRQHandler               /* 0x64  0x0190  100: SIMO Controller */
+		.long    RPU_IRQHandler                /* 0x65  0x0194  101: RPU */ /* @TODO: Is this correct? */
+		.long    AUDIO_IRQHandler              /* 0x66  0x0198  102: Audio subsystem */
+		.long    FLC1_IRQHandler               /* 0x67  0x019C  103: Flash Control 1 */
+		.long    RSV88_IRQHandler              /* 0x68  0x01A0  104: UART 3 */
+		.long    RSV89_IRQHandler              /* 0x69  0x01A4  105: UART 4 */
+		.long    RSV90_IRQHandler              /* 0x6A  0x01A8  106: UART 5 */
+		.long    RSV91_IRQHandler              /* 0x6B  0x01AC  107: Camera IF */
+		.long    RSV92_IRQHandler              /* 0x6C  0x01B0  108: I3C */
+		.long    HTMR0_IRQHandler              /* 0x6D  0x01B4  109: HTmr */
+		.long    HTMR1_IRQHandler              /* 0x6E  0x01B8  109: HTmr */
+
+		/*
+		 * Reset_Handler, or, l0dable entrypoint.
+		 */
+		.text
+		.thumb
+		.thumb_func
+		.align 2
+Reset_Handler:
+		/* Call system initialization from l0dables/lib/hardware.c. */
+		blx SystemInit
+
+		/* Call GCC constructors. */
+		ldr r0, =__libc_init_array
+		blx r0
+
+		/* Jump to C code */
+		ldr r0, =main
+		blx r0
+
+		/*
+		 * C code done, spin forever.
+		 * TODO(q3k): let epicardium know we're done.
+		 */
+.spin:
+		bl .spin
+
+		/*
+		 * Used by __libc_init_array.
+		 */
+		.globl _init
+_init:
+		bx lr
+
+		/*
+		 * The default handler for all IRQs just spins forwever.
+		 * TODO(q3k): let epicardium know we've reached an infinite loop due to
+		 *            an exception and/or unhandled IRQ, perhaps by splitting
+		 *            DefaultHandler into multiple handlers that report different
+		 *            error conditions to epicardium (eg. unhandled IRQ, fault, ...)
+		 */
+		.thumb_func
+		.type DefaultHandler, %function
+DefaultHandler:
+		b .
+
+		.macro    def_irq_handler    handler_name
+		.weakref \handler_name, DefaultHandler
+		.endm
+
+		/*
+		 * Declare all default ISRs.
+		 */
+		def_irq_handler    NMI_Handler
+		def_irq_handler    HardFault_Handler
+		def_irq_handler    MemManage_Handler
+		def_irq_handler    BusFault_Handler
+		def_irq_handler    UsageFault_Handler
+		def_irq_handler    SVC_Handler
+		def_irq_handler    DebugMon_Handler
+		def_irq_handler    PendSV_Handler
+
+		def_irq_handler    PF_IRQHandler
+		def_irq_handler    WDT0_IRQHandler
+		def_irq_handler    RTC_IRQHandler
+		def_irq_handler    TRNG_IRQHandler
+		def_irq_handler    TMR0_IRQHandler
+		def_irq_handler    TMR1_IRQHandler
+		def_irq_handler    TMR2_IRQHandler
+		def_irq_handler    TMR3_IRQHandler
+		def_irq_handler    TMR4_IRQHandler
+		def_irq_handler    RSV11_IRQHandler
+		def_irq_handler    RSV12_IRQHandler
+		def_irq_handler    I2C0_IRQHandler
+		def_irq_handler    UART1_IRQHandler
+		def_irq_handler    SPI1_IRQHandler
+		def_irq_handler    SPI2_IRQHandler
+		def_irq_handler    RSV18_IRQHandler
+		def_irq_handler    RSV19_IRQHandler
+		def_irq_handler    ADC_IRQHandler
+		def_irq_handler    RSV21_IRQHandler
+		def_irq_handler    RSV22_IRQHandler
+		def_irq_handler    FLC0_IRQHandler
+		def_irq_handler    RSV26_IRQHandler
+		def_irq_handler    TPU_IRQHandler
+		def_irq_handler    DMA0_IRQHandler
+		def_irq_handler    DMA1_IRQHandler
+		def_irq_handler    DMA2_IRQHandler
+		def_irq_handler    DMA3_IRQHandler
+		def_irq_handler    RSV32_IRQHandler
+		def_irq_handler    RSV33_IRQHandler
+		def_irq_handler    UART2_IRQHandler
+		def_irq_handler    RSV35_IRQHandler
+		def_irq_handler    I2C1_IRQHandler
+		def_irq_handler    RSV37_IRQHandler
+		def_irq_handler    SPIXFC_IRQHandler
+		def_irq_handler    BTLE_TX_DONE_IRQHandler
+		def_irq_handler    BTLE_RX_RCVD_IRQHandler
+		def_irq_handler    BTLE_RX_ENG_DET_IRQHandler
+		def_irq_handler    BTLE_SFD_DET_IRQHandler
+		def_irq_handler    BTLE_SFD_TO_IRQHandler
+		def_irq_handler    BTLE_GP_EVENT_IRQHandler
+		def_irq_handler    BTLE_CFO_IRQHandler
+		def_irq_handler    BTLE_SIG_DET_IRQHandler
+		def_irq_handler    BTLE_AGC_EVENT_IRQHandler
+		def_irq_handler    BTLE_RFFE_SPIM_IRQHandler
+		def_irq_handler    BTLE_TX_AES_IRQHandler
+		def_irq_handler    BTLE_RX_AES_IRQHandler
+		def_irq_handler    BTLE_INV_APB_ADDR_IRQHandler
+		def_irq_handler    BTLE_IQ_DATA_VALID_IRQHandler
+		def_irq_handler    WUT_IRQHandler
+		def_irq_handler    GPIOWAKE_IRQHandler
+		def_irq_handler    RSV55_IRQHandler
+		def_irq_handler    SPI0_IRQHandler
+		def_irq_handler    WDT1_IRQHandler
+		def_irq_handler    RSV58_IRQHandler
+		def_irq_handler    PT_IRQHandler
+		def_irq_handler    SDMA0_IRQHandler
+		def_irq_handler    RSV61_IRQHandler
+		def_irq_handler    I2C2_IRQHandler
+		def_irq_handler    RSV63_IRQHandler
+		def_irq_handler    RSV64_IRQHandler
+		def_irq_handler    RSV65_IRQHandler
+		def_irq_handler    SDHC_IRQHandler
+		def_irq_handler    OWM_IRQHandler
+		def_irq_handler    DMA4_IRQHandler
+		def_irq_handler    DMA5_IRQHandler
+		def_irq_handler    DMA6_IRQHandler
+		def_irq_handler    DMA7_IRQHandler
+		def_irq_handler    DMA8_IRQHandler
+		def_irq_handler    DMA9_IRQHandler
+		def_irq_handler    DMA10_IRQHandler
+		def_irq_handler    DMA11_IRQHandler
+		def_irq_handler    DMA12_IRQHandler
+		def_irq_handler    DMA13_IRQHandler
+		def_irq_handler    DMA14_IRQHandler
+		def_irq_handler    DMA15_IRQHandler
+		def_irq_handler    USBDMA_IRQHandler
+		def_irq_handler    WDT2_IRQHandler
+		def_irq_handler    ECC_IRQHandler
+		def_irq_handler    DVS_IRQHandler
+		def_irq_handler    SIMO_IRQHandler
+		def_irq_handler    RPU_IRQHandler
+		def_irq_handler    AUDIO_IRQHandler
+		def_irq_handler    FLC1_IRQHandler
+		def_irq_handler    RSV88_IRQHandler
+		def_irq_handler    RSV89_IRQHandler
+		def_irq_handler    RSV90_IRQHandler
+		def_irq_handler    RSV91_IRQHandler
+		def_irq_handler    RSV92_IRQHandler
+		def_irq_handler    HTMR0_IRQHandler
+		def_irq_handler    HTMR1_IRQHandler
+
+		.section .cinterp
+		.asciz "card10-l0dable"
+		.byte
diff --git a/l0dables/lib/hardware.c b/l0dables/lib/hardware.c
new file mode 100644
index 0000000000000000000000000000000000000000..209ee97ad24059860b437e2284f0704f928ae02e
--- /dev/null
+++ b/l0dables/lib/hardware.c
@@ -0,0 +1,130 @@
+/*
+ * Hardware routines for l0dables.
+ *
+ * You shouldn't have to do much here. SystemInit/SystemCoreClockUpdate are
+ * called automatically before main(), and provide you with a sensible execution
+ * environment.
+ *
+ * However, if you wish, you can define your own SystemInit and take over the
+ * initialization before main() gets called.
+ */
+
+#include <stddef.h>
+#include "epicardium.h"
+
+#include "max32665.h"
+#include "mxc_sys.h"
+#include "gcr_regs.h"
+#include "icc_regs.h"
+#include "pwrseq_regs.h"
+
+uint32_t SystemCoreClock = HIRC_FREQ >> 1;
+
+void SystemCoreClockUpdate(void)
+{
+	uint32_t base_freq, div, clk_src;
+
+	// Determine the clock source and frequency
+	clk_src = (MXC_GCR->clkcn & MXC_F_GCR_CLKCN_CLKSEL);
+	switch (clk_src) {
+	case MXC_S_GCR_CLKCN_CLKSEL_HIRC:
+		base_freq = HIRC_FREQ;
+		break;
+	case MXC_S_GCR_CLKCN_CLKSEL_XTAL32M:
+		base_freq = XTAL32M_FREQ;
+		break;
+	case MXC_S_GCR_CLKCN_CLKSEL_LIRC8:
+		base_freq = LIRC8_FREQ;
+		break;
+	case MXC_S_GCR_CLKCN_CLKSEL_HIRC96:
+		base_freq = HIRC96_FREQ;
+		break;
+	case MXC_S_GCR_CLKCN_CLKSEL_HIRC8:
+		base_freq = HIRC8_FREQ;
+		break;
+	case MXC_S_GCR_CLKCN_CLKSEL_XTAL32K:
+		base_freq = XTAL32K_FREQ;
+		break;
+	default:
+		// Values 001 and 111 are reserved, and should never be encountered.
+		base_freq = HIRC_FREQ;
+		break;
+	}
+	// Clock divider is retrieved to compute system clock
+	div = (MXC_GCR->clkcn & MXC_F_GCR_CLKCN_PSC) >> MXC_F_GCR_CLKCN_PSC_POS;
+
+	SystemCoreClock = base_freq >> div;
+}
+
+__weak void SystemInit()
+{
+	// Enable FPU.
+	SCB->CPACR |= SCB_CPACR_CP10_Msk | SCB_CPACR_CP11_Msk;
+	__DSB();
+	__ISB();
+
+	// Enable ICache1 Clock
+	MXC_GCR->perckcn1 &= ~(1 << 22);
+
+	// Invalidate cache and wait until ready
+	MXC_ICC1->invalidate = 1;
+	while (!(MXC_ICC1->cache_ctrl & MXC_F_ICC_CACHE_CTRL_CACHE_RDY))
+		;
+
+	// Enable Cache
+	MXC_ICC1->cache_ctrl |= MXC_F_ICC_CACHE_CTRL_CACHE_EN;
+
+	SystemCoreClockUpdate();
+
+	// Enable API interrupt.
+	NVIC_EnableIRQ(TMR5_IRQn);
+}
+
+// newlib syscall to allow printf to work.
+long _write(int fd, const char *buf, size_t cnt)
+{
+	// Only print one line at a time.  Insert `\r` between lines so
+	// they are properly displayed on the serial console.
+	size_t i, last = 0;
+	for (i = 0; i < cnt; i++) {
+		if (buf[i] == '\n') {
+			epic_uart_write_str(&buf[last], i - last);
+			epic_uart_write_str("\r", 1);
+			last = i;
+		}
+	}
+	epic_uart_write_str(&buf[last], cnt - last);
+	return cnt;
+}
+
+// newlib syscall to allow for a heap
+extern uint32_t __heap_start;
+uint32_t _sbrk(int incr)
+{
+	static char *brk = NULL;
+	if (brk == NULL) {
+		brk = (char *)&__heap_start;
+	}
+
+	// Ensure we don't overflow the heap by checking agsinst the current stack
+	// pointer (the heap grows towards the stack, and vice-versa).
+	//
+	// This is only a last-ditch attempt at saving ourselves from memory
+	// corruption. It doesn't prevent the stack from growing into the heap
+	// first.
+
+	void *sp;
+	__asm__ __volatile__("mov %0, sp" : "=r"(sp));
+
+	// Require a 'safe margin' of 4k between the heap and stack.
+	uint32_t stack_bottom = (uint32_t)sp - 4096;
+
+	if (((uint32_t)brk + incr) > stack_bottom) {
+		errno = ENOMEM;
+		return (uint32_t)-1;
+	}
+
+	char *prev_brk = brk;
+	brk += incr;
+	return (uint32_t)prev_brk;
+}
diff --git a/l0dables/lib/l0dable.ld b/l0dables/lib/l0dable.ld
new file mode 100644
index 0000000000000000000000000000000000000000..31fbb773de00115fecb9a49d7a449a38cbfc02b6
--- /dev/null
+++ b/l0dables/lib/l0dable.ld
@@ -0,0 +1,91 @@
+ENTRY(__isr_vector);
+
+/*
+ * Segment in the output l0dable.
+ *
+ * They are mostly standard, but we define them explicitely so that we can
+ * target them in sections.
+ */
+PHDRS
+{
+    header PT_PHDR PHDRS ;
+    interp PT_INTERP ;
+    text PT_LOAD FILEHDR PHDRS ;
+    data PT_LOAD ;
+}
+
+/*
+ * ELF sections.
+ */
+SECTIONS {
+    . = SIZEOF_HEADERS;
+
+    /*
+     * Customer card10-l0dable INTERP/intepreter path.
+     *
+     * We nuke the original one (.interp) provided by gcc/ld, and inject out
+     * own. This section is populated in l0dable.ld.
+     */
+    .cinterp :
+    {
+        *(.cinterp);
+    } :interp :text
+
+    .text :
+    {
+        *(.text*)
+        *(.rodata*)
+
+        KEEP(*(.init))
+        KEEP(*(.fini))
+    } :text
+
+    .data :
+    {
+        . = ALIGN(4);
+        *(.data*)
+
+        . = ALIGN(4);
+        PROVIDE_HIDDEN (__preinit_array_start = .);
+        KEEP(*(.preinit_array))
+        PROVIDE_HIDDEN (__preinit_array_end = .);
+
+
+        . = ALIGN(4);
+        PROVIDE_HIDDEN (__init_array_start = .);
+        KEEP(*(SORT(.init_array.*)))
+        KEEP(*(.init_array))
+        PROVIDE_HIDDEN (__init_array_end = .);
+
+        . = ALIGN(4);
+        PROVIDE_HIDDEN (__fini_array_start = .);
+        KEEP(*(SORT(.fini_array.*)))
+        KEEP(*(.fini_array))
+        PROVIDE_HIDDEN (__fini_array_end = .);
+    } :data
+
+    .bss :
+    {
+        . = ALIGN(4);
+        *(.bss*)
+        *(COMMON)
+    } :data
+
+    /* Used by hardware.c as start of heap. */
+    __heap_start = .;
+
+    /* Limit based on current limitations of l0dable setup - only uses core1 RAM. */
+    ASSERT(. < 0x40000, "Exceeded available RAM")
+
+    /DISCARD/ :
+    {
+        /* Compiler version - nuke. */
+        *(.comment)
+        /* ARM attributes - nuke. */
+        *(.ARM.attributes)
+        /* Original interpreter path from gcc/ld - nuke. */
+        *(.interp)
+        /* Dynamic linking section - nuke, we're not a .so and nothing is going to link against us. */
+        *(.dynamic)
+    }
+}
diff --git a/l0dables/lib/meson.build b/l0dables/lib/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..f2f16295ae51a3d5ba0680ae14c220911dd540d1
--- /dev/null
+++ b/l0dables/lib/meson.build
@@ -0,0 +1,18 @@
+l0dable_startup_lib = static_library(
+  'l0dable-startup',
+  'crt.s',
+  'hardware.c',
+  dependencies: [api_caller],
+  pic: true,
+)
+
+l0dable_startup = declare_dependency(
+  link_args: [
+    '-nostdlib', '-n',
+    '-T', meson.current_source_dir() + 'l0dable.ld',
+  ],
+  compile_args: [
+    '-fPIE', '-pie',
+  ],
+)
+
diff --git a/l0dables/meson.build b/l0dables/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..f4be10b507d2e028d204202e14fa68bdf195a6ac
--- /dev/null
+++ b/l0dables/meson.build
@@ -0,0 +1,3 @@
+subdir('lib/')
+
+subdir('blinky/')
diff --git a/lib/card10/card10.c b/lib/card10/card10.c
index 009a69d629e9015ad3f75ba615149249474b3af7..b423704f3c969103157a3064fc63c9830ac70506 100644
--- a/lib/card10/card10.c
+++ b/lib/card10/card10.c
@@ -212,10 +212,9 @@ void card10_diag(void)
 #endif
 }
 
-void core1_start(void)
+void core1_start(void *isr)
 {
-	//MXC_GCR->gp0 = (uint32_t)(&__isr_vector_core1);
-	MXC_GCR->gp0 = 0x10080000;
+	MXC_GCR->gp0 = (uint32_t)isr;
 	MXC_GCR->perckcn1 &= ~MXC_F_GCR_PERCKCN1_CPU1;
 }
 
diff --git a/lib/card10/card10.h b/lib/card10/card10.h
index 488575a104f4c289998deaabfea14626d533de8c..853a27be90980deda9df88d1c3156a704ada648e 100644
--- a/lib/card10/card10.h
+++ b/lib/card10/card10.h
@@ -9,7 +9,7 @@ extern const gpio_cfg_t bhi_interrupt_pin;
 void card10_init(void);
 void card10_diag(void);
 
-void core1_start(void);
+void core1_start(void *isr);
 void core1_stop(void);
 
 void card10_poll(void);
diff --git a/meson.build b/meson.build
index 20b7040b216e7d4294c9a911230e71bd3b96e257..e58eb189d3fcc084bad0acc72c8e030a97771d5a 100644
--- a/meson.build
+++ b/meson.build
@@ -37,3 +37,5 @@ subdir('epicardium/')
 subdir('pycardium/')
 
 subdir('hw-tests/')
+
+subdir('l0dables/')