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-libc
或 emscripten
对我来说太过分了):
正如我开头所说:修改__stack_pointer
;但我无法实现(错误如上所示)。另外不知道除了16字节对齐还有什么需要考虑的。
Clang已经定义了函数__wasm_init_tls
,但是它的函数体是空的,没有人调用它(使用--export-all
标志时弹出的符号)。反正不知道这个功能能不能用
我知道 clang 支持重定位部分、动态 linking 等,但我不知道如何使用所有这些选项来实现我的目标目的。我不太了解这些选项。
或者...使用设置指针的 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
编译该代码
我有一个可重入的 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-libc
或 emscripten
对我来说太过分了):
正如我开头所说:修改
__stack_pointer
;但我无法实现(错误如上所示)。另外不知道除了16字节对齐还有什么需要考虑的。Clang已经定义了函数
__wasm_init_tls
,但是它的函数体是空的,没有人调用它(使用--export-all
标志时弹出的符号)。反正不知道这个功能能不能用我知道 clang 支持重定位部分、动态 linking 等,但我不知道如何使用所有这些选项来实现我的目标目的。我不太了解这些选项。
或者...使用设置指针的 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