WebAssembly + 并发:尝试从 C/C++ 设置线程本地堆栈

WebAssembly + concurrency: trying to set a thread-local stack from C/C++

我有一个可重入的 C++ 函数,其 wasm 在使用导入的共享内存时输出不是“线程安全的”,因为该函数使用了位于共享线性内存上的别名堆栈硬编码位置。

我知道还没有完全支持多线程,如果我想同时使用同一模块的多个实例,避免崩溃和数据竞争,这是我的责任,但我接受挑战。

我的 X 问题是:我的代码不是线程安全的,我需要它是非重叠堆栈。

我的 Y 问题是:我正在尝试修改 __stack_pointer 以便实现堆栈分离,但它无法编译。我试过 extern unsigned char __stack_pointer; 但它抛出以下错误:

wasm-ld: error: symbol type mismatch: __stack_pointer
>>> defined as WASM_SYMBOL_TYPE_GLOBAL in <internal>
>>> defined as WASM_SYMBOL_TYPE_DATA in senseless.o

因为我可能不应该直接触摸那个指针,所以我也在考虑其他解决方案(见下文)。

工作示例:

#define WASM_EXPORT __attribute__((visibility("default"))) extern "C"
#define WASM_IMPORT extern "C"

struct senseless
{
    unsigned c[1024];
    unsigned __attribute__((noinline)) compute(unsigned seed) { return c[seed % 1024]; }
};

WASM_EXPORT unsigned compute_senseless(unsigned seed)
{
    senseless h;
    h.c[5] = seed;
    return h.compute(seed);
}

编译后WAST中的实现结果为:

(module
  (type (;0;) (func (param i32) (result i32)))
  (import "env" "memory" (memory (;0;) 2 2 shared))
  
  (func (;0;) (type 0) (param i32) (result i32)
    (local i32)                          ; int l; // address of h
    (global.set 0                        ; SP = l (4096 = sizeof(senseless))
      (local.tee 1                           ; where l = SP - 4096
        (i32.sub (global.get 0)
                 (i32.const 4096))))
    (i32.store offset=20                 ; linear_memory[l + 5 * 4] = seed
      (local.get 1) (local.set 0))
    (i32.load                            ; return_value = linear_memory[l + (seed & 1023) * 4]
      (i32.add (local.get 1)
               (i32.shl                  
                  (i32.and (local.get 0) (i32.const 1023))
                  (i32.const 2))))
    (global.set 0                        ; SP = l + 4096
      (i32.add (local.get 1) (i32.const 4096))))
        
  (global (;0;) (mut i32) (i32.const 66576)) ; SP = 66576 (stack pointer)
  (export "compute_senseless" (func 0)))

所以该函数首先减少4096字节的SP分配h,执行h.c[5] = seed,将h.c[seed % 2014]压入(wasm)堆栈,然后恢复SP。

如果此模块的两个实例在两个不同的 WebWorker 上并行工作,h.c[5] = seed 可能会导致数据竞争,因为它们使用相同的硬编码 SP 作为堆栈基础。

为了强制每个实例都有自己的堆栈非重叠区域,我需要修改堆栈指针,但我不知道该怎么做。不过我有一些想法(使用 wasi-libcemscripten 对我来说太过分了):

  1. 正如我开头所说:修改__stack_pointer;但我无法实现(错误如上所示)。另外不知道除了16字节对齐还有什么需要考虑的。

  2. Clang已经定义了函数__wasm_init_tls,但是它的函数体是空的,没有人调用它(使用--export-all标志时弹出的符号)。反正不知道这个功能能不能用

  3. 我知道 clang 支持重定位部分、动态 linking 等,但我不知道如何使用所有这些选项来实现我的目标目的。我不太了解这些选项。

  4. 或者...使用设置指针的 WAST 函数手动完成,我必须在实例化后手动调用它:

// C++
void place_SP(int addr); // Implemented in WAST.

/* To be called from javascript; I'll make sure that this call never happens concurrently, that SP is multiple of 16 (stack base is 16-byte aligned), and that I have space enough to avoid stack overlapping. */
WASM_EXPORT void init_stack(int SP) { place_SP(SP); }

// place_SP.wast
(func $place_SP (param $addr i32)
      (global.set 0 (local.get $addr)))

但我不确定我还需要在 place_SP.wast 上写什么(它是一个不同的模块吗?我是否需要指定一个 (type) 条目?),我应该如何写修改我的 makefile 以使用 senseless.o 正确编译 place_SP.wast 和 link 以生成有效的 senseless.wasm.

附加信息:

生成文件:

.SUFFIXES: .wasm

