commit 65f74c2d6c1c5d3c32063cd1818d7cb40ca3215d
parent e364e60ef0f3fab4cc3ca9a074933888e414d543
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 19 May 2026 22:28:30 -0700
Implement relocatable ld links
Diffstat:
8 files changed, 605 insertions(+), 19 deletions(-)
diff --git a/driver/ld.c b/driver/ld.c
@@ -1,18 +1,17 @@
+#include <cfree/core.h>
+#include <cfree/link.h>
+#include <cfree/object.h>
#include <stdint.h>
#include "driver.h"
#include "lib_resolve.h"
-#include <cfree/core.h>
-#include <cfree/link.h>
-#include <cfree/object.h>
-
-/* `cfree ld` — link object/archive inputs into an executable or shared
- * library. The driver loads each input via env.file_io, optionally parses a
- * `-T` linker script into the structured form, and calls cfree_link_exe or
- * cfree_link_shared directly so the full link surface (per-archive flags,
- * group cycling, build-id, soname/rpath/exports) is reachable without
- * thickening the convenience CfreeOptions struct.
+/* `cfree ld` — link object/archive inputs into an executable, shared
+ * library, or relocatable object. The driver loads each input via
+ * env.file_io, optionally parses a `-T` linker script into the structured
+ * form, and calls the public link APIs directly so the full link surface
+ * (per-archive flags, group cycling, build-id, soname/rpath/exports) is
+ * reachable without thickening the convenience CfreeOptions struct.
*
* Supported flags:
* -o out output path (required, exactly one)
@@ -22,6 +21,7 @@
* -l name resolves to lib<name>.a via -L
* -static / -pie / -no-pie target.pic
* -shared emit a shared library / dylib
+ * -r / --relocatable emit a relocatable partial-link object
* -soname NAME DT_SONAME / LC_ID_DYLIB
* -rpath DIR DT_RPATH/DT_RUNPATH entry (repeatable)
* -rpath-link DIR shared-lib search at link time
@@ -98,6 +98,7 @@ typedef struct LdOptions {
/* Shared-library output state. */
int shared; /* -shared */
+ int relocatable; /* -r / --relocatable */
const char* soname; /* -soname NAME */
const char** rpaths; /* -rpath DIR (repeatable) */
uint32_t nrpaths;
@@ -134,6 +135,7 @@ void driver_help_ld(void) {
"USAGE\n"
" cfree ld -o OUT [options] inputs.o ... inputs.a ...\n"
" cfree ld -shared -o libfoo.so [options] inputs.o ...\n"
+ " cfree ld -r -o partial.o inputs.o ...\n"
"\n"
"DESCRIPTION\n"
" Loads each input via host file I/O, optionally parses a -T linker\n"
@@ -146,6 +148,7 @@ void driver_help_ld(void) {
" -shared Emit a position-independent shared library "
"/\n"
" dylib instead of an executable\n"
+ " -r, --relocatable Emit a relocatable partial-link object\n"
"\n"
"ENTRY / SCRIPT\n"
" -e SYMBOL Entry symbol\n"
@@ -212,8 +215,8 @@ static int ld_alloc_arrays(LdOptions* o, int argc) {
o->lib_dirs = driver_alloc_zeroed(o->env, bound * sizeof(*o->lib_dirs));
o->rpaths = driver_alloc_zeroed(o->env, bound * sizeof(*o->rpaths));
o->rpath_links = driver_alloc_zeroed(o->env, bound * sizeof(*o->rpath_links));
- if (!o->object_files || !o->archives || !o->dsos || !o->order || !o->lib_dirs ||
- !o->rpaths || !o->rpath_links) {
+ if (!o->object_files || !o->archives || !o->dsos || !o->order ||
+ !o->lib_dirs || !o->rpaths || !o->rpath_links) {
driver_errf(LD_TOOL, "out of memory");
return 1;
}
@@ -467,6 +470,10 @@ static int ld_parse(int argc, char** argv, LdOptions* o) {
if (o->target.pic == CFREE_PIC_NONE) o->target.pic = CFREE_PIC_PIC;
continue;
}
+ if (driver_streq(a, "-r") || driver_streq(a, "--relocatable")) {
+ o->relocatable = 1;
+ continue;
+ }
if (driver_streq(a, "-soname")) {
if (++i >= argc) {
driver_errf(LD_TOOL, "-soname requires an argument");
@@ -608,6 +615,32 @@ static int ld_parse(int argc, char** argv, LdOptions* o) {
ld_usage();
return 1;
}
+ if (o->relocatable) {
+ if (o->shared) {
+ driver_errf(LD_TOOL, "-r and -shared are incompatible");
+ return 1;
+ }
+ if (o->pie || o->target.pic == CFREE_PIC_PIE) {
+ driver_errf(LD_TOOL, "-r and -pie are incompatible");
+ return 1;
+ }
+ if (o->interp_path) {
+ driver_errf(LD_TOOL, "-dynamic-linker requires executable output");
+ return 1;
+ }
+ if (o->soname || o->nrpaths || o->nrpath_links) {
+ driver_errf(LD_TOOL, "-soname/-rpath/-rpath-link require -shared");
+ return 1;
+ }
+ if (o->ndsos) {
+ driver_errf(LD_TOOL, "-r does not support shared-object inputs");
+ return 1;
+ }
+ if (o->script_path) {
+ driver_errf(LD_TOOL, "-r does not support linker scripts yet");
+ return 1;
+ }
+ }
return 0;
}
@@ -693,6 +726,7 @@ static int ld_run_link(LdOptions* o) {
CfreeLinkInputs inputs;
CfreeExeLinkOptions link_opts;
CfreeSharedLinkOptions shared_opts;
+ CfreeRelocatableLinkOptions reloc_opts;
uint32_t i;
int rc = 1;
@@ -822,7 +856,13 @@ static int ld_run_link(LdOptions* o) {
inputs.build_id_bytes = o->build_id_bytes;
inputs.build_id_len = o->build_id_len;
- if (o->shared) {
+ if (o->relocatable) {
+ CfreeRelocatableLinkOptions zero = {0};
+ reloc_opts = zero;
+ reloc_opts.inputs = inputs;
+ rc = cfree_link_relocatable(compiler, &reloc_opts, writer) == CFREE_OK ? 0
+ : 1;
+ } else if (o->shared) {
CfreeSharedLinkOptions zero = {0};
shared_opts = zero;
shared_opts.inputs = inputs;
@@ -866,10 +906,9 @@ out:
/* Match compiler/linker drivers: successful link outputs get executable
* file modes while still respecting the process umask. Done after closing
* the writer so the bits are stable on disk. */
- if (rc == 0 && o->output_path) {
+ if (rc == 0 && o->output_path && !o->relocatable) {
if (driver_mark_executable_output(o->output_path) != 0) {
- driver_errf(LD_TOOL, "failed to set executable mode: %s",
- o->output_path);
+ driver_errf(LD_TOOL, "failed to set executable mode: %s", o->output_path);
rc = 1;
}
}
diff --git a/include/cfree/link.h b/include/cfree/link.h
@@ -182,6 +182,10 @@ typedef struct CfreeSharedLinkOptions {
bool gc_sections;
} CfreeSharedLinkOptions;
+typedef struct CfreeRelocatableLinkOptions {
+ CfreeLinkInputs inputs;
+} CfreeRelocatableLinkOptions;
+
typedef struct CfreeJitLinkOptions {
CfreeLinkInputs inputs;
bool gc_sections;
@@ -193,6 +197,9 @@ CfreeStatus cfree_link_exe(CfreeCompiler *, const CfreeExeLinkOptions *,
CfreeWriter *out);
CfreeStatus cfree_link_shared(CfreeCompiler *, const CfreeSharedLinkOptions *,
CfreeWriter *out);
+CfreeStatus cfree_link_relocatable(CfreeCompiler *,
+ const CfreeRelocatableLinkOptions *,
+ CfreeWriter *out);
CfreeStatus cfree_link_jit(CfreeCompiler *, const CfreeJitLinkOptions *,
const CfreeJitHost *, CfreeJit **out_jit);
diff --git a/src/api/link.c b/src/api/link.c
@@ -55,9 +55,8 @@ static void load_inputs(Linker* l, const CfreeLinkInputs* in) {
compiler_panic(l->c, no_loc(),
"link: ordered archive input index out of range");
a = &in->archives[ord->index];
- link_add_archive_bytes(l, a->bytes.name, a->bytes.data,
- a->bytes.len, a->whole_archive,
- a->link_mode, a->group_id);
+ link_add_archive_bytes(l, a->bytes.name, a->bytes.data, a->bytes.len,
+ a->whole_archive, a->link_mode, a->group_id);
break;
}
case CFREE_LINK_INPUT_DSO: {
@@ -179,6 +178,30 @@ CfreeStatus cfree_link_shared(CfreeCompiler* c,
return CFREE_OK;
}
+CfreeStatus cfree_link_relocatable(CfreeCompiler* c,
+ const CfreeRelocatableLinkOptions* opts,
+ CfreeWriter* out) {
+ PanicSave saved;
+ Linker* l;
+ if (!c || !opts || !out) return CFREE_INVALID;
+ compiler_panic_save(c, &saved);
+ if (setjmp(c->panic)) {
+ compiler_run_cleanups(c);
+ compiler_panic_restore(c, &saved);
+ return CFREE_ERR;
+ }
+ l = link_new(c);
+ if (!l) {
+ compiler_panic_restore(c, &saved);
+ return CFREE_NOMEM;
+ }
+ load_inputs(l, &opts->inputs);
+ link_emit_relocatable_writer(l, out);
+ link_free(l);
+ compiler_panic_restore(c, &saved);
+ return CFREE_OK;
+}
+
CfreeStatus cfree_link_jit(CfreeCompiler* c, const CfreeJitLinkOptions* opts,
const CfreeJitHost* host, CfreeJit** out_jit) {
PanicSave saved;
diff --git a/src/link/link.h b/src/link/link.h
@@ -249,6 +249,12 @@ const LinkRelocApply* link_reloc_apply_get(LinkImage*, u32 id);
* caller-provided Writer. Path-based emit lives in the driver. */
void link_emit_image_writer(LinkImage*, Writer*);
+/* Writes an ET_REL / MH_OBJECT relocatable partial-link output. This consumes
+ * the Linker's object/archive inputs and emits a fresh ObjBuilder through the
+ * active object-format writer; it does not perform executable layout, section
+ * GC, entry resolution, GOT/IPLT synthesis, or DSO binding. */
+void link_emit_relocatable_writer(Linker*, Writer*);
+
/* JIT: maps the image into executable memory and returns an owning handle.
* The returned CfreeJit takes ownership of the LinkImage (undefers it from
* the cleanup stack registered by link_resolve); on cfree_jit_free both the
diff --git a/src/link/link_relocatable.c b/src/link/link_relocatable.c
@@ -0,0 +1,459 @@
+/* Relocatable partial linking (`ld -r`).
+ *
+ * This path deliberately builds a fresh ObjBuilder instead of routing through
+ * LinkImage. LinkImage is an executable/JIT image model: it drops non-alloc
+ * sections, resolves strong undefs, synthesizes boundary/GOT/IPLT state, and
+ * assigns final vaddrs. A relocatable output must preserve object-file
+ * structure and leave unresolved externals as relocatable symbol references.
+ */
+
+#include <string.h>
+
+#include "core/buf.h"
+#include "core/heap.h"
+#include "core/pool.h"
+#include "core/vec.h"
+#include "link/link.h"
+#include "link/link_internal.h"
+
+static SrcLoc no_loc(void) {
+ SrcLoc l = {0, 0, 0};
+ return l;
+}
+
+typedef struct RelObjSecMap {
+ ObjSecId out_sec;
+ u32 delta;
+} RelObjSecMap;
+
+typedef struct RelInputMap {
+ ObjSymId* sym;
+ u32 nsym;
+ RelObjSecMap* section;
+ u32 nsection;
+} RelInputMap;
+
+typedef struct RelGlobal {
+ Sym name;
+ u8 has_def;
+ u8 has_undef;
+ u8 undef_bind;
+ u8 pad0;
+ u32 def_input_idx;
+ ObjSymId def_sym;
+ u64 common_size;
+ u64 common_align;
+ ObjSymId out_sym;
+} RelGlobal;
+
+static int rel_bind_strength(u16 bind) {
+ switch (bind) {
+ case SB_GLOBAL:
+ return 3;
+ case SB_WEAK:
+ return 2;
+ case SB_LOCAL:
+ return 1;
+ default:
+ return 0;
+ }
+}
+
+static int rel_sym_is_def(const ObjSym* s) {
+ return s && s->kind != SK_UNDEF &&
+ (s->kind == SK_ABS || s->kind == SK_COMMON || s->kind == SK_FILE ||
+ s->section_id != OBJ_SEC_NONE);
+}
+
+static int rel_sym_is_spurious_undef(const ObjSym* s) {
+ return s && s->section_id == OBJ_SEC_NONE && s->kind != SK_ABS &&
+ s->kind != SK_COMMON && !s->referenced &&
+ (s->bind == SB_GLOBAL || s->bind == SB_WEAK);
+}
+
+static u32 rel_symbol_count(ObjBuilder* ob) {
+ ObjSymIter* it;
+ ObjSymEntry e;
+ u32 n = 0;
+ it = obj_symiter_new(ob);
+ while (obj_symiter_next(it, &e)) ++n;
+ obj_symiter_free(it);
+ return n;
+}
+
+static const ObjSym* rel_input_sym(Linker* l, u32 input_idx, ObjSymId id) {
+ LinkInput* in;
+ if (input_idx >= LinkInputs_count(&l->inputs)) return NULL;
+ in = LinkInputs_at(&l->inputs, input_idx);
+ return in && in->obj ? obj_symbol_get(in->obj, id) : NULL;
+}
+
+static void rel_maps_free(Heap* h, RelInputMap* maps, u32 ninputs) {
+ u32 i;
+ if (!maps) return;
+ for (i = 0; i < ninputs; ++i) {
+ if (maps[i].sym)
+ h->free(h, maps[i].sym, sizeof(*maps[i].sym) * maps[i].nsym);
+ if (maps[i].section)
+ h->free(h, maps[i].section, sizeof(*maps[i].section) * maps[i].nsection);
+ }
+ h->free(h, maps, sizeof(*maps) * ninputs);
+}
+
+static void rel_globals_free(Heap* h, RelGlobal* globals, u32 cap,
+ SymHash* by_name) {
+ if (by_name) symhash_fini(by_name);
+ if (globals) h->free(h, globals, sizeof(*globals) * cap);
+}
+
+static RelGlobal* rel_global_for(Linker* l, RelGlobal** globals, u32* nglobals,
+ u32* cap, SymHash* by_name, Sym name) {
+ LinkSymId hit = symhash_get(by_name, name);
+ RelGlobal* g;
+ if (hit != LINK_SYM_NONE) return &(*globals)[hit - 1u];
+ if (VEC_GROW(l->heap, *globals, *cap, *nglobals + 1u))
+ compiler_panic(l->c, no_loc(), "link -r: oom on global symbols");
+ g = &(*globals)[*nglobals];
+ memset(g, 0, sizeof(*g));
+ g->name = name;
+ (*nglobals)++;
+ symhash_set(by_name, name, (LinkSymId)*nglobals);
+ return g;
+}
+
+static void rel_record_global(Linker* l, RelGlobal** globals, u32* nglobals,
+ u32* cap, SymHash* by_name, u32 input_idx,
+ ObjSymId id, const ObjSym* s) {
+ RelGlobal* g;
+ if (!s || s->name == 0 || s->bind == SB_LOCAL) return;
+ if (rel_sym_is_spurious_undef(s)) return;
+ g = rel_global_for(l, globals, nglobals, cap, by_name, s->name);
+ if (!rel_sym_is_def(s)) {
+ if (!g->has_undef ||
+ rel_bind_strength(s->bind) > rel_bind_strength(g->undef_bind)) {
+ g->undef_bind = (u8)s->bind;
+ }
+ g->has_undef = 1u;
+ return;
+ }
+
+ if (!g->has_def) {
+ g->has_def = 1u;
+ g->def_input_idx = input_idx;
+ g->def_sym = id;
+ if (s->kind == SK_COMMON) {
+ g->common_size = s->size;
+ g->common_align = s->common_align;
+ }
+ return;
+ }
+
+ {
+ const ObjSym* prev = rel_input_sym(l, g->def_input_idx, g->def_sym);
+ int old_strength = prev ? rel_bind_strength(prev->bind) : 0;
+ int new_strength = rel_bind_strength(s->bind);
+ if (prev && prev->kind == SK_COMMON && s->kind == SK_COMMON) {
+ if (s->size > g->common_size) g->common_size = s->size;
+ if (s->common_align > g->common_align) g->common_align = s->common_align;
+ if (new_strength > old_strength) {
+ g->def_input_idx = input_idx;
+ g->def_sym = id;
+ }
+ } else if (s->kind == SK_COMMON) {
+ return;
+ } else if (prev && prev->kind == SK_COMMON) {
+ g->def_input_idx = input_idx;
+ g->def_sym = id;
+ g->common_size = 0;
+ g->common_align = 0;
+ } else if (new_strength > old_strength) {
+ g->def_input_idx = input_idx;
+ g->def_sym = id;
+ } else if (new_strength == old_strength &&
+ new_strength == rel_bind_strength(SB_GLOBAL)) {
+ size_t namelen;
+ const char* nm = pool_str(l->c->global, s->name, &namelen);
+ obj_format_demangle_c(l->c, &nm, &namelen);
+ compiler_panic(l->c, no_loc(),
+ "link -r: duplicate definition of global symbol '%.*s'",
+ (int)namelen, nm);
+ }
+ }
+}
+
+static ObjSymId rel_copy_symbol(Linker* l, ObjBuilder* out,
+ const RelInputMap* maps, u32 input_idx,
+ const ObjSym* s, int force_common,
+ u64 common_size, u64 common_align) {
+ ObjSecId sec = OBJ_SEC_NONE;
+ u64 value = s->value;
+ u64 align = s->common_align;
+ ObjSymId id;
+ if (!force_common && s->section_id != OBJ_SEC_NONE &&
+ s->section_id < maps[input_idx].nsection) {
+ const RelObjSecMap* sm = &maps[input_idx].section[s->section_id];
+ sec = sm->out_sec;
+ value += sm->delta;
+ }
+ if (force_common) {
+ value = 0;
+ align = common_align;
+ }
+ id = obj_symbol_ex(out, s->name, (SymBind)s->bind, (SymVis)s->vis,
+ force_common ? SK_COMMON : (SymKind)s->kind, sec, value,
+ force_common ? common_size : s->size, align);
+ if (id == OBJ_SYM_NONE)
+ compiler_panic(l->c, no_loc(), "link -r: oom copying symbol");
+ if (s->flags) obj_symbol_set_flags(out, id, s->flags);
+ obj_sym_mark_referenced(out, id);
+ return id;
+}
+
+static ObjSymId rel_global_out(Linker* l, ObjBuilder* out, RelInputMap* maps,
+ RelGlobal* g) {
+ const ObjSym* s;
+ if (g->out_sym != OBJ_SYM_NONE) return g->out_sym;
+ if (g->has_def) {
+ s = rel_input_sym(l, g->def_input_idx, g->def_sym);
+ if (!s) compiler_panic(l->c, no_loc(), "link -r: bad global symbol map");
+ g->out_sym =
+ rel_copy_symbol(l, out, maps, g->def_input_idx, s, s->kind == SK_COMMON,
+ g->common_size, g->common_align);
+ } else {
+ ObjSymId id;
+ id = obj_symbol(out, g->name,
+ g->has_undef ? (SymBind)g->undef_bind : SB_GLOBAL, SK_UNDEF,
+ OBJ_SEC_NONE, 0, 0);
+ if (id == OBJ_SYM_NONE)
+ compiler_panic(l->c, no_loc(), "link -r: oom creating undef symbol");
+ obj_sym_mark_referenced(out, id);
+ g->out_sym = id;
+ }
+ return g->out_sym;
+}
+
+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, no_loc(), "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, no_loc(), "link -r: incompatible ELF e_flags");
+ }
+ }
+ nsec = obj_section_count(ob);
+ maps[ii].nsection = nsec;
+ maps[ii].section = (RelObjSecMap*)l->heap->alloc(
+ l->heap, sizeof(*maps[ii].section) * nsec, _Alignof(RelObjSecMap));
+ if (!maps[ii].section)
+ compiler_panic(l->c, no_loc(), "link -r: oom on section map");
+ memset(maps[ii].section, 0, sizeof(*maps[ii].section) * nsec);
+ for (j = 1; j < nsec; ++j) {
+ const Section* s = obj_section_get(ob, (ObjSecId)j);
+ ObjSecId out_sec;
+ if (!s) continue;
+ out_sec =
+ obj_section_ex(out, s->name, (SecKind)s->kind, (SecSem)s->sem,
+ s->flags, s->align, s->entsize, OBJ_SEC_NONE, s->info);
+ if (out_sec == OBJ_SEC_NONE)
+ compiler_panic(l->c, no_loc(), "link -r: oom copying section");
+ if (s->ext_kind != OBJ_EXT_NONE || s->ext_type || s->ext_flags)
+ obj_section_set_ext(out, out_sec, (ObjExtKind)s->ext_kind, s->ext_type,
+ s->ext_flags);
+ if (s->sem == SSEM_NOBITS || s->kind == SEC_BSS) {
+ obj_reserve_bss(out, out_sec, s->bss_size, s->align);
+ } else if (s->bytes.total) {
+ u8* flat = (u8*)l->heap->alloc(l->heap, s->bytes.total, 1u);
+ if (!flat)
+ compiler_panic(l->c, no_loc(), "link -r: oom copying section bytes");
+ buf_flatten(&s->bytes, flat);
+ obj_write(out, out_sec, flat, s->bytes.total);
+ l->heap->free(l->heap, flat, s->bytes.total);
+ }
+ maps[ii].section[j].out_sec = out_sec;
+ maps[ii].section[j].delta = 0;
+ }
+ }
+ if (have_eflags) obj_set_elf_e_flags(out, eflags);
+
+ for (ii = 0; ii < ninputs; ++ii) {
+ LinkInput* in = LinkInputs_at(&l->inputs, ii);
+ ObjBuilder* ob = in->obj;
+ if (!ob) continue;
+ for (j = 1; j < maps[ii].nsection; ++j) {
+ const Section* s = obj_section_get(ob, (ObjSecId)j);
+ ObjSecId link = OBJ_SEC_NONE;
+ ObjSecId out_sec = maps[ii].section[j].out_sec;
+ if (!s || out_sec == OBJ_SEC_NONE) continue;
+ if (s->link != OBJ_SEC_NONE && s->link < maps[ii].nsection)
+ link = maps[ii].section[s->link].out_sec;
+ obj_section_set_link_info(out, out_sec, link, s->info);
+ }
+ }
+}
+
+static void rel_scan_globals(Linker* l, RelGlobal** globals, u32* nglobals,
+ u32* cap, SymHash* by_name) {
+ u32 ii;
+ for (ii = 0; ii < LinkInputs_count(&l->inputs); ++ii) {
+ LinkInput* in = LinkInputs_at(&l->inputs, ii);
+ ObjSymIter* it;
+ ObjSymEntry e;
+ if (!in->obj || in->kind == LINK_INPUT_DSO_BYTES) continue;
+ it = obj_symiter_new(in->obj);
+ while (obj_symiter_next(it, &e))
+ rel_record_global(l, globals, nglobals, cap, by_name, ii, e.id, e.sym);
+ obj_symiter_free(it);
+ }
+}
+
+static void rel_copy_symbols(Linker* l, ObjBuilder* out, RelInputMap* maps,
+ SymHash* globals_by_name, RelGlobal* globals) {
+ u32 ii;
+ for (ii = 0; ii < LinkInputs_count(&l->inputs); ++ii) {
+ LinkInput* in = LinkInputs_at(&l->inputs, ii);
+ ObjSymIter* it;
+ ObjSymEntry e;
+ u32 nsym;
+ if (!in->obj || in->kind == LINK_INPUT_DSO_BYTES) continue;
+ nsym = rel_symbol_count(in->obj) + 1u;
+ maps[ii].nsym = nsym;
+ maps[ii].sym = (ObjSymId*)l->heap->alloc(
+ l->heap, sizeof(*maps[ii].sym) * nsym, _Alignof(ObjSymId));
+ if (!maps[ii].sym)
+ compiler_panic(l->c, no_loc(), "link -r: oom on symbol map");
+ memset(maps[ii].sym, 0, sizeof(*maps[ii].sym) * nsym);
+ it = obj_symiter_new(in->obj);
+ while (obj_symiter_next(it, &e)) {
+ const ObjSym* s = e.sym;
+ if (e.id >= nsym) continue;
+ if (rel_sym_is_spurious_undef(s)) continue;
+ if (s->name != 0 && s->bind != SB_LOCAL) {
+ LinkSymId rec_idx = symhash_get(globals_by_name, s->name);
+ if (rec_idx != LINK_SYM_NONE)
+ maps[ii].sym[e.id] =
+ rel_global_out(l, out, maps, &globals[rec_idx - 1u]);
+ } else {
+ maps[ii].sym[e.id] = rel_copy_symbol(l, out, maps, ii, s, 0, 0, 0);
+ }
+ }
+ obj_symiter_free(it);
+ }
+}
+
+static void rel_copy_groups(Linker* l, ObjBuilder* out, RelInputMap* maps) {
+ u32 ii, gi, j;
+ for (ii = 0; ii < LinkInputs_count(&l->inputs); ++ii) {
+ LinkInput* in = LinkInputs_at(&l->inputs, ii);
+ ObjBuilder* ob = in->obj;
+ if (!ob || in->kind == LINK_INPUT_DSO_BYTES) continue;
+ for (gi = 1; gi < obj_group_count(ob); ++gi) {
+ const ObjGroup* g = obj_group_get(ob, (ObjGroupId)gi);
+ ObjSymId sig = OBJ_SYM_NONE;
+ ObjGroupId out_gid;
+ if (!g) continue;
+ if (g->signature != OBJ_SYM_NONE && g->signature < maps[ii].nsym)
+ sig = maps[ii].sym[g->signature];
+ out_gid = obj_group(out, g->name, sig, g->flags);
+ if (out_gid == OBJ_GROUP_NONE)
+ compiler_panic(l->c, no_loc(), "link -r: oom copying group");
+ for (j = 0; j < g->nsections; ++j) {
+ ObjSecId in_sec = g->sections[j];
+ ObjSecId out_sec = OBJ_SEC_NONE;
+ if (in_sec != OBJ_SEC_NONE && in_sec < maps[ii].nsection)
+ out_sec = maps[ii].section[in_sec].out_sec;
+ if (out_sec != OBJ_SEC_NONE) {
+ obj_group_add_section(out, out_gid, out_sec);
+ obj_section_set_group(out, out_sec, out_gid);
+ }
+ }
+ }
+ }
+}
+
+static void rel_copy_relocs(Linker* l, ObjBuilder* out, RelInputMap* maps) {
+ u32 ii, ri;
+ for (ii = 0; ii < LinkInputs_count(&l->inputs); ++ii) {
+ LinkInput* in = LinkInputs_at(&l->inputs, ii);
+ ObjBuilder* ob = in->obj;
+ u32 total;
+ if (!ob || in->kind == LINK_INPUT_DSO_BYTES) continue;
+ total = obj_reloc_total(ob);
+ for (ri = 0; ri < total; ++ri) {
+ const Reloc* r = obj_reloc_at(ob, ri);
+ ObjSecId out_sec;
+ ObjSymId out_sym = OBJ_SYM_NONE;
+ u32 out_off;
+ if (!r || r->section_id == OBJ_SEC_NONE ||
+ r->section_id >= maps[ii].nsection)
+ continue;
+ out_sec = maps[ii].section[r->section_id].out_sec;
+ if (out_sec == OBJ_SEC_NONE) continue;
+ if (r->sym != OBJ_SYM_NONE && r->sym < maps[ii].nsym)
+ out_sym = maps[ii].sym[r->sym];
+ out_off = r->offset + maps[ii].section[r->section_id].delta;
+ obj_reloc_ex(out, out_sec, out_off, (RelocKind)r->kind, out_sym,
+ r->addend, r->has_explicit_addend, r->pair);
+ }
+ }
+}
+
+static void rel_obj_cleanup(void* arg) { obj_free((ObjBuilder*)arg); }
+
+void link_emit_relocatable_writer(Linker* l, Writer* w) {
+ Heap* h;
+ ObjBuilder* out;
+ CompilerCleanup* cleanup;
+ RelInputMap* maps;
+ u32 ninputs;
+ RelGlobal* globals = NULL;
+ u32 nglobals = 0, globals_cap = 0;
+ SymHash globals_by_name;
+
+ if (!l || !w) return;
+ h = l->heap;
+
+ if (l->script)
+ compiler_panic(l->c, no_loc(),
+ "link -r: linker scripts are not supported");
+
+ link_ingest_archives(l);
+ ninputs = LinkInputs_count(&l->inputs);
+ maps = ninputs ? (RelInputMap*)h->alloc(h, sizeof(*maps) * ninputs,
+ _Alignof(RelInputMap))
+ : NULL;
+ if (ninputs && !maps)
+ compiler_panic(l->c, no_loc(), "link -r: oom on input maps");
+ if (ninputs) memset(maps, 0, sizeof(*maps) * ninputs);
+
+ out = obj_new(l->c);
+ if (!out) compiler_panic(l->c, no_loc(), "link -r: obj_new failed");
+ cleanup = compiler_defer(l->c, rel_obj_cleanup, out);
+
+ symhash_init(&globals_by_name, h);
+ rel_copy_sections(l, out, maps, ninputs);
+ rel_scan_globals(l, &globals, &nglobals, &globals_cap, &globals_by_name);
+ rel_copy_symbols(l, out, maps, &globals_by_name, globals);
+ rel_copy_groups(l, out, maps);
+ rel_copy_relocs(l, out, maps);
+
+ obj_finalize(out);
+ cfree_obj_builder_emit(out, w);
+
+ if (cleanup) compiler_undefer(l->c, cleanup);
+ obj_free(out);
+ rel_globals_free(h, globals, globals_cap, &globals_by_name);
+ rel_maps_free(h, maps, ninputs);
+}
diff --git a/src/obj/obj.c b/src/obj/obj.c
@@ -188,6 +188,16 @@ void obj_section_set_group(ObjBuilder* ob, ObjSecId id, ObjGroupId gid) {
if (s && id != OBJ_SEC_NONE) s->group_id = gid;
}
+void obj_section_set_link_info(ObjBuilder* ob, ObjSecId id, ObjSecId link,
+ u32 info) {
+ Section* s;
+ if (id == OBJ_SEC_NONE) return;
+ s = Sections_at(&ob->sections, id);
+ if (!s) return;
+ s->link = link;
+ s->info = info;
+}
+
void obj_section_set_ext(ObjBuilder* ob, ObjSecId id, ObjExtKind ek,
u32 ext_type, u32 ext_flags) {
Section* s;
diff --git a/src/obj/obj.h b/src/obj/obj.h
@@ -330,6 +330,7 @@ ObjSecId obj_section_ex(ObjBuilder*, Sym name, SecKind, SecSem, u16 flags,
void obj_section_set_flags(ObjBuilder*, ObjSecId, u16 flags);
void obj_section_set_align(ObjBuilder*, ObjSecId, u32 align);
void obj_section_set_group(ObjBuilder*, ObjSecId, ObjGroupId);
+void obj_section_set_link_info(ObjBuilder*, ObjSecId, ObjSecId link, u32 info);
/* Set format-specific raw sh_type/sh_flags overrides (see Section.ext_type
* comment). Zero ext_type means "no override". */
void obj_section_set_ext(ObjBuilder*, ObjSecId, ObjExtKind, u32 ext_type,
diff --git a/test/driver/run.sh b/test/driver/run.sh
@@ -92,6 +92,47 @@ else
fail=$((fail + 1))
fi
+cat > "$work/partial-main.c" <<'SRC'
+int foo(void);
+int _start(void) { return foo(); }
+SRC
+cat > "$work/partial-foo.c" <<'SRC'
+int foo(void) { return 0; }
+SRC
+
+if "$CFREE" cc -target x86_64-linux -c "$work/partial-main.c" \
+ -o "$work/partial-main.o" > "$work/partial-main.out" \
+ 2> "$work/partial-main.err" &&
+ "$CFREE" cc -target x86_64-linux -c "$work/partial-foo.c" \
+ -o "$work/partial-foo.o" > "$work/partial-foo.out" \
+ 2> "$work/partial-foo.err"; then
+ : > "$work/partial.o"
+ chmod 0644 "$work/partial.o"
+ if (umask 077; "$CFREE" ld -r "$work/partial-main.o" \
+ "$work/partial-foo.o" -o "$work/partial.o") \
+ > "$work/ld-r.out" 2> "$work/ld-r.err"; then
+ check_mode "ld-r-output-mode" "$work/partial.o" 644
+ if "$CFREE" ld "$work/partial.o" -o "$work/partial-exe" \
+ > "$work/ld-r-final.out" 2> "$work/ld-r-final.err"; then
+ printf 'PASS %s\n' "ld-r-final-link"
+ pass=$((pass + 1))
+ else
+ printf 'FAIL %s (final link of partial object failed)\n' \
+ "ld-r-final-link"
+ sed 's/^/ | /' "$work/ld-r-final.err"
+ fail=$((fail + 1))
+ fi
+ else
+ printf 'FAIL ld-r-output-mode (cfree ld -r failed)\n'
+ sed 's/^/ | /' "$work/ld-r.err"
+ fail=$((fail + 1))
+ fi
+else
+ printf 'FAIL ld-r-output-mode (setup compile failed)\n'
+ sed 's/^/ | /' "$work/partial-main.err" "$work/partial-foo.err"
+ fail=$((fail + 1))
+fi
+
rm -f "$work/main.o"
if (cd "$work" && "$CFREE" cc -c main.c) \
> "$work/cc-c-default.out" 2> "$work/cc-c-default.err"; then