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:
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);