[lld] [WebAssembly] Fix __init_tls_base global in coop threading (PR #208597)
Alex Crichton via llvm-commits
llvm-commits at lists.llvm.org
Thu Aug 13 11:49:58 PDT 2026
https://github.com/alexcrichton updated https://github.com/llvm/llvm-project/pull/208597
>From cda5700ae3ba0abea8f977a3e9688770d011c2c4 Mon Sep 17 00:00:00 2001
From: Alex Crichton <alex at alexcrichton.com>
Date: Thu, 9 Jul 2026 16:09:07 -0700
Subject: [PATCH] [WebAssembly] Fix `__init_tls_base` global in coop threading
This commit updates how `wasm-ld` initializes the `__init_tls_base`
global during module instantiation. Previously this global was left
entirely unmodified meaning that it was always 0. This change updates
the `__wasm_init_memory` function to set this global dynamically in PIC
mode based on the TLS address calculation, or in non-PIC mode the
generation of `__wasm_init_memory` correctly sets it to the desired TLS
address.
cc WebAssembly/wasi-libc#819
---
lld/test/wasm/cooperative-threading.s | 84 +++++++++++++++++++++++++--
lld/wasm/Driver.cpp | 5 +-
lld/wasm/Writer.cpp | 19 ++++++
3 files changed, 100 insertions(+), 8 deletions(-)
diff --git a/lld/test/wasm/cooperative-threading.s b/lld/test/wasm/cooperative-threading.s
index 4ae4d464d1f90..11b68bc9457be 100644
--- a/lld/test/wasm/cooperative-threading.s
+++ b/lld/test/wasm/cooperative-threading.s
@@ -94,6 +94,28 @@ baz:
# CHECK-NEXT: - Minimum: 0x2
# CHECK-NOT: Shared
+# Ensure __init_stack_pointer, __init_tls_base, and __tls_size are all correct.
+# CHECK: - Type: GLOBAL
+# CHECK-NEXT: Globals:
+# CHECK-NEXT: - Index: 0
+# CHECK-NEXT: Type: I32
+# CHECK-NEXT: Mutable: false
+# CHECK-NEXT: InitExpr:
+# CHECK-NEXT: Opcode: I32_CONST
+# CHECK-NEXT: Value: 65536
+# CHECK-NEXT: - Index: 1
+# CHECK-NEXT: Type: I32
+# CHECK-NEXT: Mutable: true
+# CHECK-NEXT: InitExpr:
+# CHECK-NEXT: Opcode: I32_CONST
+# CHECK-NEXT: Value: 65544
+# CHECK-NEXT: - Index: 2
+# CHECK-NEXT: Type: I32
+# CHECK-NEXT: Mutable: false
+# CHECK-NEXT: InitExpr:
+# CHECK-NEXT: Opcode: I32_CONST
+# CHECK-NEXT: Value: 8
+
# The function table is exported by default.
# CHECK: - Type: EXPORT
# CHECK: - Name: __indirect_function_table
@@ -137,8 +159,14 @@ baz:
# CHECK-NEXT: Name: __tls_align
# DIS-LABEL: <__wasm_init_memory>:
-# DIS: memory.init 2, 0
-# DIS-NEXT: end
+# DIS-EMPTY:
+# DIS-NEXT: i32.const 65544
+# DIS-NEXT: i32.const 65544
+# DIS-NEXT: call 0
+# DIS-NEXT: i32.const 0
+# DIS-NEXT: i32.const 8
+# DIS-NEXT: memory.init 2, 0
+# DIS-NEXT: end
# DIS-LABEL: <_start>:
# DIS-EMPTY:
@@ -168,6 +196,35 @@ baz:
# RUN: obj2yaml %t.so | FileCheck %s --check-prefix=PIC
# RUN: llvm-objdump --disassemble-symbols=__wasm_init_memory --no-show-raw-insn --no-leading-addr %t.so | FileCheck %s --check-prefix=PIC-DIS
+# The stack pointer is imported under the libcall ABI name and
+# __wasm_set_tls_base is imported for TLS initialization.
+# PIC: - Type: IMPORT
+# PIC: Field: __init_stack_pointer
+# PIC-NEXT: Kind: GLOBAL
+# PIC-NEXT: GlobalType: I32
+# PIC-NEXT: GlobalMutable: false
+# PIC: Field: __memory_base
+# PIC: Field: __table_base
+# PIC: Field: __wasm_set_tls_base
+# PIC-NEXT: Kind: FUNCTION
+
+# The PIC `__init_tls_base` global (global 3) is mutable and initialized ot
+# 0 since its final value is calculated once `__memory_base` is provided.
+# PIC: - Type: GLOBAL
+# PIC-NEXT: Globals:
+# PIC-NEXT: - Index: 3
+# PIC-NEXT: Type: I32
+# PIC-NEXT: Mutable: true
+# PIC-NEXT: InitExpr:
+# PIC-NEXT: Opcode: I32_CONST
+# PIC-NEXT: Value: 0
+# PIC-NEXT: - Index: 4
+# PIC-NEXT: Type: I32
+# PIC-NEXT: Mutable: false
+# PIC-NEXT: InitExpr:
+# PIC-NEXT: Opcode: I32_CONST
+# PIC-NEXT: Value: 8
+
# In PIC mode the active .rodata and .data segments are combined into a single
# active segment at __memory_base; the TLS segment remains passive.
# PIC: - Type: DATACOUNT
@@ -185,15 +242,32 @@ baz:
# PIC-NEXT: Content: '0100000002000000'
# PIC-NEXT: - Type: CUSTOM
+# PIC: GlobalNames:
+# PIC-NEXT: - Index: 0
+# PIC-NEXT: Name: __init_stack_pointer
+# PIC-NEXT: - Index: 1
+# PIC-NEXT: Name: __memory_base
+# PIC-NEXT: - Index: 2
+# PIC-NEXT: Name: __table_base
+# PIC-NEXT: - Index: 3
+# PIC-NEXT: Name: __init_tls_base
+# PIC-NEXT: - Index: 4
+# PIC-NEXT: Name: __tls_size
+# PIC-NEXT: - Index: 5
+# PIC-NEXT: Name: __tls_align
+
# Memory initialization in PIC mode has a few responsibilities: it calculates
-# the TLS address and puts it in a local, `__wasm_set_tls_base` is called,
-# TLS is initialized, and then finally BSS is zero'd out.
+# the TLS address and puts it in a local, stores it into the __init_tls_base
+# global, `__wasm_set_tls_base` is called, TLS is initialized, and then finally
+# BSS is zero'd out.
# PIC-DIS: <__wasm_init_memory>:
# PIC-DIS-NEXT: .local i32
# PIC-DIS-NEXT: i32.const 8
-# PIC-DIS-NEXT: global.get {{[0-9]+}}
+# PIC-DIS-NEXT: global.get 1
# PIC-DIS-NEXT: i32.add
# PIC-DIS-NEXT: local.tee 0
+# PIC-DIS-NEXT: global.set 3
+# PIC-DIS-NEXT: local.get 0
# PIC-DIS-NEXT: call {{[0-9]+}}
# PIC-DIS-NEXT: local.get 0
# PIC-DIS-NEXT: i32.const 0
diff --git a/lld/wasm/Driver.cpp b/lld/wasm/Driver.cpp
index 740e8878c6e03..4c2549b39def7 100644
--- a/lld/wasm/Driver.cpp
+++ b/lld/wasm/Driver.cpp
@@ -981,9 +981,8 @@ static void createSyntheticSymbols() {
// TLS symbols are all hidden/dso-local
auto tls_base_name =
ctx.arg.libcallThreadContext ? "__init_tls_base" : "__tls_base";
- ctx.sym.tlsBase =
- createGlobalVariable(tls_base_name, !ctx.arg.libcallThreadContext,
- WASM_SYMBOL_VISIBILITY_HIDDEN);
+ ctx.sym.tlsBase = createGlobalVariable(tls_base_name, true,
+ WASM_SYMBOL_VISIBILITY_HIDDEN);
ctx.sym.tlsSize = createGlobalVariable("__tls_size", false,
WASM_SYMBOL_VISIBILITY_HIDDEN);
ctx.sym.tlsAlign = createGlobalVariable("__tls_align", false,
diff --git a/lld/wasm/Writer.cpp b/lld/wasm/Writer.cpp
index 157af6a4ea9ac..06be150294600 100644
--- a/lld/wasm/Writer.cpp
+++ b/lld/wasm/Writer.cpp
@@ -1416,13 +1416,32 @@ void Writer::createInitMemoryFunction() {
// When we initialize the TLS segment we also set the TLS base.
// This allows the runtime to use this static copy of the TLS data
// for the first/main thread.
+ //
+ // Note that for `--cooperative-threading` this additionally configures
+ // the `__init_tls_base` global which is the initial TLS value that can
+ // be used for all new component model tasks. For non-PIC builds this
+ // global's statically known value is now calculated, so it's updated
+ // here. For PIC builds the result of the address computation above is
+ // what's stored into the global.
+ //
+ // Finally, note that a temporary local is used here to ensure that the
+ // result of the addition above can be reused a number of times.
if (ctx.arg.isMultithreaded() && s->isTLS()) {
if (ctx.isPic) {
// Cache the result of the addition in the TLS address local
writeU8(os, WASM_OPCODE_LOCAL_TEE, "local.tee");
writeUleb128(os, tlsAddressLocal, "tls address local");
+ if (ctx.arg.libcallThreadContext) {
+ writeU8(os, WASM_OPCODE_GLOBAL_SET, "GLOBAL_SET");
+ writeUleb128(os, ctx.sym.tlsBase->getGlobalIndex(),
+ "__init_tls_base");
+ writeU8(os, WASM_OPCODE_LOCAL_GET, "local.get");
+ writeUleb128(os, tlsAddressLocal, "tls address local");
+ }
} else {
writePtrConst(os, s->startVA, is64, "destination address");
+ if (ctx.arg.libcallThreadContext)
+ ctx.sym.tlsBase->global->setPointerValue(s->startVA);
}
writeSetTLSBase(ctx, os);
if (ctx.isPic) {
More information about the llvm-commits
mailing list