kit

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

commit 3cd976f9a92c23fee94ad15a98803084674e8990
parent 624912a1e292fe0c39d22645e67f0358ea742a02
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Mon, 25 May 2026 19:51:21 -0700

bootstrap: avoid post-link strip on release binaries

Use linker -S for release and bootstrap-release links so Mach-O outputs are signed after their final layout rather than stripped after signing.

Teach cc/ld to accept -S/--strip-debug and thread the option through the linker session, and tighten Mach-O LINKEDIT/signature layout so bootstrapped release binaries verify and compare.

Diffstat:
MMakefile | 10+++-------
Mdriver/cc.c | 10++++++++++
Mdriver/ld.c | 7+++++++
Minclude/cfree/link.h | 1+
Msrc/api/link.c | 2++
Msrc/link/link.c | 7+++++++
Msrc/link/link.h | 1+
Msrc/link/link_internal.h | 1+
Msrc/obj/macho/link.c | 87++++++++++++++++++++++++++++++++++---------------------------------------------
9 files changed, 69 insertions(+), 57 deletions(-)

diff --git a/Makefile b/Makefile @@ -1,7 +1,6 @@ CC = clang AR = ar LD = ld -STRIP = strip BUILD_DIR ?= build SYSROOT = $(shell xcrun --show-sdk-path) RELEASE ?= 0 @@ -13,9 +12,9 @@ HOST_OPTFLAGS ?= -O2 HOST_MODE_CPPFLAGS = -DNDEBUG HOST_MODE_CFLAGS = -ffunction-sections -fdata-sections ifeq ($(HOST_UNAME),Darwin) -HOST_MODE_LDFLAGS = -Wl,-dead_strip +HOST_MODE_LDFLAGS = -Wl,-dead_strip -Wl,-S else -HOST_MODE_LDFLAGS = -Wl,--gc-sections +HOST_MODE_LDFLAGS = -Wl,--gc-sections -Wl,-S endif else HOST_OPTFLAGS ?= -O0 @@ -389,9 +388,6 @@ $(LIB_AR): $(LIB_RELOC_OBJ) $(BIN): $(DRIVER_OBJS) $(LIB_AR) $(BUILD_CONFIG) $(CC) $(HOST_LDFLAGS) -o $@ $(DRIVER_OBJS) $(LIB_AR) -ifeq ($(RELEASE),1) - $(STRIP) $@ -endif $(BUILD_DIR)/lib/%.o: src/%.c Makefile $(BUILD_CONFIG) @mkdir -p $(dir $@) @@ -456,7 +452,7 @@ BOOTSTRAP_MAKEFILES = Makefile mk/config.mk rt/Makefile ifeq ($(RELEASE),1) $(BOOTSTRAP_STAMP): HOST_OPTFLAGS = -O1 $(BOOTSTRAP_STAMP): BOOTSTRAP_HOST_MODE_CFLAGS = $(HOST_MODE_CFLAGS) -$(BOOTSTRAP_STAMP): BOOTSTRAP_HOST_MODE_LDFLAGS = -Wl,--gc-sections +$(BOOTSTRAP_STAMP): BOOTSTRAP_HOST_MODE_LDFLAGS = -Wl,--gc-sections -Wl,-S endif bootstrap: bootstrap-debug bootstrap-release diff --git a/driver/cc.c b/driver/cc.c @@ -214,6 +214,7 @@ typedef struct CcOptions { int no_defaultlibs; int no_startfiles; int wants_hosted_libc; + int strip_debug; DriverHostedPlan hosted; } CcOptions; @@ -493,6 +494,14 @@ static int cc_record_wl(CcOptions* o, const char* arg) { o->gc_sections = 0; continue; } + if (n == 2 && driver_strneq(tok, "-S", 2)) { + o->strip_debug = 1; + continue; + } + if (n == 13 && driver_strneq(tok, "--strip-debug", 13)) { + o->strip_debug = 1; + continue; + } if (n >= 11 && driver_strneq(tok, "--build-id=", 11)) { char* buf = cc_dup_span(o->env, tok + 11, n - 11); int rc; @@ -2587,6 +2596,7 @@ static int cc_run_link_exe(DriverEnv* env, const CcOptions* o, lopts.build_id_bytes = o->build_id_bytes; lopts.build_id_len = o->build_id_len; lopts.gc_sections = o->gc_sections; + lopts.strip_debug = o->strip_debug; lopts.pie = o->pie; lopts.pe_subsystem = o->pe_subsystem; lopts.interp_path = cfree_slice_cstr(o->interp_path); diff --git a/driver/ld.c b/driver/ld.c @@ -109,6 +109,7 @@ typedef struct LdOptions { int new_dtags; /* 1=DT_RUNPATH (default), 0=DT_RPATH */ int export_dynamic; /* -E / --export-dynamic */ int gc_sections; /* --gc-sections / --no-gc-sections */ + int strip_debug; /* -S / --strip-debug */ int allow_undefined; /* shared output undefined-symbol policy */ /* --build-id state */ @@ -207,6 +208,7 @@ void driver_help_ld(void) { " --no-gc-sections Disable section GC (default)\n" " -E, --export-dynamic Promote defined globals into dynsym\n" " (no-op for -shared; recorded for exe)\n" + " -S, --strip-debug Omit debug info from linked output\n" " --no-undefined Reject unresolved symbols in -shared " "output\n" " -z defs Same as --no-undefined\n" @@ -834,6 +836,10 @@ static int ld_parse(int argc, char** argv, LdOptions* o) { o->gc_sections = 0; continue; } + if (driver_streq(a, "-S") || driver_streq(a, "--strip-debug")) { + o->strip_debug = 1; + continue; + } if (driver_streq(a, "-E") || driver_streq(a, "--export-dynamic")) { o->export_dynamic = 1; continue; @@ -1201,6 +1207,7 @@ static int ld_run_link(LdOptions* o) { lopts.build_id_bytes = o->build_id_bytes; lopts.build_id_len = o->build_id_len; lopts.gc_sections = o->gc_sections; + lopts.strip_debug = o->strip_debug; lopts.pie = o->pie; lopts.pe_subsystem = o->pe_subsystem; lopts.interp_path = cfree_slice_cstr(o->interp_path); diff --git a/include/cfree/link.h b/include/cfree/link.h @@ -162,6 +162,7 @@ typedef enum CfreePeSubsystem { typedef struct CfreeLinkSessionOptions { uint8_t output_kind; /* CfreeLinkOutputKind */ bool gc_sections; + bool strip_debug; bool pie; uint16_t pe_subsystem; /* CfreePeSubsystem; 0 => target default */ CfreeSlice interp_path; diff --git a/src/api/link.c b/src/api/link.c @@ -103,11 +103,13 @@ CfreeStatus cfree_link_session_new(CfreeCompiler* c, case CFREE_LINK_OUTPUT_EXE: link_set_emit_static_exe(l, 1); link_set_gc_sections(l, opts->gc_sections); + link_set_strip_debug(l, opts->strip_debug); link_set_pie(l, opts->pie); link_set_interp_path(l, opts->interp_path); break; case CFREE_LINK_OUTPUT_SHARED: link_set_gc_sections(l, opts->gc_sections); + link_set_strip_debug(l, opts->strip_debug); link_set_pie(l, 1); (void)opts->soname; (void)opts->rpaths; diff --git a/src/link/link.c b/src/link/link.c @@ -362,6 +362,13 @@ void link_set_gc_sections(Linker* l, int enable) { * pass 0 unconditionally and we don't want to noise that. */ } +void link_set_strip_debug(Linker* l, int enable) { + if (!l) return; + l->strip_debug = enable; + /* Executable layouts already omit non-alloc debug sections. Keep the flag + * recorded so driver -S/--strip-debug flows through the linker surface. */ +} + void link_set_emit_static_exe(Linker* l, int enable) { if (!l) return; l->emit_static_exe = enable ? 1 : 0; diff --git a/src/link/link.h b/src/link/link.h @@ -179,6 +179,7 @@ void link_set_extern_resolver(Linker*, LinkExternResolver, void* user); * symbols (shared link), and any section flagged KEEP by the linker * script. Unreferenced sections are dropped from the output. */ void link_set_gc_sections(Linker*, int enable); +void link_set_strip_debug(Linker*, int enable); /* Mark this link as targeting a static ET_EXEC ELF binary (vs. the * in-process JIT). Setter is called by cfree_link_exe; the JIT path diff --git a/src/link/link_internal.h b/src/link/link_internal.h @@ -165,6 +165,7 @@ struct Linker { * script and every sub-object must outlive link_resolve. */ const CfreeLinkScript* script; int gc_sections; + int strip_debug; /* Set by cfree_link_exe before link_resolve. When 1, layout_iplt * synthesizes a .init_array entry pointing at __cfree_ifunc_init so * the emitted ET_EXEC binary fills its IFUNC slots at startup. The diff --git a/src/obj/macho/link.c b/src/obj/macho/link.c @@ -1815,6 +1815,15 @@ static void uleb128(MByte* out, u64 v) { } while (v); } +static u32 uleb128_size(u64 v) { + u32 n = 0; + do { + ++n; + v >>= 7; + } while (v); + return n; +} + static void build_exports_trie(MCtx* x) { /* Format: * node = (terminal_size: uleb128) (export_data)? (children_count: u8) @@ -1851,61 +1860,35 @@ static void build_exports_trie(MCtx* x) { /* Compute leaf-node bytes length: uleb(flags=0) + uleb(offset). */ u32 flags = 0; - u32 leaf_payload_len; - { - /* count uleb bytes for flags=0 -> 1 byte */ - u32 a = 1; - /* count uleb bytes for entry_off */ - u32 b = 0; - u64 v = entry_off; - do { - ++b; - v >>= 7; - } while (v); - leaf_payload_len = a + b; - } + u32 leaf_payload_len = uleb128_size(flags) + uleb128_size(entry_off); /* Layout: root node first, then leaf. The root node's child entry * carries the absolute offset of the leaf within the trie. */ /* root: terminal_size=0, children_count=1, "_main"\0, child_offset= - * (leaf-position uleb). */ - /* We'll back-patch child_offset after we know the leaf position. */ + * (leaf-position uleb). + * + * The child offset's own ULEB width contributes to the leaf position, so + * solve for the fixed point before emitting. */ + u32 leaf_pos = 2u + (u32)nl + 1u + 1u; + for (;;) { + u32 n = uleb128_size(leaf_pos); + u32 next = 2u + (u32)nl + 1u + n; + if (next == leaf_pos) break; + leaf_pos = next; + } + mbuf_u8(out, 0); /* root terminal size */ mbuf_u8(out, 1); /* children_count */ mbuf_str(out, nm, (u32)nl); - /* child offset: 5 bytes max for uleb128(u32). Reserve and patch. */ - u32 child_off_pos = out->len; - /* Reserve 5 bytes. */ - for (u32 i = 0; i < 5; ++i) mbuf_u8(out, 0); + uleb128(out, leaf_pos); /* leaf node */ - u32 leaf_pos = out->len; + if (out->len != leaf_pos) + compiler_panic(x->c, no_loc(), "macho: exports trie leaf offset mismatch"); /* terminal_size byte then payload */ mbuf_u8(out, (u8)leaf_payload_len); uleb128(out, flags); uleb128(out, entry_off); mbuf_u8(out, 0); /* children_count */ - - /* Patch child_offset uleb. */ - u32 v = leaf_pos; - for (u32 i = 0; i < 5; ++i) { - u8 b = (u8)(v & 0x7fu); - v >>= 7; - if (v) b |= 0x80u; - out->data[child_off_pos + i] = b; - if (!v && i < 4) { - /* Remaining bytes need to be 0x00 — but we already wrote zeros; - * we need a continuation-zero so the consumer sees 5 bytes. Set - * top bit on lower bytes to indicate continuation, last byte = 0. */ - /* Actually: ULEB needs proper termination. Force final byte to - * 0 with no continuation by setting bit-7=0 on the last - * non-zero byte and also forcing remaining bytes to be 0x80 - * extension or trim. Simpler: set last byte explicitly. */ - out->data[child_off_pos + i] = (u8)(out->data[child_off_pos + i] & 0x7fu); - for (u32 j = i + 1; j < 5; ++j) out->data[child_off_pos + j] = 0x80; - out->data[child_off_pos + 4] = 0x00; - break; - } - } /* Pad trie to 8 bytes. */ mbuf_align(out, 8); } @@ -1968,6 +1951,7 @@ static void build_symtab(MCtx* x) { break; } } + if (n_sect == 0) continue; Slice nm_s = pool_slice(x->c->global, s->name); const char* nm = nm_s.s; size_t nl = nm_s.len; @@ -2060,8 +2044,11 @@ static void build_symtab(MCtx* x) { */ static void layout_linkedit(MCtx* x) { - /* fn_starts and data_in_code are both empty. */ + /* LC_FUNCTION_STARTS is a ULEB128 stream terminated by a zero byte. Keep a + * real empty table here so tools that rewrite LINKEDIT preserve the + * canonical blob order between exports and the symbol table. */ mbuf_init(&x->fn_starts, x->h); + mbuf_u8(&x->fn_starts, 0); mbuf_init(&x->data_in_code, x->h); mbuf_init(&x->codesig, x->h); @@ -2070,13 +2057,13 @@ static void layout_linkedit(MCtx* x) { cur = ALIGN_UP(cur, 8u); x->chained_fixups_off = (u32)cur; cur += x->chained_fixups.len; - /* exports trie */ - cur = ALIGN_UP(cur, 8u); + /* exports trie. Keep LINKEDIT data blobs contiguous; Apple strip rejects + * padding between chained fixups and the exports trie. */ x->exports_trie_off = (u32)cur; cur += x->exports_trie.len; - /* function starts (empty placeholder, but allocate one byte) */ - cur = ALIGN_UP(cur, 8u); + /* function starts */ x->fn_starts_off = (u32)cur; + cur += x->fn_starts.len; /* data in code */ cur = ALIGN_UP(cur, 8u); x->data_in_code_off = (u32)cur; @@ -2467,11 +2454,11 @@ void link_emit_macho(LinkImage* img, Writer* w) { while (lc.len - cmd_start < cmd_size) mbuf_u8(&lc, 0); } - /* LC_FUNCTION_STARTS / LC_DATA_IN_CODE — empty. */ + /* LC_FUNCTION_STARTS / LC_DATA_IN_CODE */ mbuf_u32(&lc, LC_FUNCTION_STARTS_C); mbuf_u32(&lc, 16); mbuf_u32(&lc, x.fn_starts_off); - mbuf_u32(&lc, 0); + mbuf_u32(&lc, x.fn_starts.len); mbuf_u32(&lc, LC_DATA_IN_CODE_C); mbuf_u32(&lc, 16); @@ -2566,7 +2553,7 @@ void link_emit_macho(LinkImage* img, Writer* w) { while (file.len < x.exports_trie_off) mbuf_u8(&file, 0); mbuf_append(&file, x.exports_trie.data, x.exports_trie.len); while (file.len < x.fn_starts_off) mbuf_u8(&file, 0); - /* fn_starts is empty */ + mbuf_append(&file, x.fn_starts.data, x.fn_starts.len); while (file.len < x.data_in_code_off) mbuf_u8(&file, 0); /* empty */ while (file.len < x.symtab_off) mbuf_u8(&file, 0);