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

问题描述 投票:0回答:2

我有一个可重入的 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)))

因此该函数首先将 SP 减少 4096 个字节来分配

h
,执行
h.c[5] = seed
,将
h.c[seed % 2014]
压入(wasm)堆栈,然后恢复 SP。

如果此模块的两个实例在两个不同的 WebWorker 上并行工作,

h.c[5] = seed
可能会导致数据竞争,因为它们使用相同的硬编码 SP 作为堆栈基础。

为了强制每个实例拥有自己的堆栈非重叠区域,我需要修改堆栈指针,但我不知道该怎么做。不过我有一些想法(使用

wasi-libc
emscripten
对我来说太过分了):

  1. 正如我一开始所说:修改

    __stack_pointer
    ;但我无法实现它(错误如上所示)。另外,我不知道除了16字节对齐之外还有什么需要考虑的吗

  2. Clang 已经定义了函数

    __wasm_init_tls
    ,但它的函数体是空的,没有人调用它(使用
    --export-all
    标志时会弹出该符号)。不知道这个功能到底能不能用。

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

  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
才能正确编译
place_SP.wast
并将其与
senseless.o
链接以生成有效的
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
增加到三页),但没有堆栈底部/大小/顶部完全改变。

解决方案

我根据所选答案使其发挥作用。我添加了一个名为

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
的调用已被内联。

c++ c clang webassembly
2个回答
1
投票

首先,如果您使用 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

编译该代码

0
投票

__stack_pointer 是内置的,我们可以使用这个巧妙的技巧。只需导出它并确保仅从 JS 端访问它即可。

void set_sp(void* sp){
    __asm__(
        "local.get 0\n"
        "global.set __stack_pointer\n"
    );
}

asm 文件非常适合声明您自己的新全局变量,因为 Clang 尚不支持定义 wasm 全局变量。在这种情况下,我会选择内联水管。

© www.soinside.com 2019 - 2024. All rights reserved.