morestack on g0

在go程序中调用c语言代码的场景中,有时候会出现more stack on g0的错误,这个错误十分常见,比如下面这个程序就可以触发。

/*static int increase(int a) {   return goIncrease(a++);}*/import "C"func main() {   goIncrease(0)}//export goIncreasefunc 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 g0SIGTRAP: trace trapPC=0x4054832 m=0 sigcode=1signal arrived during cgo executiongoroutine 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=0x4054832runtime.morestack()        /usr/local/Cellar/go/1.14.2_1/libexec/src/runtime/asm_amd64.s:416 +0x25 fp=0x7ffeefb19178 sp=0x7ffeefb19170 pc=0x4052ef5rax    0x17rbx    0x7ffeefb19140rcx    0x40dc720rdx    0x0rdi    0x2rsi    0x7ffeefb190e0rbp    0xc000260388rsp    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.gopackage 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.cppevmc_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启动一个线程,可以看到栈大小已经在创建时指定。

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 Win64CALLAX

cgo生成的桩代码中使用cgocall调用cgo工具生成的c函数,cgocall最终会调用汇编代码.asmcgocall

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}

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栈,导致节点崩溃,链的活性就会受到影响。

 
posted @ 2022-05-12 16:18  zJanly  阅读(666)  评论(0编辑  收藏  举报