在go程序中调用c语言代码的场景中,有时候会出现more stack on g0
的错误,这中错误十分常见,比如下面这个程序就可以触发。
/*
static int increase(int a) {
return goIncrease(a++);
}
*/
import "C"
func main() {
goIncrease(0)
}
//export goIncrease
func goIncrease(a C.int) C.int {
a++
if a > 2500 {
return a
}
return C.increase(a)
}
该程序中goIncrease调用c语言定义的函数increase,increase又调用了go中的代码goIncrease,这样循环调用下去,直到a大于2500时退出。运行该程序,报出下面的错误
fatal: morestack on g0
SIGTRAP: trace trap
PC=0x4054832 m=0 sigcode=1
signal arrived during cgo execution
goroutine 1 [running, locked to thread]:
runtime.abort()
/usr/local/Cellar/go/1.14.2_1/libexec/src/runtime/asm_amd64.s:859 +0x2 fp=0x7ffeefb19170 sp=0x7ffeefb19168 pc=0x4054832
runtime.morestack()
/usr/local/Cellar/go/1.14.2_1/libexec/src/runtime/asm_amd64.s:416 +0x25 fp=0x7ffeefb19178 sp=0x7ffeefb19170 pc=0x4052ef5
rax 0x17
rbx 0x7ffeefb19140
rcx 0x40dc720
rdx 0x0
rdi 0x2
rsi 0x7ffeefb190e0
rbp 0xc000260388
rsp 0x7ffeefb19168
......
错误显示主M中的主goroutine也就是G0调用runtime.morestack()申请更多的栈空间,这个函数导致程序崩溃。
在smart chain的测试过程中也遇到了类似的问题,具体情况是这样:
evmone 是Ewasm团队用C++写的EVM实现。smart chain使用该项目来执行evm code。由于smart chain本身是go语言编写,这就引入了在go代码中调用c++代码的问题。
evmc项目实现了一个binding层,它对外暴露go语言接口,内部使用cgo来调用c代码,而c代码文件中又wrap了c++实现。
#evmc/bindings/go/evmc/evmc.go
package evmc
/*
#cgo CFLAGS: -I${SRCDIR}/.. -Wall -Wextra
#include <evmc/evmc.h>
#include <evmc/helpers.h>
#include <evmc/loader.h>
......
static struct evmc_result execute_wrapper(struct evmc_vm* vm,
uintptr_t context_index, enum evmc_revision rev,
enum evmc_call_kind kind, uint32_t flags, int32_t depth, int64_t gas,
const evmc_address* destination, const evmc_address* sender,
const uint8_t* input_data, size_t input_size, const evmc_uint256be* value,
const uint8_t* code, size_t code_size, const evmc_bytes32* create2_salt)
{
struct evmc_message msg = {
kind,
flags,
depth,
gas,
*destination,
*sender,
input_data,
input_size,
*value,
*create2_salt,
};
struct evmc_host_context* context = (struct evmc_host_context*)context_index;
return evmc_execute(vm, &evmc_go_host, context, rev, &msg, code, code_size);
}
*/
import "C"
func (vm *VM) Execute(ctx HostContext, rev Revision,
......
result := C.execute_wrapper(vm.handle, C.uintptr_t(ctxId), uint32(rev),
C.enum_evmc_call_kind(kind), flags, C.int32_t(depth), C.int64_t(gas),
&evmcDestination, &evmcSender, bytesPtr(input), C.size_t(len(input)), &evmcValue,
bytesPtr(code), C.size_t(len(code)), &evmcCreate2Salt)
removeHostContext(ctxId)
......
}
VM的Execute
方法中调用了C伪包中定义的execute_wrapper
函数,execute_wrapper
函数中调用的是evmc/bindings/go/evmc/helpers.h
中定义的evmc_execute
函数
static inline struct evmc_result evmc_execute(struct evmc_vm* vm,
const struct evmc_host_interface* host,
struct evmc_host_context* context,
enum evmc_revision rev,
const struct evmc_message* msg,
uint8_t const* code,
size_t code_size)
{
return vm->execute(vm, host, context, rev, msg, code, code_size);
}
而evmc_execute
函数中wrap了vm.execute
函数指针指向的代码段,这个指针在创建虚拟机时被赋值为如下函数
#lib/evmone/execution.cpp
evmc_result execute(evmc_vm* /*unused*/, const evmc_host_interface* host, evmc_host_context* ctx,
evmc_revision rev, const evmc_message* msg, const uint8_t* code, size_t code_size) noexcept
{
......
if(op!=OP_BEGINBLOCK) {
for(int i = state->stack.size() - 1; i >= 0; i--) {
......
}
}
......
}
通过上面讲述的这样一系列binding,我们最终可以在go代码中调用c++库中的函数。
当我们尝试用smart chain运行以太坊官方项目中的智能合约测试用例时,在运行到stRandom2
目录下的randomStatetest458.json
时,程序崩溃,报出morestack on g0
的错误。
这个用例中合约递归调用该合约自身,直到gas fee不足后再逐层退出调用。错误显示g0上当前栈空间不足,无法继续调用接下来的函数。
这里简单提一下go语言的栈管理。golang的调度器中有三个关键结构体,G,M,P。G代表一个goroutine,M对应一个os thread,P对应一个cpu核。当创建一个goroutine时,go运行时创建一个G,并分配kB量级的用户栈,这个栈是位于内存堆中的,如果G运行过程中需要更多的栈空间,最终会调用src/runtime/asm_amd64.s
中的runtime·morestack
方法来进行stack split。
TEXT runtime·morestack(SB),NOSPLIT,$0-0
// Cannot grow scheduler stack (m->g0).
get_tls(CX)
MOVQ g(CX), BX
MOVQ g_m(BX), BX
MOVQ m_g0(BX), SI
CMPQ g(CX), SI
JNE 3(PC)
CALL runtime·badmorestackg0(SB)
CALL runtime·abort(SB)
// Cannot grow signal stack (m->gsignal).
MOVQ m_gsignal(BX), SI
CMPQ g(CX), SI
JNE 3(PC)
CALL runtime·badmorestackgsignal(SB)
CALL runtime·abort(SB)
但是要注意的是g0是不允许stack split的,g0不同于其他的g,它的栈是系统堆栈,当执行系统调用,cgocall,调度等任务时,会从普通g的栈上切换到g0的栈,这时的任务是不可抢占的,也不被垃圾收集器扫描,同时,系统堆栈是不支持split的,它在线程初始化时指定,一般是MB量级的。例如,开启了cgo后,创建一个新的M时会调用_cgo_sys_thread_start启动一个线程,可以看到栈大小已经在创建时指定。
#src/runtime/cgo/gcc_darwin_amd64.c
void _cgo_sys_thread_start(ThreadStart *ts)
{
pthread_attr_t attr;
sigset_t ign, oset;
pthread_t p;
pthread_attr_init(&attr);
pthread_attr_getstacksize(&attr, &size);
// Leave stacklo=0 and set stackhi=size; mstart will do the rest.
ts->g->stackhi = size;
err = _cgo_try_pthread_create(&p, &attr, threadentry, ts);
......
}
cgo生成的桩代码中使用cgocall调用cgo工具生成的c函数,cgocall最终会调用汇编代码.asmcgocall
TEXT ·asmcgocall(SB),NOSPLIT,$0-20
// Switch to system stack.
MOVQ m_g0(R8), SI
CALL gosave<>(SB)
MOVQ SI, g(CX)
MOVQ (g_sched+gobuf_sp)(SI), SP
// Now on a scheduling stack (a pthread-created stack).
// Make sure we have enough room for 4 stack-backed fast-call
// registers as per windows amd64 calling convention.
SUBQ $64, SP
ANDQ $~15, SP // alignment for gcc ABI
MOVQ DI, 48(SP) // save g
MOVQ (g_stack+stack_hi)(DI), DI
SUBQ DX, DI
MOVQ DI, 40(SP) // save depth in stack (can't just save SP, as stack might be copied during a callback)
MOVQ BX, DI // DI = first argument in AMD64 ABI
MOVQ BX, CX // CX = first argument in Win64
CALL AX
可以看出c代码都是在g0栈上执行的,且在真正执行c代码之前go运行时会保存当前的g的地址,g的上下文,栈深度等信息到系统栈中。
CALL AX
后在evmone
的代码中又回调了smart chain中的go语言函数。这个是通过cgocallbackg
实现的,它保证了这些调用都在同一个M上。
到目前为止,我们可以知道当代码不停的在go和c之间互相调用时,c语言的函数执行和go的运行时代码是执行在g0栈上的,栈上的内容除了c函数的调用栈外还有go运行时保存的g上下文信息等。而栈的大小在创建线程时指定,在运行中不会split,所以,如果不加以栈深度等限制的情况下,栈一定会溢出。而go不允许g0扩增栈的大小,这就是报出morestack on g0
的原因。
如何解决呢?
一方面,由于evmone中虚拟机执行函数是固定的,它的栈大小可以近似成一个常量,同时go的运行时在进入到c代码执行前保存的信息也基本是常量,所以我们可以通过控制递归调用c代码的深度来防止栈溢出。
另一方面,当M被锁定在某个G上时,如果该G处于不可执行状态,那么M会释放掉P,runtime寻找新的M绑定P来运行P中的G。在cgocallbackg
中runtime会将当前G与M锁定,我们只需要找到一种方式使得当前的G不可执行,就能释放P让新的M运行接下来的代码。例如下面的代码所示:
f1 := func() {
output, gasLeft, err = v.vm.Execute(...)
}
if v.isNewRound(depth) {
var mtx sync.Mutex
mtx.Lock()
go func() {
defer mtx.Unlock()
//will call c function
f1()
}()
mtx.Lock()
mtx.Unlock()
return
}
f1()
return
}
我们通过在go回调函数中创建新的goroutine,在新建的goroutine中执行这部分被c代码调用的go代码。当前G在第二次尝试获取锁时失败,导致当前M释放掉P,新建的goroutine被绑定到新的M上运行。由于每个M都有一个独立的g0栈,这就相当于变相的动态扩充了系统栈的大小。这里的互斥锁同时也保证了最初的cgocall
的调用顺序,程序还是顺序执行的。
结论:利用cgo调用c代码时要格外注意栈溢出问题,这点对于区块链应用更加重要。如果不加以合适的栈保护,一些恶意或有bug的合约代码在执行时会击穿g0栈,导致节点崩溃,链的活性就会受到影响。
**本文由CoinEx Chain开发团队成员helldealer撰写。CoinEx Chain是全球首条基于Tendermint共识协议和Cosmos SDK开发的DEX专用公链,借助IBC来实现DEX公链、智能合约链、隐私链三条链合一的方式去解决可扩展性(Scalability)、去中心化(Decentralization)、安全性(security)区块链不可能三角的问题,能够高性能的支持数字资产的交易以及基于智能合约的Defi应用。
有疑问加站长微信联系(非本文作者)