利用cgo调用c代码时要格外注意栈溢出问题,这点对于区块链应用更加重要。如果不加以合适的栈保护,一些恶意或有bug的合约代码在执行时会击穿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), BXMOVQ g_m(BX), BXMOVQ m_g0(BX), SICMPQ g(CX), SIJNE 3(PC)CALL runtime·badmorestackg0(SB)CALL runtime·abort(SB)// Cannot grow signal stack (m->gsignal).MOVQ m_gsignal(BX), SICMPQ g(CX), SIJNE 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), SICALL 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, SPANDQ $~15, SP // alignment for gcc ABIMOVQ DI, 48(SP) // save gMOVQ (g_stack+stack_hi)(DI), DISUBQ DX, DIMOVQ 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 ABIMOVQ BX, CX // CX = first argument in Win64CALL AX
cgo生成的桩代码中使用cgocall调用cgo工具生成的c函数,cgocall最终会调用汇编代码.asmcgocall
f1 := func() {output, gasLeft, err = v.vm.Execute(...)}if v.isNewRound(depth) {var mtx sync.Mutexmtx.Lock()go func() {defer mtx.Unlock()//will call c functionf1()}()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.Mutexmtx.Lock()go func() {defer mtx.Unlock()//will call c functionf1()}()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应用。