kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 70caa5263acefdad2e9fd1d2190f10a681c0b165
parent de680188bbd640e6176fdbc739a3a4a363611dd1
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 17:47:58 -0700

arm32 Phase 2: emit ARM ELF REL (addend in-field) for external-linker compatibility

ARM EABI (AAELF32) mandates SHT_REL — addends live in the relocated field,
not an r_addend slot — and stock ARM linkers (ld.lld, GNU ld) emit and expect
.rel.<name> with 8-byte Elf32_Rel entries. kit was emitting .rela.text
(SHT_RELA), which ld.lld mishandles: a bl extfn (R_ARM_THM_CALL) linked to the
wrong address and the program crashed (qemu exit 134).

- ObjElfArchOps gains uses_rel (set only for ARM) + reloc_field_addend hook.
- emit.c: when uses_rel, write 8-byte Elf32_Rel (r_offset, r_info; no addend),
  in .rel.<name> (SHT_REL), sh_entsize=ELF32_REL_SIZE. The addend is already in
  the field (data words via api_data_encode_addend; branch/MOVW-MOVT
  placeholders encode the addend-0 baseline). Other arches keep RELA verbatim.
- read.c: for REL relocs, reconstruct the implicit in-field addend via the
  per-arch decoder so kit's own linker reads the same addend ld.lld would
  (otherwise the field-embedded addend, e.g. &arr[5]=sym+20, is lost and the
  patcher overwrites it with S+0).
- reloc_arm.c: elf_arm_reloc_field_addend decodes data words (verbatim LE) and
  the Thumb-2 branch / MOVW-MOVT split-immediate fields (inverse of the
  patcher).

Verified under qemu (mps2-an385, cortex-m3): direct call, recursion, dense
switch, static-const array index, and &arr[5] data-addend all run correctly
under BOTH ld.lld and kit ld at -O0. Reloc form now byte-identical to clang
(.rel.text, R_ARM_THM_CALL, canonical f7ff fffe placeholder). x86_64/aarch64/
riscv64/riscv32 still emit .rela.*. test-elf/link/ar/cg-api green.

Diffstat:
Msrc/obj/elf/elf.h | 3+++
Msrc/obj/elf/emit.c | 47+++++++++++++++++++++++++++++++----------------
Msrc/obj/elf/read.c | 29++++++++++++++++++++++++++---
Msrc/obj/elf/reloc_arm.c | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/obj/format.h | 14++++++++++++++
Msrc/obj/registry.c | 4++++
6 files changed, 157 insertions(+), 19 deletions(-)

