The ARM64 Mach-O backend has never actually supported thread-local
storage._Thread_localand__threadparse fine (VT_TLSgets set like
you'd expect), butarm64_sym()'s TLS branch was emitting the ELF/Linux
sequence -mrs xr, tpidr_el0plusR_AARCH64_TLSLE_ADD_TPREL_* relocs-
which Mach-O's linker has never implemented. Those relocations quietly
resolve to nothing, so any access ends up reading or writing through
whatever happens to be inTPIDR_EL0, which isn't a valid per-thread
base on Darwin at all. Depending on what garbage offset comes out of
that, you get a crash or silent corruption.

macOS does this completely differently. Each thread-local global gets
a 3-word descriptor - {thunk, reserved, offset} - and accessing the
variable means callingthunk(&descriptor); dyld patches that thunk
pointer at image load time to return this thread's copy. This patch
implements that path for real:

tccgen.credirects a defined TLS global's address from its backing
storage to a synthesized descriptor living in a new
__DATA,__thread_varssection, once it's defined.arm64-gen.c's
arm64_sym()then does the Mach-O-specific thing on that branch: load
the descriptor's address and call through its first field instead of
doing theELF TPIDRdance. Andtccmacho.cpicks up three pieces of
object-file plumbing this needs that just weren't there before -
correct Mach-O section types for the descriptor and its backing data,
theMH_HAS_TLV_DESCRIPTORSheader flag (dyld silently skips descriptor
patching without it, which took a while to track down since nothing
about it fails loudly), and a correctedminos- it was hardcoded to
10.6, which predates both Apple Silicon and TLV support, and dyld
gates the whole patching mechanism on it.

Thatthunkcall is a realAAPCS64call, and it can clobberx0-x17.
That matters because those registers might be holding something TCC
still cares about - most directly,store()'s value-to-store register,
which is captured as a plainintbefore this code ever runs and never
gets re-read afterward. `g_tls = some_call();` hits this exactly:
whatever's inx0from the call gets stomped by theTLVthunkbefore
it's stored. I originally protected against this with a manual
save/restore ofx0-x17around the call. On review, the suggestion was
to usesave_regs()instead, which is the normalTCCidiom for this
kind of thing - so I tried it, and it doesn't work. Disassembling
`_Thread_local int g_x; g_x = 41;` with asave_regs()-only version
shows why:save_regs()correctly spills the41to a stack temp before
the call, updating TCC's own bookkeeping for that value - butstore()
already has its own copy of the register number, captured before
save_regs()ever runs, and it just reads straight from that register
afterward regardless of whatTCC's bookkeeping now says. The register
holds thethunkcall's leftovers by then, not41. I keptsave_regs()
in the final version anyway, alongside the manual save/restore - it's
free, and it correctly covers ordinaryTCC-tracked values including
FP/SIMDones that the manualx0-x17save never touched, so there's
no reason not to have both.

Two other things came up during review and testing that the first
version of this patch didn't handle: initializedTLS globals, and
linking across separate object files. Both turned out to need real
fixes, not just relaxing a restriction.

For initializedglobals, the immediate problem was that.tdataand
.tbssare separate sections, and the offset written into each
descriptor was relative to its own section rather than to the combined
templatedyldactually reads from. A file with both
`_Thread_local int g_shared = 7;` and `_Thread_local int
g_shared_uninit;` had both variables silently alias the same address.
The fix is to always put Mach-O ARM64 TLS backing storage - whatever
its initializer - into one section (.tdata, PROGBITS); a growing
PROGBITSsection is already zero-filled bysection_realloc(), so
uninitializedglobalsstill come out zeroed without needing a separate
NOBITSsection.

Cross-object-file linking needed two changes, and I want to be honest
that I got there by trying the obvious thing, watching it fail, and
narrowing down why, twice in a row. The offset field started as a
plain constant computed at compile time, which only happens to be
correct when nothing else's.tdatacontribution lands ahead of it -
false the moment two files each define their ownTLS globaland get
linked together. I tried stashing the backing symbol's table index
instead, since that's cheap and I could look it up once addresses were
final; that's also wrong, because per-object-file symbol indices don't
mean anything after multiple files' symbol tables get merged and
renumbered during linking, and two structurally similar source files
kept coincidentally reusing the same local index. What actually works
is an ordinary relocation against the backing symbol, resolved by the
normal relocation pass the same as any other pointer field, converted
into the offsetdyldwants only afterward, once every address is
final. That relocation would normally also pick up a rebase entry
tellingdyldto add theASLRslide to it at launch, which is correct
for a real pointer but wrong once it's been turned into a small
offset -dyldadds the slide on top and you get "malformed
thread-local, offset=<garbage>". There's now a narrow, specifically
targeted exemption incheck_relocs()for exactly that one relocation
so it doesn't get a rebase entry; thethunkpointer field, which does
need the normal bind path since__tlv_bootstrapis genuinely external,
is unaffected.

Testing throughout: TinyCC's own make test suite passes in full,
including the existing arm64 and atomics tests, with no regressions at
any point in this process. Beyond that, I wrote and ran: a basic
single-threaded read-modify-write test; a 4-thread test confirming
actual per-thread isolation (distinct storage addresses, correct
independent counters under concurrent increment) rather than just "it
doesn't crash"; a test specifically targeting the store()-after-a-call
register collision described above, including address-of and
pointer-chasing through TLS storage and multiple TLS globals sharing
one backing section; a test for initialized globals, including arrays,
with correct initial values and independent mutation across threads; a
test mixing initialized and uninitialized globals in one file; a test
with a TLS global defined in one file and used, both directly and
through the defining file's own accessor, from a second file, across
threads; and a test with two separate files each defining their own
TLS global, linked together and exercised concurrently.

Last, I rebuilt V's (vlang/v) real self-hosted compiler with thistcc
as-cc tcc, since that's what motivated this in the first place
(vlang/v#28023, vlang/tccbin#86). The_Thread_localfailure is gone.
Doing that did turn up one separate, pre-existing gap - bundledtcc
also doesn't resolve__sync_add_and_fetch,__sync_bool_compare_and_
swap, or__sync_lock_test_and_set, which V's-preallocallocator uses
and which was previously masked because compilation never got past the
_Thread_localerror to reach it. I checked and that's unrelated to
this patch - it reproduces identically against unpatchedtcconce
_Thread_localis avoided by using a different GC mode - so I haven't
touched it here.

Scope, to be clear about what this doesn't cover: only variables
actually defined somewhere in the object files being linked are
handled. Anextern _Thread_localthat's never defined anywhere in the
link (resolved only from adylib, say) still falls through to the
broken ELF-stylecodegen. I haven't attempted or tested that case.
---
arm64-gen.c | 69 ++++++++++++++++++++++++++++++++++-
tccgen.c | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++-
tccmacho.c | 90 +++++++++++++++++++++++++++++++++++++++++++---
3 files changed, 254 insertions(+), 6 deletions(-)

diff --git a/arm64-gen.c b/arm64-gen.c
index 85aa7c0e..f4ac22fe 100644
--- a/arm64-gen.c
+++ b/arm64-gen.c
@@ -466,7 +466,74 @@ static void arm64_strv(int sz_, int dst, int bas, uint64_t 
off)
static void arm64_sym(int r, Sym *sym, addr_t addend)
{
if (sym->type.t & VT_TLS) {
-#if TCC_TARGET_PE
+#ifdef TCC_TARGET_MACHO
+ /* Mach-O has no ELF-style TPIDR-relative TLS. Instead, a
+ `_Thread_local`/`__thread` symbol's address (see tccgen.c,
+ tlv_redirect_sym) is redirected to a 3-pointer TLV descriptor
+ {thunk, reserved, init_ptr} living in the __DATA,__thread_vars
+ section - `sym` itself still carries VT_TLS, but by this point
+ its (section, offset) binding already points at that descriptor,
+ not at the backing storage. Getting the real per-thread address
+ means loading the descriptor's address and calling through its
+ first field, `thunk(&descriptor) -> address`. That's an ordinary
+ GOT-relative load, emitted directly here rather than via a
+ recursive arm64_sym() call, since `sym` still has VT_TLS set and
+ would just re-enter this branch.
+
+ That call is a real AAPCS64 call and may clobber any of x0-x17,
+ which may collide with values TCC currently has live there (e.g.
+ store()'s "value to store" register, or another operand
+ mid-_expression_) that TCC's own save_regs() does not protect
+ against physical clobbering by inline codegen like this. So
+ explicitly save/restore x0-x17 around the call, and stash the
+ result across the restore so it lands correctly in `r` even if
+ `r` itself is one of the protected registers.
+
+ NOTE (scope): caller-saved SIMD/FP registers (v0-v7, v16-v31)
+ are *not* preserved here. A `_Thread_local` access interleaved
+ with a live FP/SIMD temporary in those registers is unsafe with
+ this patch. */
+ /* save_regs(0) alone is *not* sufficient here, verified empirically:
+ it correctly spills whatever TCC still has tracked on the vstack
+ to memory, but store()'s value register `r` was already captured
+ as a raw, TCC-bookkeeping-unaware int by vstore() before this
+ function is even called (see tccgen.c: `r = vtop->r & VT_VALMASK;
+ ... store(r, vtop - 1);`) - store() never re-reads it after this
+ call, so a save_regs()-only version silently stores whatever this
+ call left behind in that register instead of the original value.
+ Confirmed by disassembling `_Thread_local int g_x; g_x = 41;`:
+ save_regs() correctly emits `stur w0, [x29, #-4]` to spill the
+ 41, but the immediately following `str w0, [x30]` (the actual
+ store) still reads directly from w0, which by then holds this
+ call's clobbered leftovers, not 41.
+
+ So: call save_regs(0) too, since it's free and correctly handles
+ ordinary TCC-tracked values (including FP/SIMD ones reachable
+ through the normal vtop path, not through a frozen raw register
+ parameter) - but *also* physically save/restore x0-x17 so the
+ register state is bit-for-bit unchanged by the time this
+ function returns, which is what a frozen raw parameter like
+ store()'s `r` actually needs. */
+ save_regs(0);
+ {
+ int i;
+ o(ARM64_SUB_IMM | ARM64_SF(1) | ARM64_RN(31) | ARM64_RD(31) | 
ARM64_IMM12(160)); // sub sp, sp, #160
+ for (i = 0; i < 18; i += 2)
+ o(ARM64_STP_X | ARM64_RT(i) | ARM64_RT2(i + 1) | ARM64_RN(31) | 
ARM64_IMM7(i)); // stp xi, xi+1, [sp, #i*8]
+ greloca(cur_text_section, sym, ind, R_AARCH64_ADR_GOT_PAGE, 0);
+ o(ARM64_ADRP | 0); // adrp x0, &descriptor@page
+ greloca(cur_text_section, sym, ind, R_AARCH64_LD64_GOT_LO12_NC, 0);
+ o(ARM64_LDR_X | ARM64_RN(0) | 0); // ldr x0, [x0, &descriptor@pageoff]
+ o(ARM64_LDR_X | ARM64_RN(0) | 8); // ldr x8, [x0] ; x8 = thunk
+ o(ARM64_BLR | ARM64_RN(8)); // blr x8 ; x0 = real address
+ o(ARM64_STR_X | ARM64_RN(31) | ARM64_IMM12(144 / 8) | 0); // str x0, [sp, 
#144]
+ for (i = 0; i < 18; i += 2)
+ o(ARM64_LDP_X | ARM64_RT(i) | ARM64_RT2(i + 1) | ARM64_RN(31) | 
ARM64_IMM7(i)); // ldp xi, xi+1, [sp, #i*8]
+ o(ARM64_LDR_X | ARM64_RN(31) | ARM64_IMM12(144 / 8) | r); // ldr xr, [sp, 
#144]
+ o(ARM64_ADD_IMM | ARM64_SF(1) | ARM64_RN(31) | ARM64_RD(31) | 
ARM64_IMM12(160)); // add sp, sp, #160
+ }
+ goto add_addend;
+#elif TCC_TARGET_PE
Sym *s2 = external_global_sym(TOK___tls_index, &int_type);
int r2 = get_reg(RC_INT);
arm64_sym(30, s2, 0);
diff --git a/tccgen.c b/tccgen.c
index 227ff618..75532d9f 100644
--- a/tccgen.c
+++ b/tccgen.c
@@ -8231,14 +8231,88 @@ static void decl_initializer(init_params *p, CType 
*type, unsigned long c, int f
}
}

+#if defined(TCC_TARGET_MACHO) && defined(TCC_TARGET_ARM64)
+/* Redirect a Mach-O `_Thread_local`/`__thread` global's address from its
+ backing storage (already allocated at (backing_sec, backing_addr) by the
+ caller, in a `.tbss`/`.tdata` section) to a freshly synthesized 3-pointer
+ TLV descriptor { thunk, reserved, init_ptr }, matching the layout Apple's
+ own toolchain emits for `@TLVPPAGE`/`@TLVPPAGEOFF`-addressed variables.
+ arm64_macho_tls_sym() (see arm64-gen.c) then accesses the variable by
+ loading this descriptor's address and calling through its first field.
+
+ Scope: this only wires up variables *defined* in this translation unit
+ (V always compiles to a single .c file per `v self`/`tcc -o exe file.c`
+ build). `extern _Thread_local` symbols defined in another object file or
+ dylib are not redirected and keep using the (broken, on this target)
+ ELF-style TPIDR codegen - out of scope for this patch. */
+static void tlv_redirect_sym(Sym *sym, Section *backing_sec, addr_t 
backing_addr,
+ unsigned long size)
+{
+ Section *tv_sec;
+ Sym *bootstrap_sym, *backing_sym;
+ addr_t desc_addr;
+
+ tv_sec = find_section(tcc_state, ".thread_vars");
+ tv_sec->sh_flags = SHF_ALLOC | SHF_WRITE;
+ tv_sec->sh_type = SHT_PROGBITS;
+
+ /* external_global_sym()/put_extern_sym() go through the same C-symbol
+ path as normal parsed identifiers, which prepends the Mach-O leading
+ underscore. The real libSystem-exported symbol is `__tlv_bootstrap`
+ (two underscores), so ask for one underscore here to land on two. */
+ bootstrap_sym = external_helper_sym(tok_alloc("_tlv_bootstrap", 14)->tok);
+ backing_sym = get_sym_ref(&sym->type, backing_sec, backing_addr, size);
+
+ desc_addr = section_add(tv_sec, 3 * PTR_SIZE, PTR_SIZE);
+ greloca(tv_sec, bootstrap_sym, desc_addr, R_DATA_PTR, 0);
+ /* The third field is *not* an absolute pointer: dyld's TLV loader reads
+ it as a byte offset into this image's per-thread template region
+ (the __thread_data section - see fill_with_defaults() above, which
+ always uses one unified section for both initialized and
+ zero-initialized Mach-O ARM64 TLS globals precisely so this offset
+ is unambiguous), and rejects anything that looks like an absolute
+ (ASLR-slid) address as "malformed thread-local".
+
+ Still emit an ordinary R_DATA_PTR relocation against the backing
+ symbol here, same as the thunk pointer above: relocate_sections()
+ needs it to compute the symbol's correct final absolute address
+ regardless of how many object files' .tdata contributions get
+ merged ahead of it, which a raw compile-time constant can't do (two
+ files each defining their own _Thread_local global and getting
+ linked together, for example - tcc's normal same-named-section
+ merging places the second file's contribution at an offset only
+ known once linking finishes; a symbol table index stashed at
+ compile time doesn't survive that either, since indices are
+ per-object-file and get renumbered on merge same as everything
+ else - tried that first, empirically confirmed it also breaks).
+
+ check_relocs() (tccmacho.c) has a matching, narrowly-scoped
+ exemption so this specific relocation - R_DATA_PTR, target section
+ named ".thread_vars", locally-defined symbol - does not also get a
+ rebase entry; ordinarily it would, and dyld would then add the
+ load's ASLR slide a second time on top of the offset Mach-O
+ output's tlv_patch_descriptor_offsets() (tccmacho.c) computes below,
+ corrupting it ("malformed thread-local, offset=<slide-sized
+ garbage>" at launch, confirmed empirically before that exemption was
+ added). tlv_patch_descriptor_offsets() runs after relocate_sections()
+ has resolved this relocation to the backing symbol's correct final
+ absolute address, and replaces it with
+ that_address - __thread_data's own final section address. */
+ greloca(tv_sec, backing_sym, desc_addr + 2 * PTR_SIZE, R_DATA_PTR, 0);
+
+ put_extern_sym(sym, tv_sec, desc_addr, size);
+}
+#endif
+
/* parse an initializer for type 't' if 'has_init' is non zero, and
+ 'v' is a variable, 'type' is the associated type. If 'has_init' is
allocate space in local or global data space ('r' is either
VT_LOCAL or VT_CONST). If 'v' is non zero, then an associated
variable 'v' of scope 'scope' is declared before initializers
are parsed. If 'v' is zero, then a reference to the new object
is put in the value stack. If 'has_init' is 2, a special parsing
is done to handle string constants. */
-static void decl_initializer_alloc(CType *type, AttributeDef *ad, int r,
+static void decl_initializer_alloc(CType *type, AttributeDef *ad, int r,
int has_init, int v, int scope)
{
int size, align, addr;
@@ -8400,9 +8474,29 @@ static void decl_initializer_alloc(CType *type, 
AttributeDef *ad, int r,
while ((tp->t & (VT_BTYPE|VT_ARRAY)) == (VT_PTR|VT_ARRAY))
tp = &tp->ref->type;
if (type->t & VT_TLS) {
+#if defined(TCC_TARGET_MACHO) && defined(TCC_TARGET_ARM64)
+ /* Mach-O's TLV descriptor "init offset" field (see
+ tlv_redirect_sym below) must be relative to the single
+ combined per-image thread-local template - the
+ concatenation of __thread_data and __thread_bss at
+ final link time, in whatever order the linker places
+ them. A raw offset constant computed against .tbss's
+ own section-local numbering goes stale the moment
+ anything in .tdata sits before it in that template, so
+ always use one section (.tdata, PROGBITS) for both
+ initialized and zero-initialized Mach-O ARM64 TLS
+ globals - section_add() into a growing PROGBITS section
+ is already zero-filled by section_realloc(), so the
+ has_init=0 case still comes out correctly zeroed
+ without needing a separate SHT_NOBITS section. */
+ sec = find_section(tcc_state, ".tdata");
+ sec->sh_flags = SHF_ALLOC | SHF_WRITE | SHF_TLS;
+ sec->sh_type = SHT_PROGBITS;
+#else
sec = find_section(tcc_state, has_init ? ".tdata" : ".tbss");
sec->sh_flags = SHF_ALLOC | SHF_WRITE | SHF_TLS;
sec->sh_type = has_init ? SHT_PROGBITS : SHT_NOBITS;
+#endif
} else if (tp->t & VT_CONSTANT) {
sec = rodata_section;
} else if (has_init) {
@@ -8431,6 +8525,11 @@ static void decl_initializer_alloc(CType *type, 
AttributeDef *ad, int r,
patch_storage(sym, ad, NULL);
}
/* update symbol definition */
+#if defined(TCC_TARGET_MACHO) && defined(TCC_TARGET_ARM64)
+ if (type->t & VT_TLS)
+ tlv_redirect_sym(sym, sec, addr, size);
+ else
+#endif
put_extern_sym(sym, sec, addr, size);
} else {
/* push global reference */
diff --git a/tccmacho.c b/tccmacho.c
index cee09fd7..aa82bc81 100644
--- a/tccmacho.c
+++ b/tccmacho.c
@@ -45,6 +45,7 @@
#define MH_DYLDLINK (0x4)
#define MH_DYLIB (0x6)
#define MH_PIE (0x200000)
+#define MH_HAS_TLV_DESCRIPTORS (0x800000)

#define CPU_SUBTYPE_LIB64 (0x80000000)
#define CPU_SUBTYPE_X86_ALL (3)
@@ -230,6 +231,9 @@ struct dyld_chained_ptr_64_bind
#define S_SYMBOL_STUBS 0x8
#define S_MOD_INIT_FUNC_POINTERS 0x9
#define S_MOD_TERM_FUNC_POINTERS 0xa
+#define S_THREAD_LOCAL_REGULAR 0x11
+#define S_THREAD_LOCAL_ZEROFILL 0x12
+#define S_THREAD_LOCAL_VARIABLES 0x13

#define S_ATTR_PURE_INSTRUCTIONS 0x80000000
#define S_ATTR_SOME_INSTRUCTIONS 0x00000400
@@ -400,6 +404,9 @@ enum skind {
sk_fini,
sk_rw_data,
sk_bss,
+ sk_thread_vars,
+ sk_thread_data,
+ sk_thread_bss,
sk_linkedit,
sk_last
};
@@ -767,9 +774,28 @@ static void check_relocs(TCCState *s1, struct macho *mo)
rel->r_addend += attr->plt_offset;
}
}
- if (type == R_DATA_PTR || type == R_JMP_SLOT)
- bind_rebase_add(mo, sym->st_shndx == SHN_UNDEF ? 1 : 0,
- s->sh_info, &save_rel, NULL);
+ if (type == R_DATA_PTR || type == R_JMP_SLOT) {
+ /* tlv_redirect_sym() (tccgen.c) emits an R_DATA_PTR relocation
+ against the backing symbol for a Mach-O TLV descriptor's
+ third field, so relocate_sections() below computes its
+ correct absolute address regardless of how many object
+ files' .tdata contributions get merged ahead of it - but
+ that field ends up holding a small section-relative
+ offset, not a real pointer (see tlv_patch_descriptor_
+ offsets() and the comment in tlv_redirect_sym for why), so
+ it must not also get a rebase entry: dyld would add the
+ ASLR slide to it a second time at launch, corrupting it
+ (confirmed empirically - "malformed thread-local,
+ offset=<slide-sized garbage>"). Recognize and skip exactly
+ that one case; the descriptor's thunk field (against the
+ genuinely undefined __tlv_bootstrap) still goes through
+ the normal bind path below untouched. */
+ int skip_rebase = sym->st_shndx != SHN_UNDEF
+ && !strcmp(s1->sections[s->sh_info]->name, ".thread_vars");
+ if (!skip_rebase)
+ bind_rebase_add(mo, sym->st_shndx == SHN_UNDEF ? 1 : 0,
+ s->sh_info, &save_rel, NULL);
+ }
}
}
/* remove deleted binds */
@@ -1247,6 +1273,9 @@ const struct {
/*[sk_fini] =*/ { 4, S_MOD_TERM_FUNC_POINTERS, "__mod_term_func" },
/*[sk_rw_data] =*/ { 4, S_REGULAR, "__data" },
/*[sk_bss] =*/ { 4, S_ZEROFILL, "__bss" },
+ /*[sk_thread_vars] =*/ { 4, S_THREAD_LOCAL_VARIABLES, "__thread_vars" },
+ /*[sk_thread_data] =*/ { 4, S_THREAD_LOCAL_REGULAR, "__thread_data" },
+ /*[sk_thread_bss] =*/ { 4, S_THREAD_LOCAL_ZEROFILL, "__thread_bss" },
/*[sk_linkedit] =*/ { 5, S_REGULAR, NULL },
};

@@ -1649,7 +1678,7 @@ static void collect_sections(TCCState *s1, struct macho 
*mo, const char *filenam
default: sk = sk_unknown; break;
case SHT_INIT_ARRAY: sk = sk_init; break;
case SHT_FINI_ARRAY: sk = sk_fini; break;
- case SHT_NOBITS: sk = sk_bss; break;
+ case SHT_NOBITS: sk = (flags & SHF_TLS) ? sk_thread_bss : sk_bss; break;
case SHT_SYMTAB: sk = sk_discard; break;
case SHT_STRTAB:
if (s == stabstr_section)
@@ -1686,6 +1715,10 @@ static void collect_sections(TCCState *s1, struct macho 
*mo, const char *filenam
sk = sk_debug_str;
else if (s == dwarf_line_str_section)
sk = sk_debug_line_str;
+ else if (!strcmp(s->name, ".thread_vars"))
+ sk = sk_thread_vars;
+ else if (flags & SHF_TLS)
+ sk = sk_thread_data;
else if (flags & SHF_EXECINSTR)
sk = sk_text;
else if (flags & SHF_WRITE)
@@ -1756,8 +1789,18 @@ static void collect_sections(TCCState *s1, struct macho 
*mo, const char *filenam

dyldbv = add_lc(mo, LC_BUILD_VERSION, sizeof(*dyldbv));
dyldbv->platform = PLATFORM_MACOS;
+#ifdef TCC_TARGET_ARM64
+ /* Apple silicon only ever runs macOS 11.0+, and dyld gates TLV
+ descriptor patching (among other things) on the image's declared
+ minos - claiming 10.6 (pre-TLV, pre-Apple-silicon) leaves any
+ `_Thread_local`/`__thread` global's TLV thunk unpatched, so calling
+ it lands on the `__tlv_bootstrap` trap and aborts. */
+ dyldbv->minos = (11 << 16);
+ dyldbv->sdk = (11 << 16);
+#else
dyldbv->minos = (10 << 16) + (6 << 8);
dyldbv->sdk = (10 << 16) + (6 << 8);
+#endif
dyldbv->ntools = 0;

dyldsv = add_lc(mo, LC_SOURCE_VERSION, sizeof(*dyldsv));
@@ -1978,6 +2021,13 @@ static void macho_write(TCCState *s1, struct macho *mo, 
FILE *fp)
mo->mh.mh.filetype = MH_DYLIB;
mo->mh.mh.flags = MH_DYLDLINK;
}
+ if (mo->sk_to_sect[sk_thread_vars].s)
+ /* Tell dyld this image has __DATA,__thread_vars TLV descriptors
+ that need their thunk pointer patched at load time - without
+ this flag dyld skips that step entirely and the descriptor is
+ left pointing at the `__tlv_bootstrap` trap (abort on first
+ access to any `_Thread_local`/`__thread` variable). */
+ mo->mh.mh.flags |= MH_HAS_TLV_DESCRIPTORS;
mo->mh.mh.ncmds = mo->nlc;
mo->mh.mh.sizeofcmds = 0;
for (i = 0; i < mo->nlc; i++)
@@ -2176,6 +2226,35 @@ ST_FUNC void bind_rebase_import(TCCState *s1, struct 
macho *mo)
}
#endif

+#if defined(TCC_TARGET_ARM64)
+/* tccgen.c's tlv_redirect_sym() writes each TLV descriptor's third field
+ (the "init offset" dyld reads) as an ordinary R_DATA_PTR relocation
+ against the backing symbol, same as any other pointer field, so that
+ normal relocation resolution (relocate_sections(), below) computes its
+ correct final absolute address regardless of how many separately
+ compiled object files' .tdata contributions got merged ahead of it.
+ dyld does not want an absolute address there though - it wants a byte
+ offset into the image's per-thread template region (this __thread_data
+ section), and aborts with "malformed thread-local" otherwise. Now that
+ relocate_sections() has run and every address is final, walk each
+ descriptor and convert its resolved absolute address into that offset
+ by subtracting __thread_data's own final section address. */
+static void tlv_patch_descriptor_offsets(TCCState *s1, struct macho *mo)
+{
+ Section *tv, *td;
+ addr_t off;
+
+ tv = mo->sk_to_sect[sk_thread_vars].s;
+ if (!tv)
+ return;
+ td = mo->sk_to_sect[sk_thread_data].s;
+ for (off = 0; off + 3 * PTR_SIZE <= tv->data_offset; off += 3 * PTR_SIZE) {
+ unsigned char *field = tv->data + off + 2 * PTR_SIZE;
+ write64le(field, read64le(field) - td->sh_addr);
+ }
+}
+#endif
+
ST_FUNC int macho_output_file(TCCState *s1, const char *filename)
{
int fd, mode, file_type;
@@ -2216,6 +2295,9 @@ ST_FUNC int macho_output_file(TCCState *s1, const char 
*filename)
s1->output_type = TCC_OUTPUT_EXE;
relocate_sections(s1);
s1->output_type = save_output;
+#if defined(TCC_TARGET_ARM64)
+ tlv_patch_descriptor_offsets(s1, &mo);
+#endif
#ifdef CONFIG_NEW_MACHO
bind_rebase_import(s1, &mo);
#endif
--
2.52.0

Richard Wheeler
Sent with[Proton Mail](https://proton.me/mail/home)secure email.

Richard Wheeler

Sent with [Proton Mail](https://proton.me/mail/home) secure email.
_______________________________________________
Tinycc-devel mailing list
[email protected]
https://lists.nongnu.org/mailman/listinfo/tinycc-devel

Reply via email to