[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