blob: 5c195dc704c8961876905d68e1e2dc06dfcfc850 [file] [log] [blame]
/*
* Copyright (c) 2023 Intel Corporation
*
* SPDX-License-Identifier: Apache-2.0
*
*/
#include "zephyr/sys/__assert.h"
#include <zephyr/sys/util.h>
#include <zephyr/llext/elf.h>
#include <zephyr/llext/loader.h>
#include <zephyr/llext/llext.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(llext, CONFIG_LLEXT_LOG_LEVEL);
#include <string.h>
K_HEAP_DEFINE(llext_heap, CONFIG_LLEXT_HEAP_SIZE * 1024);
static const char ELF_MAGIC[] = {0x7f, 'E', 'L', 'F'};
static inline int llext_read(struct llext_loader *l, void *buf, size_t len)
{
return l->read(l, buf, len);
}
static inline int llext_seek(struct llext_loader *l, size_t pos)
{
return l->seek(l, pos);
}
static sys_slist_t _llext_list = SYS_SLIST_STATIC_INIT(&_llext_list);
sys_slist_t *llext_list(void)
{
return &_llext_list;
}
struct llext *llext_by_name(const char *name)
{
sys_slist_t *mlist = llext_list();
sys_snode_t *node = sys_slist_peek_head(mlist);
struct llext *ext = CONTAINER_OF(node, struct llext, _llext_list);
while (node != NULL) {
if (strncmp(ext->name, name, sizeof(ext->name)) == 0) {
return ext;
}
node = sys_slist_peek_next(node);
ext = CONTAINER_OF(node, struct llext, _llext_list);
}
return NULL;
}
const void * const llext_find_sym(const struct llext_symtable *sym_table, const char *sym_name)
{
if (sym_table == NULL) {
/* Buildin symbol table */
STRUCT_SECTION_FOREACH(llext_const_symbol, sym) {
if (strcmp(sym->name, sym_name) == 0) {
return sym->addr;
}
}
} else {
/* find symbols in module */
for (size_t i = 0; i < sym_table->sym_cnt; i++) {
if (strcmp(sym_table->syms[i].name, sym_name) == 0) {
return sym_table->syms[i].addr;
}
}
}
return NULL;
}
/*
* Find all relevant string and symbol tables
*/
static int llext_find_tables(struct llext_loader *ldr)
{
int ret = 0;
size_t pos = ldr->hdr.e_shoff;
elf_shdr_t shdr;
ldr->sects[LLEXT_SECT_SHSTRTAB] =
ldr->sects[LLEXT_SECT_STRTAB] =
ldr->sects[LLEXT_SECT_SYMTAB] = (elf_shdr_t){0};
/* Find symbol and string tables */
for (int i = 0, str_cnt = 0; i < ldr->hdr.e_shnum && str_cnt < 3; i++) {
ret = llext_seek(ldr, pos);
if (ret != 0) {
LOG_ERR("failed seeking to position %u\n", pos);
goto out;
}
ret = llext_read(ldr, &shdr, sizeof(elf_shdr_t));
if (ret != 0) {
LOG_ERR("failed reading section header at position %u\n", pos);
goto out;
}
pos += ldr->hdr.e_shentsize;
LOG_DBG("section %d at %x: name %d, type %d, flags %x, addr %x, size %d",
i,
ldr->hdr.e_shoff + i * ldr->hdr.e_shentsize,
shdr.sh_name,
shdr.sh_type,
shdr.sh_flags,
shdr.sh_addr,
shdr.sh_size);
switch (shdr.sh_type) {
case SHT_SYMTAB:
case SHT_DYNSYM:
LOG_DBG("symtab at %d", i);
ldr->sects[LLEXT_SECT_SYMTAB] = shdr;
ldr->sect_map[i] = LLEXT_SECT_SYMTAB;
str_cnt++;
break;
case SHT_STRTAB:
if (ldr->hdr.e_shstrndx == i) {
LOG_DBG("shstrtab at %d", i);
ldr->sects[LLEXT_SECT_SHSTRTAB] = shdr;
ldr->sect_map[i] = LLEXT_SECT_SHSTRTAB;
} else {
LOG_DBG("strtab at %d", i);
ldr->sects[LLEXT_SECT_STRTAB] = shdr;
ldr->sect_map[i] = LLEXT_SECT_STRTAB;
}
str_cnt++;
break;
default:
break;
}
}
if (!ldr->sects[LLEXT_SECT_SHSTRTAB].sh_type ||
!ldr->sects[LLEXT_SECT_STRTAB].sh_type ||
!ldr->sects[LLEXT_SECT_SYMTAB].sh_type) {
LOG_ERR("Some sections are missing or present multiple times!");
ret = -ENOENT;
}
out:
return ret;
}
/*
* Maps the section indexes and copies special section headers for easier use
*/
static int llext_map_sections(struct llext_loader *ldr)
{
int ret = 0;
size_t pos = ldr->hdr.e_shoff;
elf_shdr_t shdr;
char name[32];
for (int i = 0; i < ldr->hdr.e_shnum; i++) {
ret = llext_seek(ldr, pos);
if (ret != 0) {
goto out;
}
ret = llext_read(ldr, &shdr, sizeof(elf_shdr_t));
if (ret != 0) {
goto out;
}
pos += ldr->hdr.e_shentsize;
elf_word str_idx = shdr.sh_name;
ret = llext_seek(ldr, ldr->sects[LLEXT_SECT_SHSTRTAB].sh_offset + str_idx);
if (ret != 0) {
goto out;
}
ret = llext_read(ldr, name, sizeof(name));
if (ret != 0) {
goto out;
}
name[sizeof(name) - 1] = '\0';
LOG_DBG("section %d name %s", i, name);
enum llext_section sect_idx;
if (strncmp(name, ".text", sizeof(name)) == 0) {
sect_idx = LLEXT_SECT_TEXT;
} else if (strncmp(name, ".data", sizeof(name)) == 0) {
sect_idx = LLEXT_SECT_DATA;
} else if (strncmp(name, ".rodata", sizeof(name)) == 0) {
sect_idx = LLEXT_SECT_RODATA;
} else if (strncmp(name, ".bss", sizeof(name)) == 0) {
sect_idx = LLEXT_SECT_BSS;
} else {
LOG_DBG("Not copied section %s", name);
continue;
}
ldr->sects[sect_idx] = shdr;
ldr->sect_map[i] = sect_idx;
}
out:
return ret;
}
static inline enum llext_section llext_sect_from_mem(enum llext_mem m)
{
enum llext_section s;
switch (m) {
case LLEXT_MEM_BSS:
s = LLEXT_SECT_BSS;
break;
case LLEXT_MEM_DATA:
s = LLEXT_SECT_DATA;
break;
case LLEXT_MEM_RODATA:
s = LLEXT_SECT_RODATA;
break;
case LLEXT_MEM_TEXT:
s = LLEXT_SECT_TEXT;
break;
default:
CODE_UNREACHABLE;
}
return s;
}
static int llext_allocate_mem(struct llext_loader *ldr, struct llext *ext)
{
int ret = 0;
enum llext_section sect_idx;
for (enum llext_mem mem_idx = 0; mem_idx < LLEXT_MEM_COUNT; mem_idx++) {
sect_idx = llext_sect_from_mem(mem_idx);
if (ldr->sects[sect_idx].sh_size > 0) {
ext->mem[mem_idx] =
k_heap_aligned_alloc(&llext_heap, sizeof(uintptr_t),
ldr->sects[sect_idx].sh_size,
K_NO_WAIT);
if (ext->mem[mem_idx] == NULL) {
ret = -ENOMEM;
goto out;
}
}
}
out:
return ret;
}
static int llext_copy_sections(struct llext_loader *ldr, struct llext *ext)
{
int ret = 0;
enum llext_section sect_idx;
for (enum llext_mem mem_idx = 0; mem_idx < LLEXT_MEM_COUNT; mem_idx++) {
sect_idx = llext_sect_from_mem(mem_idx);
if (ldr->sects[sect_idx].sh_size > 0) {
ret = llext_seek(ldr, ldr->sects[sect_idx].sh_offset);
if (ret != 0) {
goto out;
}
ret = llext_read(ldr, ext->mem[mem_idx], ldr->sects[sect_idx].sh_size);
if (ret != 0) {
goto out;
}
}
}
out:
return ret;
}
static int llext_count_export_syms(struct llext_loader *ldr)
{
int ret = 0;
elf_sym_t sym;
size_t ent_size = ldr->sects[LLEXT_SECT_SYMTAB].sh_entsize;
size_t syms_size = ldr->sects[LLEXT_SECT_SYMTAB].sh_size;
size_t pos = ldr->sects[LLEXT_SECT_SYMTAB].sh_offset;
size_t sym_cnt = syms_size / sizeof(elf_sym_t);
char name[32];
LOG_DBG("symbol count %u", sym_cnt);
for (int i = 0; i < sym_cnt; i++) {
ret = llext_seek(ldr, pos);
if (ret != 0) {
goto out;
}
ret = llext_read(ldr, &sym, ent_size);
if (ret != 0) {
goto out;
}
pos += ent_size;
uint32_t stt = ELF_ST_TYPE(sym.st_info);
uint32_t stb = ELF_ST_BIND(sym.st_info);
uint32_t sect = sym.st_shndx;
ret = llext_seek(ldr, ldr->sects[LLEXT_SECT_STRTAB].sh_offset + sym.st_name);
if (ret != 0) {
goto out;
}
ret = llext_read(ldr, name, sizeof(name));
if (ret != 0) {
goto out;
}
name[sizeof(name) - 1] = '\0';
if (stt == STT_FUNC && stb == STB_GLOBAL) {
LOG_DBG("function symbol %d, name %s, type tag %d, bind %d, sect %d",
i, name, stt, stb, sect);
ldr->sym_cnt++;
} else {
LOG_DBG("unhandled symbol %d, name %s, type tag %d, bind %d, sect %d",
i, name, stt, stb, sect);
}
}
out:
return ret;
}
static inline int llext_allocate_symtab(struct llext_loader *ldr, struct llext *ext)
{
int ret = 0;
ext->sym_tab.syms = k_heap_alloc(&llext_heap, ldr->sym_cnt * sizeof(struct llext_symbol),
K_NO_WAIT);
ext->sym_tab.sym_cnt = ldr->sym_cnt;
memset(ext->sym_tab.syms, 0, ldr->sym_cnt * sizeof(struct llext_symbol));
return ret;
}
static inline int llext_copy_symbols(struct llext_loader *ldr, struct llext *ext)
{
int ret = 0;
elf_sym_t sym;
size_t ent_size = ldr->sects[LLEXT_SECT_SYMTAB].sh_entsize;
size_t syms_size = ldr->sects[LLEXT_SECT_SYMTAB].sh_size;
size_t pos = ldr->sects[LLEXT_SECT_SYMTAB].sh_offset;
size_t sym_cnt = syms_size / sizeof(elf_sym_t);
char name[32];
int i, j = 0;
for (i = 0; i < sym_cnt; i++) {
ret = llext_seek(ldr, pos);
if (ret != 0) {
goto out;
}
ret = llext_read(ldr, &sym, ent_size);
if (ret != 0) {
goto out;
}
pos += ent_size;
uint32_t stt = ELF_ST_TYPE(sym.st_info);
uint32_t stb = ELF_ST_BIND(sym.st_info);
uint32_t sect = sym.st_shndx;
ret = llext_seek(ldr, ldr->sects[LLEXT_SECT_STRTAB].sh_offset + sym.st_name);
if (ret != 0) {
goto out;
}
llext_read(ldr, name, sizeof(name));
if (ret != 0) {
goto out;
}
if (stt == STT_FUNC && stb == STB_GLOBAL && sect != SHN_UNDEF) {
ext->sym_tab.syms[j].name = k_heap_alloc(&llext_heap,
sizeof(name),
K_NO_WAIT);
strcpy(ext->sym_tab.syms[j].name, name);
ext->sym_tab.syms[j].addr =
(void *)((uintptr_t)ext->mem[ldr->sect_map[sym.st_shndx]]
+ sym.st_value);
LOG_DBG("function symbol %d name %s addr %p",
j, name, ext->sym_tab.syms[j].addr);
j++;
}
}
out:
return ret;
}
static int llext_link(struct llext_loader *ldr, struct llext *ext)
{
int ret = 0;
uintptr_t loc = 0;
elf_shdr_t shdr;
elf_rel_t rel;
elf_sym_t sym;
size_t pos = ldr->hdr.e_shoff;
elf_word rel_cnt = 0;
char name[32];
for (int i = 0; i < ldr->hdr.e_shnum - 1; i++) {
ret = llext_seek(ldr, pos);
if (ret != 0) {
goto out;
}
ret = llext_read(ldr, &shdr, sizeof(elf_shdr_t));
if (ret != 0) {
goto out;
}
pos += ldr->hdr.e_shentsize;
/* find relocation sections */
if (shdr.sh_type != SHT_REL && shdr.sh_type != SHT_RELA) {
continue;
}
rel_cnt = shdr.sh_size / sizeof(elf_rel_t);
ret = llext_seek(ldr, ldr->sects[LLEXT_SECT_SHSTRTAB].sh_offset + shdr.sh_name);
if (ret != 0) {
goto out;
}
ret = llext_read(ldr, name, sizeof(name));
if (ret != 0) {
goto out;
}
if (strncmp(name, ".rel.text", sizeof(name)) == 0 ||
strncmp(name, ".rela.text", sizeof(name)) == 0) {
loc = (uintptr_t)ext->mem[LLEXT_MEM_TEXT];
} else if (strncmp(name, ".rel.bss", sizeof(name)) == 0) {
loc = (uintptr_t)ext->mem[LLEXT_MEM_BSS];
} else if (strncmp(name, ".rel.rodata", sizeof(name)) == 0) {
loc = (uintptr_t)ext->mem[LLEXT_MEM_RODATA];
} else if (strncmp(name, ".rel.data", sizeof(name)) == 0) {
loc = (uintptr_t)ext->mem[LLEXT_MEM_DATA];
}
LOG_DBG("relocation section %s (%d) linked to section %d has %d relocations",
name, i, shdr.sh_link, rel_cnt);
for (int j = 0; j < rel_cnt; j++) {
/* get each relocation entry */
ret = llext_seek(ldr, shdr.sh_offset + j * sizeof(elf_rel_t));
if (ret != 0) {
goto out;
}
ret = llext_read(ldr, &rel, sizeof(elf_rel_t));
if (ret != 0) {
goto out;
}
/* get corresponding symbol */
ret = llext_seek(ldr, ldr->sects[LLEXT_SECT_SYMTAB].sh_offset
+ ELF_R_SYM(rel.r_info) * sizeof(elf_sym_t));
if (ret != 0) {
goto out;
}
ret = llext_read(ldr, &sym, sizeof(elf_sym_t));
if (ret != 0) {
goto out;
}
ret = llext_seek(ldr, ldr->sects[LLEXT_SECT_STRTAB].sh_offset +
sym.st_name);
if (ret != 0) {
goto out;
}
ret = llext_read(ldr, name, sizeof(name));
if (ret != 0) {
goto out;
}
LOG_DBG("relocation %d:%d info %x (type %d, sym %d) offset %d sym_name "
"%s sym_type %d sym_bind %d sym_ndx %d",
i, j, rel.r_info, ELF_R_TYPE(rel.r_info), ELF_R_SYM(rel.r_info),
rel.r_offset, name, ELF_ST_TYPE(sym.st_info),
ELF_ST_BIND(sym.st_info), sym.st_shndx);
uintptr_t link_addr, op_loc, op_code;
op_loc = loc + rel.r_offset;
/* If symbol is undefined, then we need to look it up */
if (sym.st_shndx == SHN_UNDEF) {
link_addr = (uintptr_t)llext_find_sym(NULL, name);
if (link_addr == 0) {
LOG_ERR("Undefined symbol with no entry in "
"symbol table %s, offset %d, link section %d",
name, rel.r_offset, shdr.sh_link);
ret = -ENODATA;
goto out;
} else {
op_code = (uintptr_t)(loc + rel.r_offset);
LOG_INF("found symbol %s at 0x%lx, updating op code 0x%lx",
name, link_addr, op_code);
}
} else if (ELF_ST_TYPE(sym.st_info) == STT_SECTION) {
/* Current relocation location holds an offset into the section */
link_addr = (uintptr_t)ext->mem[ldr->sect_map[sym.st_shndx]]
+ sym.st_value
+ *((uintptr_t *)op_loc);
LOG_INF("found section symbol %s addr 0x%lx", name, link_addr);
} else {
/* Nothing to relocate here */
continue;
}
LOG_INF("relocating (linking) symbol %s type %d binding %d ndx %d offset "
"%d link section %d",
name, ELF_ST_TYPE(sym.st_info), ELF_ST_BIND(sym.st_info),
sym.st_shndx, rel.r_offset, shdr.sh_link);
LOG_INF("writing relocation symbol %s type %d sym %d at addr 0x%lx "
"addr 0x%lx",
name, ELF_R_TYPE(rel.r_info), ELF_R_SYM(rel.r_info),
op_loc, link_addr);
/* relocation */
arch_elf_relocate(&rel, op_loc, link_addr);
}
}
out:
return ret;
}
/*
* Load a valid ELF as an extension
*/
static int do_llext_load(struct llext_loader *ldr, struct llext *ext)
{
int ret = 0;
memset(ldr->sects, 0, sizeof(ldr->sects));
ldr->sect_cnt = 0;
ldr->sym_cnt = 0;
size_t sect_map_sz = ldr->hdr.e_shnum * sizeof(uint32_t);
ldr->sect_map = k_heap_alloc(&llext_heap, sect_map_sz, K_NO_WAIT);
if (!ldr->sect_map) {
LOG_ERR("Failed to allocate memory for section map, size %u", sect_map_sz);
ret = -ENOMEM;
goto out;
}
memset(ldr->sect_map, 0, ldr->hdr.e_shnum*sizeof(uint32_t));
ldr->sect_cnt = ldr->hdr.e_shnum;
LOG_DBG("Finding ELF tables...");
ret = llext_find_tables(ldr);
if (ret != 0) {
LOG_ERR("Failed to find important ELF tables, ret %d", ret);
goto out;
}
LOG_DBG("Mapping ELF sections...");
ret = llext_map_sections(ldr);
if (ret != 0) {
LOG_ERR("Failed to map ELF sections, ret %d", ret);
goto out;
}
LOG_DBG("Allocation memory for ELF sections...");
ret = llext_allocate_mem(ldr, ext);
if (ret != 0) {
LOG_ERR("Failed to map memory for ELF sections, ret %d", ret);
goto out;
}
LOG_DBG("Copying sections...");
ret = llext_copy_sections(ldr, ext);
if (ret != 0) {
LOG_ERR("Failed to copy ELF sections, ret %d", ret);
goto out;
}
LOG_DBG("Counting exported symbols...");
ret = llext_count_export_syms(ldr);
if (ret != 0) {
LOG_ERR("Failed to count exported ELF symbols, ret %d", ret);
goto out;
}
LOG_DBG("Allocating memory for symbol table...");
ret = llext_allocate_symtab(ldr, ext);
if (ret != 0) {
LOG_ERR("Failed to allocate extension symbol table, ret %d", ret);
goto out;
}
LOG_DBG("Copying symbols...");
ret = llext_copy_symbols(ldr, ext);
if (ret != 0) {
LOG_ERR("Failed to copy symbols, ret %d", ret);
goto out;
}
LOG_DBG("Linking ELF...");
ret = llext_link(ldr, ext);
if (ret != 0) {
LOG_ERR("Failed to link, ret %d", ret);
goto out;
}
out:
if (ldr->sect_map != NULL) {
k_heap_free(&llext_heap, ldr->sect_map);
}
if (ret != 0) {
LOG_DBG("Failed to load extension, freeing memory...");
for (enum llext_mem mem_idx = 0; mem_idx < LLEXT_MEM_COUNT; mem_idx++) {
if (ext->mem[mem_idx] != NULL) {
k_heap_free(&llext_heap, ext->mem[mem_idx]);
}
}
for (int i = 0; i < ext->sym_tab.sym_cnt; i++) {
if (ext->sym_tab.syms[i].name != NULL) {
k_heap_free(&llext_heap, ext->sym_tab.syms[i].name);
}
}
k_heap_free(&llext_heap, ext->sym_tab.syms);
} else {
LOG_DBG("loaded module, .text at %p, .rodata at %p", ext->mem[LLEXT_MEM_TEXT],
ext->mem[LLEXT_MEM_RODATA]);
}
return ret;
}
int llext_load(struct llext_loader *ldr, const char *name, struct llext **ext)
{
int ret = 0;
elf_ehdr_t ehdr;
ret = llext_seek(ldr, 0);
if (ret != 0) {
LOG_ERR("Failed to seek for ELF header");
goto out;
}
ret = llext_read(ldr, &ehdr, sizeof(ehdr));
if (ret != 0) {
LOG_ERR("Failed to read ELF header");
goto out;
}
/* check whether this is an valid elf file */
if (memcmp(ehdr.e_ident, ELF_MAGIC, sizeof(ELF_MAGIC)) != 0) {
LOG_HEXDUMP_ERR(ehdr.e_ident, 16, "Invalid ELF, magic does not match");
ret = -EINVAL;
goto out;
}
switch (ehdr.e_type) {
case ET_REL:
case ET_DYN:
LOG_DBG("Loading relocatable or shared elf");
*ext = k_heap_alloc(&llext_heap, sizeof(struct llext), K_NO_WAIT);
if (*ext == NULL) {
LOG_ERR("Not enough memory for extension metadata");
ret = -ENOMEM;
goto out;
}
memset(*ext, 0, sizeof(struct llext));
for (int i = 0; i < LLEXT_MEM_COUNT; i++) {
(*ext)->mem[i] = NULL;
}
ldr->hdr = ehdr;
ret = do_llext_load(ldr, *ext);
break;
default:
LOG_ERR("Unsupported elf file type %x", ehdr.e_type);
*ext = NULL;
ret = -EINVAL;
goto out;
}
if (ret == 0) {
strncpy((*ext)->name, name, sizeof((*ext)->name));
(*ext)->name[sizeof((*ext)->name) - 1] = '\0';
sys_slist_append(&_llext_list, &(*ext)->_llext_list);
LOG_INF("Loaded extension %s", (*ext)->name);
}
out:
return ret;
}
void llext_unload(struct llext *ext)
{
__ASSERT(ext, "Expected non-null extension");
sys_slist_find_and_remove(&_llext_list, &ext->_llext_list);
for (int i = 0; i < LLEXT_MEM_COUNT; i++) {
if (ext->mem[i] != NULL) {
LOG_DBG("freeing memory region %d", i);
k_heap_free(&llext_heap, ext->mem[i]);
ext->mem[i] = NULL;
}
}
if (ext->sym_tab.syms != NULL) {
for (int i = 0; i < ext->sym_tab.sym_cnt; i++) {
k_heap_free(&llext_heap, ext->sym_tab.syms[i].name);
}
k_heap_free(&llext_heap, ext->sym_tab.syms);
}
k_heap_free(&llext_heap, ext);
}
int llext_call_fn(struct llext *ext, const char *sym_name)
{
void (*fn)(void);
fn = llext_find_sym(&ext->sym_tab, sym_name);
if (fn == NULL) {
return -EINVAL;
}
fn();
return 0;
}