yald is a linker.
The cache (src/core/cache.c) memoizes whole links: on each run, yald hashes
every resolved input file's content (a 128-bit non-cryptographic mix, not a
cryptographic hash -- collisions are not a security concern for a local build
cache) and combines it with the relevant flags into one key. If a prior run
already produced the requested output file for that exact key, and the file
on disk still has the size/mtime recorded for that run, yald skips parsing,
resolution, layout, and writing entirely.
File modification time is used only as a fast pre-check before re-hashing a given input: if a file's path, size, and mtime match what was recorded last time, its previously computed hash is reused instead of re-reading and re-hashing its contents. If any of those differ, the content is re-hashed, so a file rewritten with identical bytes (mtime bumped, content unchanged) still correctly hits the cache, and any real content change always invalidates it, regardless of mtime.
This is whole-link memoization, not fine-grained incremental linking: a single changed input still causes a full re-parse and re-link, just with no wasted work when nothing changed. Finer-grained reuse (skipping re-parsing of individual unchanged objects on a partial rebuild) is a natural next step but is not implemented in v0.1.
The cache directory defaults to $XDG_CACHE_HOME/yald or ~/.cache/yald,
and can be disabled per-run with --no-cache.
The thread pool (src/core/threadpool.c) is a fixed-size worker pool over a
bounded ring-buffer work queue, built on C11/C23 <threads.h> rather than
raw pthread: the standard library's thrd_t/mtx_t/cnd_t map directly
onto pthreads on Linux/glibc, and using them keeps the code free of
platform-specific threading calls without adding a dependency. Section
parsing, relocation application, and output writing are all dispatched
through yald_parallel_for(), which blocks until every submitted unit of
work has completed.
Requires a C23 compiler (tested with GCC 16 and Clang 22) and Meson >= 1.1.
meson setup build
meson compile -C build
meson test -C build
This produces the yald executable, libyald.so, and libyald.a in build/.
Install headers/binaries the usual way with meson install -C build.
yald input.o other.o -o output -e main --gc-sections --icf=safe
To make GCC or Clang use it, either pass a directory containing a program
named ld (or ld.bfd) via -B:
mkdir -p /tmp/yald-bin && ln -s /path/to/build/yald /tmp/yald-bin/ld
gcc -B/tmp/yald-bin -static hello.c -o hello
or, on toolchains that accept an arbitrary path for -fuse-ld= (recent
Clang does; whether a given GCC build does depends on how it was configured):
clang -fuse-ld=/path/to/build/yald hello.c -o hello
-o, -l, -L, -shared, -static, -pie, -no-pie, --dynamic-linker,
-soname/-h, -rpath/-R, --gc-sections/--no-gc-sections,
-e/--entry, --icf=safe|all|none, -z noexecstack|execstack|relro|norelro,
--version-script, -Bstatic/-Bdynamic/-Bshareable,
--start-group/--end-group (accepted, no-op -- see "Architecture" above),
@response-file, --threads=N, --no-cache, -v/-V/--version.
Anything else is accepted and ignored with a warning on stderr rather than
causing a hard failure, since GCC and Clang pass a long tail of flags
(--hash-style=, --build-id, -plugin-opt=..., and similar) that do not
change what yald needs to do for the cases it supports. A handful of flags
that are recognized only to consume their following argument correctly
(-m, -T, -plugin, -plugin-opt, -Map, -y, -R) are listed
explicitly in src/cli/args.c.
#include <yald/yald.h>
const char *inputs[] = {"a.o", "b.o"};
yald_link_options opts = {
.output_path = "a.out",
.input_paths = inputs,
.input_count = 2,
.entry_symbol = "_start",
.gc_sections = true,
};
yald_link_result result = yald_link(&opts);
if (result.status != YALD_OK) {
fprintf(stderr, "%s\n", result.error_message);
}include/yald/yald.h is the entire public surface; no internal struct
(yald_object, yald_section, and so on) is exposed across the library
boundary.
- Thread Local Storage.
R_X86_64_TPOFF32,R_X86_64_GOTTPOFF, and the general/local-dynamic TLS relocations are not implemented; a section using them is linked with a warning and the relocation left unpatched. This means linking against a stock glibc (which uses__threadinternally forerrno, locale state, and more) will produce a binary that crashes at startup. Freestanding programs, and libraries that do not rely on thread-locals, are unaffected. - IFUNC resolution.
R_X86_64_IRELATIVEand the__rela_iplt_start/__rela_iplt_endbounds glibc's static startup uses to run ifunc resolvers are stubbed out as an empty range rather than actually processed. Combined with the TLS gap above, static linking against glibc is not currently usable; this is the main piece of unfinished work before that changes. - Dynamic linking (
-shared,-pie, non-static default linking) is scaffolded but incomplete. yald parses a shared library's.dynsymto avoid reporting its exports as unresolved, and can emit an ELF withET_DYNset, but does not yet emitPT_DYNAMIC,.dynsym/.dynstrfor the output, real PLT stubs, orDT_NEEDEDentries. A binary produced with-sharedor without-staticwill not run correctly today. Use-staticfor anything you need to actually execute. - No retained
.symtab/debug sections in output. yald currently emits only the sections needed to run the program (plus a real section header table for tool compatibility) and does not carry a.symtab,.strtab, or debug info into the output, similar to always linking as if-swere given. Preserving debug sections is straightforward future work; it was left out to keep the output-writing path simple for v0.1. - ODR checking is a heuristic, not a real check. The only signal used is
a size mismatch between differently-sized definitions of the same weak
symbol name across translation units. It catches gross mismatches (e.g. an
inline function's definition disagreeing on struct layout) but nothing
that requires actually comparing code or type information, which would
need compiler cooperation (e.g.
-fno-semantic-interposition-style type metadata) that plain object files do not carry. - ICF's "safe" mode heuristic. A function is excluded from folding in
--icf=safeif its address is taken anywhere through a non-PC-relative relocation (R_X86_64_64,R_X86_64_32,R_X86_64_32S,R_X86_64_PC64). This is the same idea lld's--icf=safeuses, but is not a proof of safety in every case a sufficiently adversarial program could construct;--icf=allfolds regardless and is only as safe as your program's tolerance for merged function pointers. - Architecture: x86-64 only. The section/symbol data model and the format
registry in
src/elf/format.care written to make adding aarch64 (or a second object format) a matter of adding a backend, but no second backend exists yet. - Linker scripts (
-T) are accepted syntactically (their filename argument is consumed) but never read; anything relying on placement or symbol definitions from a custom linker script will not get them from yald.
GPL-3.0-only. The exact license text is in LICENSE. SPDX headers in
source files read GPL-3.0-only.