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:
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 && \