我有一个可重入的 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
对我来说太过分了):
正如我一开始所说:修改
__stack_pointer
;但我无法实现它(错误如上所示)。另外,我不知道除了16字节对齐之外还有什么需要考虑的吗
Clang 已经定义了函数
__wasm_init_tls
,但它的函数体是空的,没有人调用它(使用 --export-all
标志时会弹出该符号)。不知道这个功能到底能不能用。
我知道 clang 支持重定位部分、动态链接等,但我不知道如何使用所有这些选项来实现我的目的。我不太了解这些选项。
或者...使用 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
的调用已被内联。
首先,如果您使用 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
编译该代码
__stack_pointer 是内置的,我们可以使用这个巧妙的技巧。只需导出它并确保仅从 JS 端访问它即可。
void set_sp(void* sp){
__asm__(
"local.get 0\n"
"global.set __stack_pointer\n"
);
}
asm 文件非常适合声明您自己的新全局变量,因为 Clang 尚不支持定义 wasm 全局变量。在这种情况下,我会选择内联水管。