Skip to content

Instantly share code, notes, and snippets.

@holiman
Last active July 23, 2019 08:56
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save holiman/3f9e778fe722f984b62a00588b4b1042 to your computer and use it in GitHub Desktop.
Save holiman/3f9e778fe722f984b62a00588b4b1042 to your computer and use it in GitHub Desktop.

Writing an EVM-debugger for Geth

This document provides some ideas on how someone fairly easily could build the basic blocks for a debugger using geth.

Integration

The tracer interface defines four methods that a 'tracer' needs to implement:

type Tracer interface {
	CaptureStart(from common.Address, to common.Address, call bool, input []byte, gas uint64, value *big.Int) error
	CaptureState(env *EVM, pc uint64, op OpCode, gas, cost uint64, memory *Memory, stack *Stack, contract *Contract, depth int, err error) error
	CaptureFault(env *EVM, pc uint64, op OpCode, gas, cost uint64, memory *Memory, stack *Stack, contract *Contract, depth int, err error) error
	CaptureEnd(output []byte, gasUsed uint64, t time.Duration, err error) error
}

This is used today to generate tracing; outputting detailed information about what happens at every step of the execution, and would act as the basic interaction points between the deugger and the evm.

The methods above would be sufficient to implement basically everything needed from a debugger.

Designing the debugger

A debugger would

  • Open an external port for communication with a UI,
  • Take messages from the UI, such as
    • setBreakpoint(int pc, bool persistent)
    • unsetBreakpoint(int pc)
    • setBreakOnError()/unsetBreakOnError
    • setBreakOnOp(byte[] breakOnOpcodes)

When a breakpoint has been hit, the CaptureState would

  1. Send (to client) the current state info
  2. Halt the execution (not return control to the caller) until the external client either calls continue() or step()

When the execution is done, the debugger should be able to restart the execution. This needs some internal legwork to get it working. Probably easiest to just have a loop:

for ;; notExited{
   snapshot = evm.StateDB.Snapshot()
   executeInDebugger(tx)
   evm.StateDB.RevertToSnapshot(snapshot)
}

More advanced stuff

  • It would be possible, via the env *vm.EVM in CaptureState to modify the storage slots of the contract being executed
evm.StateDB.SetState(contract.Address(), loc, common.BigToHash(value))
  • It would also be possible to also manipulate memory contents, via memory.Set32(mStart.Uint64(), val) from CaptureState -- but currently all memory operations assume the memory to already be expanded properly, so the debugger interface would have to ensure to take care of that to not cause panics. Example to store a single byte: memory.store[off] = byte(val & 0xff)

What needs to be done

  • An external user interface, that communicates with the geth/evm process
  • An internal debugger, which acts as a server and communicates over some port
  • The debugger protocol needs to be figured out -- ideally we can reuse some existing debugger protocol (maybe mozilla)
  • Implement the Tracer interface methods,
  • Add flags to evm and/or geth to enable remote debugger.

Initially, I'd recommend starting with the evm, which is not as mission-critical as geth. As a second step, adding the similar functionality to geth needs some more thought -- the usage scenario would probably be to debug a certain transaction, so perhaps add a method debug_debugTransaction(<txhash>) should boot up the debugger server on the desired tx.

The EVMLab opviewer could be used to make a PoC UI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment