commit 8dea968324928d6a77b54be45cb1037097f109d8
parent 4cff1a6f663d2b9cc7efb6e2cf4305b38ca840db
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 16 Jul 2026 10:11:23 -0700
link: enforce target compatibility and canonical symbols
Diffstat:
15 files changed, 773 insertions(+), 63 deletions(-)
diff --git a/src/api/link.c b/src/api/link.c
@@ -625,9 +625,11 @@ static int link_report_slice_cmp(KitSlice a, KitSlice b) {
return 0;
}
-static int link_report_symbol_include(const LinkSymbol* s) {
+static int link_report_symbol_include(const LinkImage* img,
+ const LinkSymbol* s) {
if (!s || !s->name || !s->defined || s->imported) return 0;
if (s->kind == SK_FILE || s->kind == SK_SECTION) return 0;
+ if (s->bind != SB_LOCAL && !link_symbol_is_canonical_global(img, s)) return 0;
return 1;
}
@@ -661,7 +663,7 @@ static KitStatus link_report_collect_symbols(LinkImage* img,
if (!out->ids) return KIT_NOMEM;
for (i = 0; i < out->cap; ++i) {
const LinkSymbol* s = LinkSyms_at(&img->syms, i);
- if (link_report_symbol_include(s)) out->ids[out->n++] = s->id;
+ if (link_report_symbol_include(img, s)) out->ids[out->n++] = s->id;
}
for (i = 1; i < out->n; ++i) {
LinkSymId key = out->ids[i];
diff --git a/src/link/link.c b/src/link/link.c
@@ -15,6 +15,7 @@
#include <kit/archive.h>
#include <kit/core.h>
#include <kit/object.h>
+#include <kit/target.h>
#include <string.h>
#include "core/heap.h"
@@ -22,8 +23,162 @@
#include "core/slice.h"
#include "core/vec.h"
#include "link/link_internal.h"
+#include "obj/elf/elf.h"
#include "obj/format.h"
+static const char* link_float_abi_name(KitFloatAbi abi) {
+ switch (abi) {
+ case KIT_FLOAT_ABI_DEFAULT:
+ return "default";
+ case KIT_FLOAT_ABI_SOFT:
+ return "soft";
+ case KIT_FLOAT_ABI_SINGLE:
+ return "single";
+ case KIT_FLOAT_ABI_DOUBLE:
+ return "double";
+ }
+ return "unknown";
+}
+
+/* OS compatibility is deliberately directional: an ELF object carrying the
+ * generic/Linux OSABI may be consumed by Linux, Android, or a freestanding
+ * image, because that header value is what assemblers use for otherwise
+ * platform-neutral relocatables. A FreeBSD object or a Mach-O platform stamp
+ * is never borrowed across operating systems. */
+static int link_target_os_compatible(KitTargetSpec expected,
+ KitTargetSpec actual) {
+ if (expected.os == actual.os) return 1;
+ if (expected.obj != KIT_OBJ_ELF || actual.os != KIT_OS_LINUX) return 0;
+ return expected.os == KIT_OS_ANDROID ||
+ expected.os == KIT_OS_FREESTANDING;
+}
+
+static const char* link_target_mismatch(KitTargetSpec expected,
+ KitTargetSpec actual) {
+ if (expected.arch != actual.arch) return "architecture";
+ if (expected.obj != actual.obj) return "object format";
+ if (expected.ptr_size != actual.ptr_size) return "pointer size";
+ if (expected.big_endian != actual.big_endian) return "endianness";
+ if (!link_target_os_compatible(expected, actual)) return "operating system";
+ if ((expected.arch == KIT_ARCH_RV32 || expected.arch == KIT_ARCH_RV64) &&
+ expected.float_abi != KIT_FLOAT_ABI_DEFAULT &&
+ actual.float_abi != KIT_FLOAT_ABI_DEFAULT &&
+ expected.float_abi != actual.float_abi)
+ return "float ABI";
+ return NULL;
+}
+
+static void link_target_panic(Linker* l, Slice name, Slice container,
+ KitTargetSpec actual, const char* reason) {
+ KitTargetSpec expected = l->c->target;
+ if (container.s) {
+ compiler_panic(
+ l->c, SRCLOC_NONE,
+ "link: incompatible input '%.*s(%.*s)': %s mismatch; expected "
+ "arch=%s os=%s format=%s ptr=%u endian=%s float=%s, got "
+ "arch=%s os=%s format=%s ptr=%u endian=%s float=%s",
+ SLICE_ARG(container), SLICE_ARG(name), reason,
+ kit_target_arch_name(expected.arch), kit_target_os_name(expected.os),
+ kit_target_obj_name(expected.obj), (u32)expected.ptr_size,
+ expected.big_endian ? "big" : "little",
+ link_float_abi_name((KitFloatAbi)expected.float_abi),
+ kit_target_arch_name(actual.arch), kit_target_os_name(actual.os),
+ kit_target_obj_name(actual.obj), (u32)actual.ptr_size,
+ actual.big_endian ? "big" : "little",
+ link_float_abi_name((KitFloatAbi)actual.float_abi));
+ }
+ compiler_panic(
+ l->c, SRCLOC_NONE,
+ "link: incompatible input '%.*s': %s mismatch; expected "
+ "arch=%s os=%s format=%s ptr=%u endian=%s float=%s, got "
+ "arch=%s os=%s format=%s ptr=%u endian=%s float=%s",
+ SLICE_ARG(name), reason, kit_target_arch_name(expected.arch),
+ kit_target_os_name(expected.os), kit_target_obj_name(expected.obj),
+ (u32)expected.ptr_size, expected.big_endian ? "big" : "little",
+ link_float_abi_name((KitFloatAbi)expected.float_abi),
+ kit_target_arch_name(actual.arch), kit_target_os_name(actual.os),
+ kit_target_obj_name(actual.obj), (u32)actual.ptr_size,
+ actual.big_endian ? "big" : "little",
+ link_float_abi_name((KitFloatAbi)actual.float_abi));
+}
+
+static void link_validate_target(Linker* l, Slice name, Slice container,
+ KitTargetSpec actual) {
+ const char* reason = link_target_mismatch(l->c->target, actual);
+ if (reason) link_target_panic(l, name, container, actual, reason);
+}
+
+static void link_validate_obj_target(Linker* l, Slice name, ObjBuilder* ob) {
+ Compiler* source = obj_compiler(ob);
+ if (!source)
+ compiler_panic(l->c, SRCLOC_NONE,
+ "link: input '%.*s' has no target-owning compiler",
+ SLICE_ARG(name));
+ link_validate_target(l, name, SLICE_NULL, source->target);
+}
+
+static void link_validate_bytes_target(Linker* l, Slice name, Slice container,
+ const u8* data, size_t len,
+ int allow_text_dso) {
+ KitTargetSpec actual;
+ KitStatus st = kit_detect_target(data, len, &actual);
+ if (st == KIT_OK) {
+ link_validate_target(l, name, container, actual);
+ return;
+ }
+ if (allow_text_dso && kit_detect_fmt(data, len) == KIT_BIN_UNKNOWN) return;
+ if (container.s)
+ compiler_panic(l->c, SRCLOC_NONE,
+ "link: cannot determine target of input '%.*s(%.*s)'",
+ SLICE_ARG(container), SLICE_ARG(name));
+ compiler_panic(l->c, SRCLOC_NONE,
+ "link: cannot determine target of input '%.*s'",
+ SLICE_ARG(name));
+}
+
+void link_merge_elf_e_flags(Linker* l, ObjBuilder* ob, KitSlice label) {
+ u32 incoming;
+ u32 known = EF_RISCV_RVC | EF_RISCV_FLOAT_ABI_MASK | EF_RISCV_RVE |
+ EF_RISCV_TSO;
+ u32 must_match = EF_RISCV_FLOAT_ABI_MASK | EF_RISCV_RVE | ~known;
+ u32 merge_bits = EF_RISCV_RVC | EF_RISCV_TSO;
+ KitFloatAbi incoming_abi;
+ KitFloatAbi target_abi;
+ Slice first;
+ if (!l || !ob || l->c->target.obj != KIT_OBJ_ELF ||
+ (l->c->target.arch != KIT_ARCH_RV32 &&
+ l->c->target.arch != KIT_ARCH_RV64) ||
+ !obj_get_elf_e_flags(ob, &incoming))
+ return;
+ incoming_abi = elf_riscv_float_abi_from_e_flags(incoming);
+ target_abi = (KitFloatAbi)l->c->target.float_abi;
+ if (target_abi != KIT_FLOAT_ABI_DEFAULT && incoming_abi != target_abi) {
+ compiler_panic(l->c, SRCLOC_NONE,
+ "link: incompatible input '%.*s': RISC-V float ABI is %s, "
+ "link target requires %s (e_flags=0x%x)",
+ SLICE_ARG(label), link_float_abi_name(incoming_abi),
+ link_float_abi_name(target_abi), incoming);
+ }
+ if (!l->have_elf_e_flags) {
+ l->elf_e_flags = incoming;
+ l->elf_e_flags_source = pool_intern_slice(l->c->global, label);
+ l->have_elf_e_flags = 1;
+ return;
+ }
+ if ((l->elf_e_flags & must_match) != (incoming & must_match)) {
+ first = l->elf_e_flags_source
+ ? pool_slice(l->c->global, l->elf_e_flags_source)
+ : SLICE_LIT("<first input>");
+ compiler_panic(l->c, SRCLOC_NONE,
+ "link: incompatible RISC-V ELF e_flags: input '%.*s' has "
+ "0x%x, input '%.*s' has 0x%x (float ABI, RVE, and reserved "
+ "bits must agree)",
+ SLICE_ARG(label), incoming, SLICE_ARG(first),
+ l->elf_e_flags);
+ }
+ l->elf_e_flags |= incoming & merge_bits;
+}
+
/* ---- SrcLoc helper ---- */
/* SymHash is a HASHMAP_DEFINE instance — see link_internal.h. The thin
@@ -109,6 +264,8 @@ LinkInputId link_add_obj(Linker* l, ObjBuilder* ob) {
LinkInputId id;
LinkInput* in;
if (!l || !ob) return LINK_INPUT_NONE;
+ link_validate_obj_target(l, SLICE_LIT("<in-memory object>"), ob);
+ link_merge_elf_e_flags(l, ob, SLICE_LIT("<in-memory object>"));
in = inputs_push(l, &id);
in->kind = LINK_INPUT_OBJ;
in->order = l->next_input_order++;
@@ -124,7 +281,10 @@ LinkInputId link_add_obj_bytes(Linker* l, const char* name, const u8* data,
KitBinFmt fmt;
const ObjFormatImpl* impl;
const char* reader_name;
+ Slice label;
if (!l || !data || !len) return LINK_INPUT_NONE;
+ label = name ? slice_from_cstr(name) : SLICE_LIT("(unnamed)");
+ link_validate_bytes_target(l, label, SLICE_NULL, data, len, 0);
fmt = kit_detect_fmt(data, len);
impl = obj_format_lookup_bin(fmt);
if (!impl || !impl->read)
@@ -141,6 +301,7 @@ LinkInputId link_add_obj_bytes(Linker* l, const char* name, const u8* data,
l->c, SRCLOC_NONE, "link_add_obj_bytes: %.*s returned NULL for '%.*s'",
SLICE_ARG(slice_from_cstr(reader_name)),
SLICE_ARG(name ? slice_from_cstr(name) : SLICE_LIT("(unnamed)")));
+ link_merge_elf_e_flags(l, ob, label);
in = inputs_push(l, &id);
in->order = l->next_input_order++;
in->obj = ob; /* re-uses the ObjBuilder slot for ownership */
@@ -167,7 +328,10 @@ LinkInputId link_add_dso_bytes(Linker* l, const char* name, const u8* data,
KitBinFmt fmt;
ObjFormatDsoReader reader;
const char* reader_name;
+ Slice label;
if (!l || !data || !len) return LINK_INPUT_NONE;
+ label = name ? slice_from_cstr(name) : SLICE_LIT("(unnamed)");
+ link_validate_bytes_target(l, label, SLICE_NULL, data, len, 1);
if (!obj_format_dso_reader_for_bytes(data, len, &fmt, &reader))
compiler_panic(
l->c, SRCLOC_NONE,
@@ -182,6 +346,7 @@ LinkInputId link_add_dso_bytes(Linker* l, const char* name, const u8* data,
l->c, SRCLOC_NONE, "link_add_dso_bytes: %.*s returned NULL for '%.*s'",
SLICE_ARG(slice_from_cstr(reader_name)),
SLICE_ARG(name ? slice_from_cstr(name) : SLICE_LIT("(unnamed)")));
+ link_merge_elf_e_flags(l, ob, label);
in = inputs_push(l, &id);
in->kind = LINK_INPUT_DSO_BYTES;
in->order = l->next_input_order++;
@@ -271,6 +436,10 @@ LinkInputId link_add_archive_bytes(Linker* l, const char* name, const u8* data,
ObjBuilder* ob = NULL;
KitBinFmt mfmt = kit_detect_fmt(mem.data, mem.size);
const ObjFormatImpl* member_impl = obj_format_lookup_bin(mfmt);
+ link_validate_bytes_target(
+ l, mem.name.len ? mem.name : SLICE_LIT("(unnamed)"),
+ name ? slice_from_cstr(name) : SLICE_LIT("(unnamed)"), mem.data,
+ mem.size, 0);
if (target_impl && target_impl->archive_member) {
ObjFormatArchiveMember desc;
ObjFormatArchiveAction action;
diff --git a/src/link/link_internal.h b/src/link/link_internal.h
@@ -223,6 +223,12 @@ struct Linker {
* dynamic ELF emit path (Phase 6). */
int emit_pie;
u16 pe_subsystem;
+ /* RISC-V psABI e_flags accumulated from object inputs selected for this
+ * link. Float-ABI/RVE conflicts are rejected while registering inputs;
+ * RVC/TSO feature-presence bits are OR-merged. */
+ u32 elf_e_flags;
+ Sym elf_e_flags_source;
+ u8 have_elf_e_flags;
/* Caller-supplied PT_INTERP. layout_dyn falls back to a target-
* derived default when this is 0. */
Sym interp_path;
@@ -338,6 +344,7 @@ void link_emit_internal_abs64(LinkImage* img, LinkSectionId lsid, u32 offset,
/* ---- Public entries (link_resolve.c) --------------------------------------
*/
void link_ingest_archives(struct Linker*);
+void link_merge_elf_e_flags(struct Linker*, ObjBuilder*, KitSlice label);
/* PE/COFF only: synthesize a tiny ObjBuilder providing the mingw CRT
* `__CTOR_LIST__` / `__CTOR_END__` / `__DTOR_LIST__` / `__DTOR_END__`
* boundary symbols. See link_resolve.c for the contract. */
@@ -424,6 +431,10 @@ struct LinkImage {
* symbol's dso_input_id back to the providing dylib's install-name). */
struct Linker* linker;
+ /* Final merged RISC-V ELF flags, copied after archive selection. */
+ u32 elf_e_flags;
+ u8 have_elf_e_flags;
+
LinkSyms syms; /* LinkSymId = slot index + 1 */
SymHash globals; /* name -> LinkSymId for global/weak */
@@ -528,6 +539,27 @@ struct LinkImage {
u8 headers_present;
};
+/* Whether S is the canonical LinkSymbol for its non-local name.
+ *
+ * Resolution deliberately keeps one LinkSymbol per input symbol: after an
+ * undefined reference is resolved, its per-input slot mirrors the definition
+ * so relocations can retain their stable LinkSymId. img->globals remains the
+ * authority for the single canonical global/weak slot. Format emitters and
+ * post-link reports must use this predicate rather than mistaking those
+ * resolved reference slots for distinct definitions.
+ *
+ * Locals and nameless records are outside the global-name authority and are
+ * therefore not canonical globals. A named non-local missing from globals is
+ * retained defensively; synthetic/import bookkeeping can briefly have that
+ * shape while an image is being assembled. */
+static inline int link_symbol_is_canonical_global(const LinkImage* img,
+ const LinkSymbol* s) {
+ LinkSymId canonical;
+ if (!img || !s || !s->name || s->bind == SB_LOCAL) return 0;
+ canonical = symhash_get(&img->globals, s->name);
+ return canonical == LINK_SYM_NONE || canonical == s->id;
+}
+
/* Page granularity used for ELF segment alignment and the file-offset /
* vaddr congruence the runtime loader requires. 16 KiB matches AArch64
* Apple Silicon and the common Linux/AArch64 kernel config; 4 KiB pages
diff --git a/src/link/link_layout.c b/src/link/link_layout.c
@@ -2144,8 +2144,6 @@ static int link_section_name_is_dynamic(Slice nm) {
static void link_validate_freestanding(Linker* l) {
u32 ii;
- u32 eflags0 = 0;
- int have_eflags = 0;
if (!l->freestanding_strict) return;
for (ii = 0; ii < LinkInputs_count(&l->inputs); ++ii) {
@@ -2164,28 +2162,6 @@ static void link_validate_freestanding(Linker* l) {
}
if (!ob) continue;
- /* (d) cross-input ABI mismatch. The per-input arch / object-format match
- * against the link target is enforced by the driver (build.c / ld.c), which
- * has each input's detected KitTargetSpec via kit_detect_target; libkit
- * sees only the format-neutral ObjBuilders here. What libkit can still check
- * cheaply is ELF e_flags compatibility across inputs (RISC-V float-ABI /
- * RVC bits; 0 for x86_64/aarch64) — a mismatch is a genuine ABI conflict
- * even within a single arch. */
- {
- u32 ef;
- if (obj_get_elf_e_flags(ob, &ef)) {
- if (!have_eflags) {
- eflags0 = ef;
- have_eflags = 1;
- } else if (ef != eflags0) {
- compiler_panic(l->c, SRCLOC_NONE,
- "link: freestanding link: input '%s' has incompatible "
- "ELF e_flags (0x%x vs 0x%x)",
- label, ef, eflags0);
- }
- }
- }
-
/* (a)+(b) dynamic-interpreter / dynamic-section / PLT-GOT-import inputs.
* A freestanding static image must not pull in .interp (PT_INTERP) or any
* .dynamic/.dynsym/.plt/.got.plt artifact. */
@@ -2225,6 +2201,8 @@ LinkImage* link_resolve(Linker* l) {
img->text_base_set = l->text_base_set;
img->text_base = l->text_base;
img->shared = l->emit_shared;
+ img->elf_e_flags = l->elf_e_flags;
+ img->have_elf_e_flags = l->have_elf_e_flags;
img->ninput_maps = LinkInputs_count(&l->inputs);
metrics_count(l->c, "link.inputs", img->ninput_maps);
diff --git a/src/link/link_relocatable.c b/src/link/link_relocatable.c
@@ -234,25 +234,14 @@ static ObjSecId rel_find_compatible_section(ObjBuilder* out,
static void rel_copy_sections(Linker* l, ObjBuilder* out, RelInputMap* maps,
u32 ninputs) {
u32 ii, j;
- int have_eflags = 0;
- u32 eflags = 0;
for (ii = 0; ii < ninputs; ++ii) {
LinkInput* in = LinkInputs_at(&l->inputs, ii);
ObjBuilder* ob = in->obj;
u32 nsec;
- u32 in_eflags;
if (in->kind == LINK_INPUT_DSO_BYTES)
compiler_panic(l->c, SRCLOC_NONE,
"link -r: DSO inputs are not supported");
if (!ob) continue;
- if (obj_get_elf_e_flags(ob, &in_eflags)) {
- if (!have_eflags) {
- eflags = in_eflags;
- have_eflags = 1;
- } else if (eflags != in_eflags) {
- compiler_panic(l->c, SRCLOC_NONE, "link -r: incompatible ELF e_flags");
- }
- }
nsec = obj_section_count(ob);
maps[ii].nsection = nsec;
maps[ii].section = (RelObjSecMap*)l->heap->alloc(
@@ -292,7 +281,7 @@ static void rel_copy_sections(Linker* l, ObjBuilder* out, RelInputMap* maps,
maps[ii].section[j].delta = delta;
}
}
- if (have_eflags) obj_set_elf_e_flags(out, eflags);
+ if (l->have_elf_e_flags) obj_set_elf_e_flags(out, l->elf_e_flags);
for (ii = 0; ii < ninputs; ++ii) {
LinkInput* in = LinkInputs_at(&l->inputs, ii);
diff --git a/src/link/link_resolve.c b/src/link/link_resolve.c
@@ -503,19 +503,36 @@ static int resolve_elf_loader_owned_undef(Linker* l, LinkSymbol* s) {
Slice nm;
if (!l || !s || !s->name) return 0;
if (!l->emit_pie || l->c->target.obj != KIT_OBJ_ELF) return 0;
- if (l->c->target.arch != KIT_ARCH_X86_64 &&
- l->c->target.arch != KIT_ARCH_RV64 && l->c->target.arch != KIT_ARCH_RV32)
- return 0;
nm = pool_slice(l->c->global, s->name);
- if (!slice_eq_cstr(nm, "__tls_get_addr")) return 0;
- /* Some glibc dynamic TLS helpers are provided by the ELF interpreter itself
- * (ld-linux), not by a regular DT_NEEDED DSO. GNU ld permits this
- * unresolved-looking reference in dynamic executables; model it as a
- * function import with no provider DSO so the PLT/dynsym machinery can emit
- * any needed JUMP_SLOT without adding the interpreter as DT_NEEDED. RISC-V
- * TLS-GD references that relax fully to local-exec will skip their call
- * relocations later, leaving this dynsym entry harmlessly unused. */
- s->kind = SK_FUNC;
+ if (slice_eq_cstr(nm, "__tls_get_addr")) {
+ if (l->c->target.arch != KIT_ARCH_X86_64 &&
+ l->c->target.arch != KIT_ARCH_RV64 &&
+ l->c->target.arch != KIT_ARCH_RV32)
+ return 0;
+ /* Some glibc dynamic TLS helpers are provided by the ELF interpreter
+ * itself (ld-linux), not by a regular DT_NEEDED DSO. GNU ld permits this
+ * unresolved-looking reference in dynamic executables; model it as a
+ * function import with no provider DSO so the PLT/dynsym machinery can
+ * emit any needed JUMP_SLOT without adding the interpreter as DT_NEEDED.
+ * RISC-V TLS-GD references that relax fully to local-exec will skip their
+ * call relocations later, leaving this dynsym entry harmlessly unused. */
+ s->kind = SK_FUNC;
+ } else if (slice_eq_cstr(nm, "__stack_chk_guard")) {
+ /* glibc's AArch64 and RISC-V ABIs expose the process canary from
+ * ld-linux, while libc only carries an undefined versioned reference to
+ * it. Treat the guard as an interpreter-owned object import. This keeps
+ * the loader out of DT_NEEDED (it is already PT_INTERP) and lets the
+ * ordinary GOT/dynsym machinery bind the address at startup. x86-64 reads
+ * its guard directly from fs:0x28 and never reaches this path. */
+ if (l->c->target.os != KIT_OS_LINUX ||
+ (l->c->target.arch != KIT_ARCH_ARM_64 &&
+ l->c->target.arch != KIT_ARCH_RV64 &&
+ l->c->target.arch != KIT_ARCH_RV32))
+ return 0;
+ s->kind = SK_OBJ;
+ } else {
+ return 0;
+ }
s->imported = 1;
s->dso_input_id = LINK_INPUT_NONE;
s->elf_version = 0;
@@ -1196,6 +1213,11 @@ static void include_archive_member(Linker* l, const LinkArchive* ar,
u32 idx;
Sym coff_dll = 0;
if (mem->included) return;
+ if (mem->obj)
+ link_merge_elf_e_flags(
+ l, mem->obj,
+ mem->name ? pool_slice(l->c->global, mem->name)
+ : SLICE_LIT("<unnamed archive member>"));
in = LinkInputs_push(&l->inputs, &idx);
if (!in)
compiler_panic(l->c, SRCLOC_NONE,
diff --git a/src/obj/elf/link.c b/src/obj/elf/link.c
@@ -1411,10 +1411,9 @@ void link_emit_elf(LinkImage* img, Writer* w) {
* mirrors fields onto the undef). Only the canonical
* (first registered) entry is in img->globals. Skip the
* shadow copies. */
- if (!is_local && s->name) {
- LinkSymId canonical = symhash_get(&img->globals, s->name);
- if (canonical != LINK_SYM_NONE && canonical != s->id) continue;
- }
+ if (!is_local && s->name &&
+ !link_symbol_is_canonical_global(img, s))
+ continue;
{
Slice nm_s = s->name ? pool_slice(c->global, s->name) : SLICE_NULL;
nm = nm_s.s ? nm_s.s : "";
@@ -1665,15 +1664,13 @@ void link_emit_elf(LinkImage* img, Writer* w) {
LinkSyms_at(&img->syms, img->entry_sym - 1)->vaddr;
ehdr.e_phoff = ehdr_sz;
ehdr.e_shoff = shdr_off;
- /* e_flags carries the arch ABI bits (RISC-V float-ABI / RVC). This was
- * previously hardcoded 0 for all arches; writing arch->e_flags lands
- * the RV32/RV64 flags. (RV64 descriptor e_flags is unchanged, so its
- * header now reflects RVC|FLOAT_ABI_DOUBLE — see integration notes.) */
+ /* Preserve the linker's psABI merge. RVC/TSO are presence bits and were
+ * ORed across selected inputs; float-ABI/RVE/reserved bits were required to
+ * agree. A synthetic/no-input image falls back to the arch descriptor. */
ehdr.e_flags = arch->e_flags;
- /* rv32: ilp32 and ilp32f share KIT_ARCH_RV32, so the descriptor's float-ABI
- * bits are a placeholder. Override them from -mabi so the executable's ABI
- * matches its objects (and a soft ilp32 image isn't mislabelled single). */
- if (e_machine == EM_RISCV && class32) {
+ if (e_machine == EM_RISCV && img->have_elf_e_flags) {
+ ehdr.e_flags = img->elf_e_flags;
+ } else if (e_machine == EM_RISCV) {
u32 fa = elf_riscv_float_abi_to_e_flags(c->target.float_abi);
ehdr.e_flags = (ehdr.e_flags & ~(u32)EF_RISCV_FLOAT_ABI_MASK) | fa;
}
diff --git a/src/obj/macho/link.c b/src/obj/macho/link.c
@@ -2013,6 +2013,7 @@ static void build_symtab(MCtx* x) {
if (!s->defined) continue;
if (s->bind != SB_GLOBAL && s->bind != SB_WEAK) continue;
if (s->name == 0) continue;
+ if (!link_symbol_is_canonical_global(img, s)) continue;
if (s->kind == SK_ABS) continue; /* skip abs externs */
/* Locate which OutSec contains this vaddr to figure out n_sect.
* n_sect is the 1-based index into the flat section_64 table the
diff --git a/test/link/harness/link_exe_runner.c b/test/link/harness/link_exe_runner.c
@@ -2,6 +2,7 @@
*
* Usage:
* link_exe_runner [--gc-sections] [--entry NAME] [--linker-script <path>]
+ * [--symbols <path>]
* -o <out.exe>
* [--archive [--whole-archive] <lib.a>] <in.o> ...
*
@@ -109,10 +110,27 @@ static int write_exe(const char* path, const uint8_t* data, size_t len) {
return 0;
}
+static int write_data(const char* path, const uint8_t* data, size_t len) {
+ int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ size_t w = 0;
+ if (fd < 0) return -1;
+ while (w < len) {
+ ssize_t k = write(fd, data + w, len - w);
+ if (k <= 0) {
+ close(fd);
+ return -1;
+ }
+ w += (size_t)k;
+ }
+ close(fd);
+ return 0;
+}
+
int main(int argc, char** argv) {
const char* out_path = NULL;
const char* entry_name = "_start";
const char* script_path = NULL;
+ const char* symbols_path = NULL;
int gc_sections = 0;
int next_archive = 0;
int next_whole = 0;
@@ -141,6 +159,8 @@ int main(int argc, char** argv) {
entry_name = argv[++i];
} else if (!strcmp(argv[i], "--linker-script") && i + 1 < argc) {
script_path = argv[++i];
+ } else if (!strcmp(argv[i], "--symbols") && i + 1 < argc) {
+ symbols_path = argv[++i];
} else if (!strcmp(argv[i], "-o") && i + 1 < argc) {
out_path = argv[++i];
} else {
@@ -266,12 +286,26 @@ int main(int argc, char** argv) {
size_t out_len;
const uint8_t* out_bytes = kit_writer_mem_bytes(w, &out_len);
int wrc = write_exe(out_path, out_bytes, out_len);
+ if (wrc == 0 && symbols_path) {
+ KitWriter* sw = NULL;
+ if (kit_writer_mem(&g_heap, &sw) != KIT_OK || !sw ||
+ kit_link_session_write_symbols(link, KIT_LINK_SYMBOLS_NM, sw) !=
+ KIT_OK ||
+ kit_writer_status(sw) != KIT_OK) {
+ wrc = -1;
+ } else {
+ size_t symbols_len = 0;
+ const uint8_t* symbols = kit_writer_mem_bytes(sw, &symbols_len);
+ wrc = write_data(symbols_path, symbols, symbols_len);
+ }
+ if (sw) kit_writer_close(sw);
+ }
kit_link_session_free(link);
if (script) kit_link_script_free(&ctx, script);
kit_writer_close(w);
free_compiler_target(c, kt);
if (wrc) {
- fprintf(stderr, "link-exe-runner: write failed\n");
+ fprintf(stderr, "link-exe-runner: output write failed\n");
return 2;
}
return 0;
diff --git a/test/link/link_compat_test.c b/test/link/link_compat_test.c
@@ -0,0 +1,385 @@
+/* Public LinkSession input-target compatibility coverage. */
+
+#include <kit/core.h>
+#include <kit/link.h>
+#include <kit/object.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "lib/kit_unit.h"
+
+static KitUnit g_u;
+#define EXPECT(c, ...) CU_EXPECT(&g_u, c, __VA_ARGS__)
+
+typedef struct EmittedObject {
+ KitObjBuilder* ob;
+ KitWriter* writer;
+ KitSlice bytes;
+} EmittedObject;
+
+static int emit_empty_object(KitCompiler* c, EmittedObject* out) {
+ size_t len = 0;
+ memset(out, 0, sizeof(*out));
+ if (kit_obj_builder_new(c, &out->ob) != KIT_OK || !out->ob) return 0;
+ if (kit_obj_builder_finalize(out->ob) != KIT_OK) return 0;
+ if (kit_writer_mem(&g_u.heap, &out->writer) != KIT_OK || !out->writer)
+ return 0;
+ if (kit_obj_builder_emit(out->ob, out->writer) != KIT_OK) return 0;
+ out->bytes.data = kit_writer_mem_bytes(out->writer, &len);
+ out->bytes.len = len;
+ return out->bytes.data != NULL && out->bytes.len != 0;
+}
+
+static void emitted_object_fini(EmittedObject* o) {
+ if (o->writer) kit_writer_close(o->writer);
+ if (o->ob) kit_obj_builder_free(o->ob);
+ memset(o, 0, sizeof(*o));
+}
+
+static void ar_field(char* dst, size_t width, const char* value) {
+ size_t n = strlen(value);
+ memset(dst, ' ', width);
+ if (n > width) n = width;
+ memcpy(dst, value, n);
+}
+
+static uint8_t* make_archive(KitSlice member, size_t* len_out) {
+ static const char magic[] = "!<arch>\n";
+ size_t padded = member.len + (member.len & 1u);
+ size_t total = 8u + 60u + padded;
+ uint8_t* bytes = (uint8_t*)malloc(total);
+ char size_buf[32];
+ char* hdr;
+ if (!bytes) return NULL;
+ memset(bytes, 0, total);
+ memcpy(bytes, magic, 8u);
+ hdr = (char*)bytes + 8u;
+ ar_field(hdr + 0u, 16u, "foreign.o/");
+ ar_field(hdr + 16u, 12u, "0");
+ ar_field(hdr + 28u, 6u, "0");
+ ar_field(hdr + 34u, 6u, "0");
+ ar_field(hdr + 40u, 8u, "100644");
+ snprintf(size_buf, sizeof(size_buf), "%lu", (unsigned long)member.len);
+ ar_field(hdr + 48u, 10u, size_buf);
+ hdr[58] = '`';
+ hdr[59] = '\n';
+ memcpy(bytes + 68u, member.data, member.len);
+ if (member.len & 1u) bytes[68u + member.len] = '\n';
+ *len_out = total;
+ return bytes;
+}
+
+static KitLinkSession* new_reloc_session(KitCompiler* c) {
+ KitLinkSessionOptions opts;
+ KitLinkSession* s = NULL;
+ memset(&opts, 0, sizeof(opts));
+ opts.output_kind = KIT_LINK_OUTPUT_RELOCATABLE;
+ if (kit_link_session_new(c, &opts, &s) != KIT_OK) return NULL;
+ return s;
+}
+
+static void check_input_target_boundary(void) {
+ KitCompiler* expected = NULL;
+ KitCompiler* foreign = NULL;
+ EmittedObject x64;
+ EmittedObject aa64;
+ KitLinkSession* s = NULL;
+ KitStatus st;
+ uint8_t* archive = NULL;
+ size_t archive_len = 0;
+
+ memset(&x64, 0, sizeof(x64));
+ memset(&aa64, 0, sizeof(aa64));
+ EXPECT(kit_unit_compiler_new(
+ &g_u, kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF),
+ &expected) == KIT_OK &&
+ expected,
+ "x64 compiler");
+ EXPECT(kit_unit_compiler_new(
+ &g_u, kit_unit_target(KIT_ARCH_ARM_64, KIT_OS_LINUX, KIT_OBJ_ELF),
+ &foreign) == KIT_OK &&
+ foreign,
+ "aarch64 compiler");
+ if (!expected || !foreign) goto done;
+ EXPECT(emit_empty_object(expected, &x64), "emit x64 object");
+ EXPECT(emit_empty_object(foreign, &aa64), "emit aarch64 object");
+ if (!x64.bytes.data || !aa64.bytes.data) goto done;
+
+ s = new_reloc_session(expected);
+ EXPECT(s != NULL, "valid link session");
+ if (s) {
+ EXPECT(kit_link_session_add_obj_bytes(s, KIT_SLICE_LIT("native-x64.o"),
+ &x64.bytes) == KIT_OK,
+ "matching object accepted");
+ kit_link_session_free(s);
+ s = NULL;
+ }
+
+ s = new_reloc_session(expected);
+ EXPECT(s != NULL, "mismatch link session");
+ if (s) {
+ g_u.suppress_fatal = 1;
+ st = kit_link_session_add_obj_bytes(s, KIT_SLICE_LIT("foreign-aa64.o"),
+ &aa64.bytes);
+ g_u.suppress_fatal = 0;
+ EXPECT(st == KIT_ERR, "foreign object rejected at add boundary");
+ EXPECT(strstr(g_u.last_diag, "foreign-aa64.o") != NULL &&
+ strstr(g_u.last_diag, "architecture mismatch") != NULL &&
+ strstr(g_u.last_diag, "expected") != NULL &&
+ strstr(g_u.last_diag, "got") != NULL,
+ "foreign object diagnostic is named and actionable: %s",
+ g_u.last_diag);
+ kit_link_session_free(s);
+ s = NULL;
+ }
+
+ s = new_reloc_session(expected);
+ EXPECT(s != NULL, "DSO mismatch link session");
+ if (s) {
+ g_u.suppress_fatal = 1;
+ st = kit_link_session_add_dso_bytes(s, KIT_SLICE_LIT("foreign-aa64.so"),
+ &aa64.bytes);
+ g_u.suppress_fatal = 0;
+ EXPECT(st == KIT_ERR, "foreign DSO/import input rejected at add boundary");
+ EXPECT(strstr(g_u.last_diag, "foreign-aa64.so") != NULL &&
+ strstr(g_u.last_diag, "architecture mismatch") != NULL,
+ "DSO diagnostic names the incompatible input: %s", g_u.last_diag);
+ kit_link_session_free(s);
+ s = NULL;
+ }
+
+ archive = make_archive(aa64.bytes, &archive_len);
+ EXPECT(archive != NULL, "foreign archive fixture");
+ s = new_reloc_session(expected);
+ EXPECT(s != NULL, "archive link session");
+ if (archive && s) {
+ KitLinkArchiveInput input;
+ memset(&input, 0, sizeof(input));
+ input.name = KIT_SLICE_LIT("libforeign.a");
+ input.bytes.data = archive;
+ input.bytes.len = archive_len;
+ g_u.suppress_fatal = 1;
+ st = kit_link_session_add_archive_bytes(s, &input);
+ g_u.suppress_fatal = 0;
+ EXPECT(st == KIT_ERR, "foreign archive member rejected before selection");
+ EXPECT(strstr(g_u.last_diag, "libforeign.a(foreign.o)") != NULL &&
+ strstr(g_u.last_diag, "architecture mismatch") != NULL,
+ "archive diagnostic names container and member: %s", g_u.last_diag);
+ }
+ if (s) {
+ kit_link_session_free(s);
+ s = NULL;
+ }
+
+ s = new_reloc_session(expected);
+ EXPECT(s != NULL, "builder link session");
+ if (s) {
+ g_u.suppress_fatal = 1;
+ st = kit_link_session_add_obj(s, aa64.ob);
+ g_u.suppress_fatal = 0;
+ EXPECT(st == KIT_ERR, "foreign in-memory builder rejected");
+ EXPECT(strstr(g_u.last_diag, "<in-memory object>") != NULL &&
+ strstr(g_u.last_diag, "architecture mismatch") != NULL,
+ "in-memory diagnostic names input: %s", g_u.last_diag);
+ }
+
+done:
+ if (s) kit_link_session_free(s);
+ free(archive);
+ emitted_object_fini(&aa64);
+ emitted_object_fini(&x64);
+ if (foreign) kit_compiler_free(foreign);
+ if (expected) kit_compiler_free(expected);
+}
+
+static void check_format_boundary(KitOSKind os, KitObjFmt obj,
+ const char* label) {
+ KitCompiler* expected = NULL;
+ KitCompiler* foreign = NULL;
+ EmittedObject native;
+ EmittedObject other;
+ KitLinkSession* s = NULL;
+ KitStatus st;
+ uint8_t* archive = NULL;
+ size_t archive_len = 0;
+
+ memset(&native, 0, sizeof(native));
+ memset(&other, 0, sizeof(other));
+ EXPECT(kit_unit_compiler_new(
+ &g_u, kit_unit_target(KIT_ARCH_X86_64, os, obj), &expected) ==
+ KIT_OK &&
+ expected,
+ "%s x64 compiler", label);
+ EXPECT(kit_unit_compiler_new(
+ &g_u, kit_unit_target(KIT_ARCH_ARM_64, os, obj), &foreign) ==
+ KIT_OK &&
+ foreign,
+ "%s aarch64 compiler", label);
+ if (!expected || !foreign) goto done;
+ EXPECT(emit_empty_object(expected, &native), "%s emit x64 object", label);
+ EXPECT(emit_empty_object(foreign, &other), "%s emit aarch64 object", label);
+ if (!native.bytes.data || !other.bytes.data) goto done;
+
+ s = new_reloc_session(expected);
+ EXPECT(s != NULL, "%s raw mismatch session", label);
+ if (s) {
+ g_u.suppress_fatal = 1;
+ st = kit_link_session_add_obj_bytes(s, kit_slice_cstr(label), &other.bytes);
+ g_u.suppress_fatal = 0;
+ EXPECT(st == KIT_ERR, "%s foreign raw object rejected", label);
+ EXPECT(strstr(g_u.last_diag, label) != NULL &&
+ strstr(g_u.last_diag, "architecture mismatch") != NULL,
+ "%s raw diagnostic names expected/actual input: %s", label,
+ g_u.last_diag);
+ kit_link_session_free(s);
+ s = NULL;
+ }
+
+ archive = make_archive(other.bytes, &archive_len);
+ EXPECT(archive != NULL, "%s archive fixture", label);
+ s = new_reloc_session(expected);
+ EXPECT(s != NULL, "%s archive mismatch session", label);
+ if (archive && s) {
+ KitLinkArchiveInput input;
+ memset(&input, 0, sizeof(input));
+ input.name = kit_slice_cstr(label);
+ input.bytes.data = archive;
+ input.bytes.len = archive_len;
+ g_u.suppress_fatal = 1;
+ st = kit_link_session_add_archive_bytes(s, &input);
+ g_u.suppress_fatal = 0;
+ EXPECT(st == KIT_ERR, "%s foreign archive member rejected", label);
+ EXPECT(strstr(g_u.last_diag, label) != NULL &&
+ strstr(g_u.last_diag, "foreign.o") != NULL,
+ "%s archive diagnostic names container/member: %s", label,
+ g_u.last_diag);
+ }
+ if (s) {
+ kit_link_session_free(s);
+ s = NULL;
+ }
+
+ s = new_reloc_session(expected);
+ EXPECT(s != NULL, "%s builder mismatch session", label);
+ if (s) {
+ g_u.suppress_fatal = 1;
+ st = kit_link_session_add_obj(s, other.ob);
+ g_u.suppress_fatal = 0;
+ EXPECT(st == KIT_ERR, "%s foreign in-memory builder rejected", label);
+ }
+
+done:
+ if (s) kit_link_session_free(s);
+ free(archive);
+ emitted_object_fini(&other);
+ emitted_object_fini(&native);
+ if (foreign) kit_compiler_free(foreign);
+ if (expected) kit_compiler_free(expected);
+}
+
+static void check_platform_and_format_rules(void) {
+ KitCompiler* linux = NULL;
+ KitCompiler* freebsd = NULL;
+ KitCompiler* macho = NULL;
+ KitCompiler* android = NULL;
+ EmittedObject linux_obj;
+ EmittedObject freebsd_obj;
+ EmittedObject macho_obj;
+ KitLinkSession* s = NULL;
+ KitStatus st;
+
+ memset(&linux_obj, 0, sizeof(linux_obj));
+ memset(&freebsd_obj, 0, sizeof(freebsd_obj));
+ memset(&macho_obj, 0, sizeof(macho_obj));
+ EXPECT(kit_unit_compiler_new(
+ &g_u,
+ kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF),
+ &linux) == KIT_OK &&
+ linux,
+ "platform-rule Linux compiler");
+ EXPECT(kit_unit_compiler_new(
+ &g_u,
+ kit_unit_target(KIT_ARCH_X86_64, KIT_OS_FREEBSD, KIT_OBJ_ELF),
+ &freebsd) == KIT_OK &&
+ freebsd,
+ "platform-rule FreeBSD compiler");
+ EXPECT(kit_unit_compiler_new(
+ &g_u,
+ kit_unit_target(KIT_ARCH_X86_64, KIT_OS_MACOS, KIT_OBJ_MACHO),
+ &macho) == KIT_OK &&
+ macho,
+ "format-rule Mach-O compiler");
+ EXPECT(kit_unit_compiler_new(
+ &g_u,
+ kit_unit_target(KIT_ARCH_X86_64, KIT_OS_ANDROID, KIT_OBJ_ELF),
+ &android) == KIT_OK &&
+ android,
+ "platform-rule Android compiler");
+ if (!linux || !freebsd || !macho || !android) goto done;
+ EXPECT(emit_empty_object(linux, &linux_obj), "emit generic Linux ELF");
+ EXPECT(emit_empty_object(freebsd, &freebsd_obj), "emit FreeBSD ELF");
+ EXPECT(emit_empty_object(macho, &macho_obj), "emit x64 Mach-O");
+ if (!linux_obj.bytes.data || !freebsd_obj.bytes.data ||
+ !macho_obj.bytes.data)
+ goto done;
+
+ s = new_reloc_session(linux);
+ EXPECT(s != NULL, "platform mismatch session");
+ if (s) {
+ g_u.suppress_fatal = 1;
+ st = kit_link_session_add_obj_bytes(s, KIT_SLICE_LIT("freebsd-x64.o"),
+ &freebsd_obj.bytes);
+ g_u.suppress_fatal = 0;
+ EXPECT(st == KIT_ERR, "foreign operating-system ELF rejected");
+ EXPECT(strstr(g_u.last_diag, "operating system mismatch") != NULL,
+ "platform mismatch diagnostic: %s", g_u.last_diag);
+ kit_link_session_free(s);
+ s = NULL;
+ }
+
+ s = new_reloc_session(linux);
+ EXPECT(s != NULL, "format mismatch session");
+ if (s) {
+ g_u.suppress_fatal = 1;
+ st = kit_link_session_add_obj_bytes(s, KIT_SLICE_LIT("foreign-x64.o"),
+ &macho_obj.bytes);
+ g_u.suppress_fatal = 0;
+ EXPECT(st == KIT_ERR, "foreign object format rejected");
+ EXPECT(strstr(g_u.last_diag, "object format mismatch") != NULL,
+ "format mismatch diagnostic: %s", g_u.last_diag);
+ kit_link_session_free(s);
+ s = NULL;
+ }
+
+ /* Generic EI_OSABI_NONE/Linux relocatables are valid inputs to Android;
+ * this is an explicit directional rule, not general OS borrowing. */
+ s = new_reloc_session(android);
+ EXPECT(s != NULL, "Android generic-ELF session");
+ if (s) {
+ EXPECT(kit_link_session_add_obj_bytes(s, KIT_SLICE_LIT("generic-linux.o"),
+ &linux_obj.bytes) == KIT_OK,
+ "Android accepts a generic/Linux ELF relocatable");
+ }
+
+done:
+ if (s) kit_link_session_free(s);
+ emitted_object_fini(&macho_obj);
+ emitted_object_fini(&freebsd_obj);
+ emitted_object_fini(&linux_obj);
+ if (android) kit_compiler_free(android);
+ if (macho) kit_compiler_free(macho);
+ if (freebsd) kit_compiler_free(freebsd);
+ if (linux) kit_compiler_free(linux);
+}
+
+int main(void) {
+ kit_unit_init(&g_u);
+ check_input_target_boundary();
+ check_format_boundary(KIT_OS_MACOS, KIT_OBJ_MACHO, "foreign-macho.a");
+ check_format_boundary(KIT_OS_WINDOWS, KIT_OBJ_COFF, "foreign-coff.a");
+ check_platform_and_format_rules();
+ kit_unit_summary(&g_u, "link_compat_test");
+ return kit_unit_status(&g_u);
+}
diff --git a/test/link/macho-symbols.sh b/test/link/macho-symbols.sh
@@ -0,0 +1,81 @@
+#!/bin/sh
+# Verify that resolved per-input reference slots do not become duplicate
+# external definitions in a linked Mach-O LC_SYMTAB or linker symbol report.
+# Distinct global aliases at the same address must remain distinct.
+
+set -eu
+
+root=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)
+runner=${LINK_EXE_RUNNER:-$root/build/test/link-exe-runner}
+kit=${KIT:-$root/build/kit}
+cc=${CC:-clang}
+nm_bin=$(command -v llvm-nm 2>/dev/null || command -v nm 2>/dev/null || true)
+work=$root/build/test/macho-symbols
+src=$root/test/link/macho_symbols
+
+[ -x "$runner" ] || {
+ printf 'macho-symbols: missing runner: %s\n' "$runner" >&2
+ exit 1
+}
+[ -x "$kit" ] || {
+ printf 'macho-symbols: missing kit binary: %s\n' "$kit" >&2
+ exit 1
+}
+[ -n "$nm_bin" ] || {
+ printf 'macho-symbols: no platform nm oracle\n' >&2
+ exit 1
+}
+
+rm -rf "$work"
+mkdir -p "$work"
+
+defined_count() {
+ awk -v sym="$2" '
+ $NF == sym && $(NF - 1) != "U" { ++n }
+ END { print n + 0 }
+ ' "$1"
+}
+
+check_one() {
+ arch=$1
+ triple=$2
+ out=$work/$arch
+ mkdir -p "$out"
+
+ for base in definition reference_a reference_b; do
+ "$cc" --target="$triple" -O0 -ffreestanding -fno-stack-protector \
+ -fno-PIC -fno-pie -c "$src/$base.c" -o "$out/$base.o" \
+ >"$out/compile-$base.stdout" 2>"$out/compile-$base.stderr"
+ done
+ "$cc" --target="$triple" -c "$src/aliases.S" -o "$out/aliases.o" \
+ >"$out/compile-aliases.stdout" 2>"$out/compile-aliases.stderr"
+
+ KIT_TEST_ARCH=$arch KIT_TEST_OBJ=macho "$runner" \
+ --entry test_main --symbols "$out/symbols.txt" -o "$out/linked.macho" \
+ "$out/definition.o" "$out/reference_a.o" "$out/reference_b.o" \
+ "$out/aliases.o" >"$out/link.stdout" 2>"$out/link.stderr"
+ "$nm_bin" "$out/linked.macho" >"$out/platform-nm.txt" \
+ 2>"$out/platform-nm.stderr"
+ "$kit" nm "$out/linked.macho" >"$out/kit-nm.txt" \
+ 2>"$out/kit-nm.stderr"
+
+ for table in "$out/platform-nm.txt" "$out/kit-nm.txt" \
+ "$out/symbols.txt"; do
+ [ "$(defined_count "$table" _audit_macho_provided)" -eq 1 ] || {
+ printf 'macho-symbols: %s: duplicate canonical definition in %s\n' \
+ "$arch" "$table" >&2
+ exit 1
+ }
+ for alias in _audit_macho_alias_a _audit_macho_alias_b; do
+ [ "$(defined_count "$table" "$alias")" -eq 1 ] || {
+ printf 'macho-symbols: %s: missing distinct alias %s in %s\n' \
+ "$arch" "$alias" "$table" >&2
+ exit 1
+ }
+ done
+ done
+ printf 'ok macho canonical symbols %s\n' "$arch"
+}
+
+check_one aa64 arm64-apple-macos
+check_one x64 x86_64-apple-macos
diff --git a/test/link/macho_symbols/aliases.S b/test/link/macho_symbols/aliases.S
@@ -0,0 +1,7 @@
+.section __DATA,__data
+.globl _audit_macho_alias_a
+.globl _audit_macho_alias_b
+.p2align 2
+_audit_macho_alias_a:
+_audit_macho_alias_b:
+.long 7
diff --git a/test/link/macho_symbols/definition.c b/test/link/macho_symbols/definition.c
@@ -0,0 +1 @@
+int audit_macho_provided(void) { return 19; }
diff --git a/test/link/macho_symbols/reference_a.c b/test/link/macho_symbols/reference_a.c
@@ -0,0 +1,9 @@
+extern int audit_macho_provided(void);
+extern int audit_macho_second_reference(void);
+
+int test_main(void) {
+ return audit_macho_provided() == 19 &&
+ audit_macho_second_reference() == 19
+ ? 0
+ : 1;
+}
diff --git a/test/link/macho_symbols/reference_b.c b/test/link/macho_symbols/reference_b.c
@@ -0,0 +1,3 @@
+extern int audit_macho_provided(void);
+
+int audit_macho_second_reference(void) { return audit_macho_provided(); }