WASI_SDK := <my-wasi-sdk-path> # I'm using wasi-sdk 12.
CXX := $(WASI_SDK)/bin/clang++
LD := $(WASI_SDK)/bin/wasm-ld

CPPFLAGS := --sysroot=$(WASI_SDK)/share/wasi-sysroot
CXXFLAGS := -O3 -flto -fno-exceptions
LDFLAGS := --lto-O3 -E --no-entry --import-memory --max-memory=131072 \
       --features=atomics,bulk-memory --shared-memory

senseless.wasm: senseless.o
    $(LD) --verbose $(LDFLAGS) $^ -o $@
    wasm-opt $@ -Oz -o $@
    wasm-strip $@
    wasm2wat -f --enable-threads --enable-bulk-memory $@ > $*.wast

senseless.o: senseless.cpp
    $(CXX) -v -c $(CPPFLAGS) $(CXXFLAGS) $< -o $@

clean:
    $(RM) senseless.wasm senseless.o

编译器日志输出(只有我认为是相关的东西):

# Internal compiler call (I have omitted options regarding paths):
 "<wasi-sdk>/bin/clang-11" -cc1 -triple wasm32-unknown-wasi -emit-llvm-bc -flto -flto-unit -disable-free -disable-llvm-verifier -discard-value-names -main-file-name senseless.cpp -mrelocation-model static -mframe-pointer=none -fno-rounding-math -mconstructor-aliases -target-cpu generic -fvisibility hidden -debugger-tuning=gdb -v -O3 -fdeprecated-macro -ferror-limit 19 -fgnuc-version=4.2.1 -fcolor-diagnostics -vectorize-loops -vectorize-slp -o senseless.o -x c++ senseless.cpp

# Memory layout (linker output)
wasm-ld: mem: global base = 1024 # I wonder what are the first 1024 bytes used for.
wasm-ld: mem: __wasm_init_memory_flag offset=1024     size=4        align=4
wasm-ld: mem: static data = 4 # No .rodata/.bss in this example, only the (unused) i32 flag.
wasm-ld: mem: stack size  = 65536 # One page (64KiB) for the stack, from 66576 to 1040.
wasm-ld: mem: stack base  = 1040 # I wonder what are the bytes from 1028 to 1039 used for.
wasm-ld: mem: stack top   = 66576
wasm-ld: mem: heap base   = 66576 # Heap from 66576 up to 131072.
wasm-ld: mem: total pages = 2
wasm-ld: mem: max pages   = 2

注意: 我试图通过将 -z,stack-size=131072 添加到 LDFLAGS 来个性化堆栈大小,因此堆栈大小是两页而不是一页例如(并将 --max-memory 增加到三页),但是堆栈 base/size/top 的 none 完全改变了。

解决方案

我根据选择的答案使其工作。我添加了一个名为 stack-trick.S 的文件(.hidden 构造是为了避免导出 place_SP,这是默认设置):

.globl place_SP
.hidden place_SP
.globaltype __stack_pointer, i32

place_SP:
   .functype place_SP(i32) -> ()
   local.set 0
   global.set __stack_pointer
   end_function

现在,在我的 senseless.cpp 代码中,我添加了:

extern "C" void place_SP(int);

WASM_EXPORT void init_stack(/* some args */)
{
     int SP = /* some computation based on parameters */;
     place_SP(SP);
}

在 makefile 中,我添加了:

senseless.wasm: stack-trick.o

stack-trick.o: stack-trick.S
    $(CXX) -c $< -o $@

senseless.wast 现在有一个奇特的新导出函数 (init_stack),此外,对 place_SP 的调用已被内联。

首先,如果您使用 emscripten 进行多线程处理,那么每个线程都已经拥有自己的堆栈和 __stack_pointer 的值。那是定义线程的一部分。

如果您仍想自己操作堆栈(可能在单个线程中有多个堆栈),那么您可以使用 emscripten 辅助函数 stackSave(获取当前线程的 SP)和 stackRestore (设置当前线程的SP)。

如果您根本不使用 emscripten,那么您就处于未知领域(正在使用什么运行时?您如何启动新线程?),但是进行堆栈指针操作的最简单方法是使用汇编代码.查看emscripten如何实现这些功能:

https://github.com/emscripten-core/emscripten/blob/main/system/lib/compiler-rt/stack_ops.S

所以你可以这样做:

.globaltype __stack_pointer, i32

place_SP:
  .functype place_SP(i32) -> ()
  local.get 0
  global.set __stack_pointer
  end_function

然后用 clang -c splace_sp.s -o place_sp.o

编译该代码