diff --git a/src/obj/elf/elf.h b/src/obj/elf/elf.h @@ -83,6 +83,7 @@ #define ELF32_PHDR_SIZE 32 #define ELF32_SYM_SIZE 16u #define ELF32_RELA_SIZE 12u +#define ELF32_REL_SIZE 8u /* Elf32_Rel: r_offset@0, r_info@4 (no addend) */ #define ELF32_DYN_SIZE 8u /* ---- special section indices ---- */ @@ -505,6 +506,8 @@ u32 elf_arm_reloc_from(u32 elf_type); const char* elf_arm_reloc_name(u32 elf_type); KitFloatAbi elf_arm_float_abi_from_e_flags(u32 e_flags); u32 elf_arm_float_abi_to_e_flags(KitFloatAbi abi); +/* REL addend reconstruction (ObjElfArchOps.reloc_field_addend). */ +i64 elf_arm_reloc_field_addend(u32 kind, const u8* field, u32 width); /* ---- little-endian byte writers (Writer-based) ---- * Writes go through the shared writer_u*_le helpers (core/bytes.h); the diff --git a/src/obj/elf/emit.c b/src/obj/elf/emit.c @@ -180,8 +180,15 @@ void emit_elf(Compiler* c, ObjBuilder* ob, Writer* w) { (u32)c->target.ptr_size); } int is32 = (c->target.ptr_size == 4); + /* SHT_REL vs SHT_RELA: ARM EABI (AAELF32) carries the addend in the + * relocated field (8-byte Elf32_Rel, no r_addend slot); every other arch + * uses RELA. `uses_rel` is set per-arch in obj_elf_arch_ops. When set the + * reloc records below omit r_addend and the section is named ".rel.*". */ + int uses_rel = elf->uses_rel != 0; u32 sym_size = is32 ? ELF32_SYM_SIZE : ELF64_SYM_SIZE; u32 rela_size = is32 ? ELF32_RELA_SIZE : ELF64_RELA_SIZE; + /* Bytes per relocation entry actually emitted (REL = 8, RELA = 12/24). */ + u32 reloc_ent_size = uses_rel ? ELF32_REL_SIZE : rela_size; u32 ehdr_size = is32 ? ELF32_EHDR_SIZE : ELF64_EHDR_SIZE; u32 shdr_size = is32 ? ELF32_SHDR_SIZE : ELF64_SHDR_SIZE; @@ -407,8 +414,8 @@ void emit_elf(Compiler* c, ObjBuilder* ob, Writer* w) { /* Allocate one .rela section per obj section that has any relocs. */ typedef struct RelaPlan { u32 obj_section; /* obj section the rela applies to */ - u8* bytes; /* arena-allocated rela bytes */ - u32 size; /* bytes count = nrelocs * rela_size (24 or 12) */ + u8* bytes; /* arena-allocated rela/rel bytes */ + u32 size; /* bytes count = nrelocs * reloc_ent_size */ } RelaPlan; RelaPlan* rela_plans = arena_zarray(c->scratch, RelaPlan, nobjsec); @@ -421,7 +428,7 @@ void emit_elf(Compiler* c, ObjBuilder* ob, Writer* w) { const u32* rix = obj_reloc_section(ob, si, &nr); if (!nr) continue; u8* buf = - (u8*)arena_alloc(c->scratch, (size_t)rela_size * nr, _Alignof(u64)); + (u8*)arena_alloc(c->scratch, (size_t)reloc_ent_size * nr, _Alignof(u64)); u32 j = 0; /* rix[] lists section si's live relocs in ascending global order. */ for (u32 k = 0; k < nr; ++k) { @@ -442,15 +449,20 @@ void emit_elf(Compiler* c, ObjBuilder* ob, Writer* w) { } else { sym_elf_idx = sym_to_elf[r->sym]; } - /* Elf32_Rela (12B): r_offset@0, r_info@4 (ELF32_R_INFO, 8-bit - * type), r_addend@8 — all 4-byte. Elf64_Rela (24B): all 8-byte. */ - u8* slot = &buf[j * rela_size]; + /* Elf32_Rel (8B): r_offset@0, r_info@4 — addend lives in the relocated + * field (ARM EABI). Elf32_Rela (12B): + r_addend@8. Elf64_Rela (24B): + * all 8-byte. The addend is already present in the section field for + * REL: data words carry it (api_data_encode_addend), and the Thumb-2 + * branch / MOVW-MOVT placeholders encode the (addend 0) baseline; a + * non-zero MOVW/MOVT addend is folded in at the arm32 emit site. */ + u8* slot = &buf[j * reloc_ent_size]; if (is32) { for (int b = 0; b < 4; ++b) slot[b] = (u8)((u32)r->offset >> (b * 8)); u32 info = ELF32_R_INFO(sym_elf_idx, etype); for (int b = 0; b < 4; ++b) slot[4 + b] = (u8)(info >> (b * 8)); - for (int b = 0; b < 4; ++b) - slot[8 + b] = (u8)((u32)r->addend >> (b * 8)); + if (!uses_rel) + for (int b = 0; b < 4; ++b) + slot[8 + b] = (u8)((u32)r->addend >> (b * 8)); } else { for (int b = 0; b < 8; ++b) slot[b] = (u8)((u64)r->offset >> (b * 8)); u64 info = ELF64_R_INFO(sym_elf_idx, etype); @@ -462,32 +474,35 @@ void emit_elf(Compiler* c, ObjBuilder* ob, Writer* w) { } rela_plans[nrela_plans].obj_section = si; rela_plans[nrela_plans].bytes = buf; - rela_plans[nrela_plans].size = nr * rela_size; + rela_plans[nrela_plans].size = nr * reloc_ent_size; nrela_plans++; } - /* Append ElfSec entries for each .rela.<name>. Names are ".rela" + - * the obj section name; allocate in scratch. */ + /* Append ElfSec entries for each reloc section. Names are ".rel"/".rela" + * + the obj section name (REL for ARM, RELA otherwise); allocate in + * scratch. */ + const char* rel_prefix = uses_rel ? ".rel" : ".rela"; + u32 prefix_len = uses_rel ? 4u : 5u; u32* rela_elf_idx = arena_array(c->scratch, u32, nrela_plans + 1); for (u32 ri = 0; ri < nrela_plans; ++ri) { u32 si = rela_plans[ri].obj_section; const Section* s = obj_section_get(ob, si); u32 base_len; const char* base = sym_to_str(c, s->name, &base_len); - u32 nlen = 5 + base_len; /* ".rela" + base */ + u32 nlen = prefix_len + base_len; /* ".rel"/".rela" + base */ char* nm = (char*)arena_alloc(c->scratch, nlen + 1, 1); - memcpy(nm, ".rela", 5); - memcpy(nm + 5, base, base_len); + memcpy(nm, rel_prefix, prefix_len); + memcpy(nm + prefix_len, base, base_len); nm[nlen] = 0; ElfSec* es = &secs[nsecs]; memset(es, 0, sizeof *es); es->name = nm; es->name_len = nlen; - es->sh_type = SHT_RELA; + es->sh_type = uses_rel ? SHT_REL : SHT_RELA; es->sh_flags = SHF_INFO_LINK; es->sh_addralign = is32 ? 4 : 8; - es->sh_entsize = rela_size; + es->sh_entsize = reloc_ent_size; es->sh_info = obj_to_elf[si]; /* section the relas apply to */ /* sh_link filled below once we know symtab's elf index. */ es->raw_bytes = rela_plans[ri].bytes; diff --git a/src/obj/elf/read.c b/src/obj/elf/read.c @@ -899,12 +899,26 @@ ObjBuilder* read_elf(Compiler* c, const char* name, const u8* data, ObjSecId target = elf_to_obj[sh->sh_info]; if (target == OBJ_SEC_NONE) continue; + /* For REL the addend lives in the relocated field, not an r_addend slot. + * Locate the target section's bytes so the per-arch decoder can recover + * it (matching what a field-reading linker like ld.lld does). A NOBITS + * target has no file bytes — its relocs keep addend 0. */ + const ShdrRec* tgt_sh = &shdrs[sh->sh_info]; + const u8* tgt_bytes = NULL; + u64 tgt_size = 0; + if (is_rel && arch && arch->reloc_field_addend && + tgt_sh->sh_type != SHT_NOBITS && + tgt_sh->sh_offset + tgt_sh->sh_size <= len) { + tgt_bytes = data + tgt_sh->sh_offset; + tgt_size = tgt_sh->sh_size; + } + u32 nrec = (u32)(sh->sh_size / entsize); const u8* base = data + sh->sh_offset; for (u32 j = 0; j < nrec; ++j) { - /* Elf32_Rela (12B): r_offset@0, r_info@4 (ELF32 packing, 8-bit - * type), r_addend@8. Elf64_Rela (24B): r_offset@0, r_info@8, - * r_addend@16. */ + /* Elf32_Rel (8B): r_offset@0, r_info@4 — addend in-field. + * Elf32_Rela (12B): + r_addend@8. Elf64_Rela (24B): r_offset@0, + * r_info@8, r_addend@16. */ const u8* p = base + (u64)j * entsize; u64 r_offset = elf_rd_addr(p + 0, is32); u64 r_info = is32 ? (u64)rd_u32_le(p + 4) : rd_u64_le(p + 8); @@ -920,6 +934,15 @@ ObjBuilder* read_elf(Compiler* c, const char* name, const u8* data, "read_elf: unsupported reloc type %u for e_machine 0x%x", etype, (u32)e_machine); + /* REL: reconstruct the implicit in-field addend (width capped to the + * field's remaining bytes; Thumb-2 instructions and data words are 4). */ + if (tgt_bytes && r_offset < tgt_size) { + u32 width = (u32)(tgt_size - r_offset); + if (width > 4) width = 4; + r_addend = arch->reloc_field_addend((u32)kind, tgt_bytes + r_offset, + width); + } + ObjSymId target_sym = OBJ_SYM_NONE; if (esym && sym_elf_to_obj && esym < nsyms) target_sym = sym_elf_to_obj[esym]; diff --git a/src/obj/elf/reloc_arm.c b/src/obj/elf/reloc_arm.c @@ -4,6 +4,7 @@ * R_ABS32 / R_REL32 / R_ABS16 / R_ABS8 kinds; only the instruction-embedded * Thumb-2 kinds get arch entries. The split-immediate byte patcher lives in * src/arch/arm32/reloc.c. */ +#include "core/bytes.h" #include "obj/elf/elf.h" #include "obj/obj.h" @@ -128,3 +129,81 @@ u32 elf_arm_float_abi_to_e_flags(KitFloatAbi abi) { return 0u; /* soft / agnostic: no float-ABI bit */ } } + +/* A 32-bit Thumb-2 instruction is two independently-little-endian 16-bit + * halfwords, the HIGH halfword FIRST in memory (matches arm_rd_t32 in + * src/arch/arm32/reloc.c). The returned value has hw1 in bits [31:16]. */ +static u32 arm_rd_t32_field(const u8* p) { + return ((u32)rd_u16_le(p) << 16) | (u32)rd_u16_le(p + 2); +} + +static i32 sign_extend(u32 v, u32 bits) { + u32 m = 1u << (bits - 1); + return (i32)((v ^ m) - m); +} + +/* Reconstruct the implicit addend a REL relocation carries in its field — the + * inverse of the patchers in src/arch/arm32/reloc.c. For REL there is no + * r_addend slot, so kit's own linker must recover the same addend a + * field-reading linker (ld.lld, GNU ld) would. `field` points at the + * relocated location; `width` is its byte width (4 for data words and Thumb-2 + * instructions). Mirrors the AAELF32 REL addend conventions. */ +i64 elf_arm_reloc_field_addend(u32 kind /* RelocKind */, const u8* field, + u32 width) { + switch (kind) { + case R_ABS32: + case R_REL32: + case R_PC32: + /* Plain little-endian data word; the addend sits in the field verbatim + * (api_data_encode_addend pre-wrote it on emit). */ + return width >= 4 ? (i64)(i32)rd_u32_le(field) : 0; + case R_ABS16: + return (i64)(i16)rd_u16_le(field); + case R_ABS8: + return (i64)(i8)field[0]; + case R_ARM_THM_CALL: + case R_ARM_THM_JUMP24: { + /* BL/B.W 25-bit field {S,I1,I2,imm10,imm11}, J↔I XOR-with-S. The patcher + * wrote disp = (S&~1)+A-P-4; at read time S=P=0, so A = disp + 4. The + * canonical placeholder (disp=-4) thus decodes to addend 0. */ + u32 instr = arm_rd_t32_field(field); + u32 sbit = (instr >> 26) & 1u; + u32 j1 = (instr >> 13) & 1u, j2 = (instr >> 11) & 1u; + u32 i1 = (~(j1 ^ sbit)) & 1u, i2 = (~(j2 ^ sbit)) & 1u; + u32 imm10 = (instr >> 16) & 0x3ffu, imm11 = instr & 0x7ffu; + u32 off = (sbit << 24) | (i1 << 23) | (i2 << 22) | (imm10 << 12) | + (imm11 << 1); + return (i64)sign_extend(off, 25) + 4; + } + case R_ARM_THM_JUMP19: { + /* B<cond>.W 21-bit field {S,J2,J1,imm6,imm11} (no XOR-with-S). */ + u32 instr = arm_rd_t32_field(field); + u32 sbit = (instr >> 26) & 1u; + u32 j2 = (instr >> 11) & 1u, j1 = (instr >> 13) & 1u; + u32 imm6 = (instr >> 16) & 0x3fu, imm11 = instr & 0x7ffu; + u32 off = + (sbit << 20) | (j2 << 19) | (j1 << 18) | (imm6 << 12) | (imm11 << 1); + return (i64)sign_extend(off, 21) + 4; + } + case R_ARM_THM_MOVW_ABS_NC: + case R_ARM_THM_MOVT_ABS: + case R_ARM_THM_MOVW_PREL_NC: + case R_ARM_THM_MOVT_PREL: { + /* MOVW/MOVT 16-bit immediate scattered imm4:i:imm3:imm8. Each reloc + * carries only one 16-bit half of (S+A); at read time S=0 so the field + * is (A & 0xffff) for MOVW or ((A>>16) & 0xffff) for MOVT. Current arm32 + * codegen always emits addend 0 here (a constant byte offset folds into + * a separate add), so this decodes to 0. A non-zero addend split across + * the MOVW/MOVT pair would need both halves recombined into one 32-bit + * A for kit-ld's (S+A) patcher — a Phase-2+ refinement; lld already + * handles the per-half group-relocation form. */ + u32 instr = arm_rd_t32_field(field); + u32 imm4 = (instr >> 16) & 0xfu, i = (instr >> 26) & 1u; + u32 imm3 = (instr >> 12) & 7u, imm8 = instr & 0xffu; + u32 imm16 = (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8; + return (i64)imm16; + } + default: + return 0; + } +} diff --git a/src/obj/format.h b/src/obj/format.h @@ -67,6 +67,13 @@ typedef struct ObjElfArchOps { * hosted-vs-freestanding split for RISC-V is still applied by the * caller in src/obj/elf/link.c; this field is the per-arch maximum. */ u32 tls_tp_bias; + /* SHT_REL (vs SHT_RELA): 1 for arches whose ELF ABI carries relocation + * addends in the relocated field rather than an explicit r_addend slot. + * The 32-bit ARM EABI (AAELF32) mandates REL; clang/gas/GNU ld all emit + * and expect `.rel.<name>` with 8-byte Elf32_Rel entries. Every other + * arch kit targets uses RELA (0). emit.c folds the addend into the field + * and writes Elf32_Rel when this is set. */ + u8 uses_rel; u32 (*reloc_to)(u32 kind); u32 (*reloc_from)(u32 wire_type); /* Diagnostic spelling of a per-arch ELF reloc wire type (e.g. @@ -77,6 +84,13 @@ typedef struct ObjElfArchOps { * EF_RISCV_FLOAT_ABI_*; other arches have no float-ABI e_flags and * leave this NULL (callers treat NULL as KIT_FLOAT_ABI_DEFAULT). */ KitFloatAbi (*float_abi_from_e_flags)(u32 e_flags); + /* REL-only: reconstruct the implicit addend a relocation carries in its + * relocated field (Elf32_Rel has no r_addend slot). `field` points at the + * `width`-byte relocated location in the target section. Only set for + * `uses_rel` arches (ARM); the reader calls it per REL reloc so kit's own + * linker reads the same addend a field-reading linker (ld.lld) would. + * NULL or a NOBITS target -> addend 0. */ + i64 (*reloc_field_addend)(u32 kind, const u8* field, u32 width); } ObjElfArchOps; /* Structural role of a Mach-O relocation entry. Mach-O's r_type is a 4-bit diff --git a/src/obj/registry.c b/src/obj/registry.c @@ -164,6 +164,9 @@ static const ObjElfArchOps obj_elf_arch_ops[] = { * convention — clang/gas mark only hard-float). */ .e_flags = EF_ARM_EABI_VER5, .default_musl_interp = NULL, + /* AAELF32 mandates SHT_REL (addend in-field, no r_addend slot). + * Stock ARM linkers (ld.lld, GNU ld) expect `.rel.<name>`. */ + .uses_rel = 1, .r_relative = ELF_R_ARM_NONE, .r_glob_dat = ELF_R_ARM_NONE, .r_jump_slot = ELF_R_ARM_NONE, @@ -174,6 +177,7 @@ static const ObjElfArchOps obj_elf_arch_ops[] = { .reloc_from = elf_arm_reloc_from, .reloc_name = elf_arm_reloc_name, .float_abi_from_e_flags = elf_arm_float_abi_from_e_flags, + .reloc_field_addend = elf_arm_reloc_field_addend, }, #endif #if !KIT_ARCH_AA64_ENABLED && !KIT_ARCH_X64_ENABLED && \