This document provides some ideas on how someone fairly easily could build the basic blocks for a debugger using geth.
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.
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
- Send (to client) the current state info
- Halt the execution (not return control to the caller) until the
external client either calls
continue()
orstep()
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)
}
- It would be possible, via the
env *vm.EVM
inCaptureState
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)
fromCaptureState
-- 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 causepanic
s. Example to store a single byte:memory.store[off] = byte(val & 0xff)
- 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/orgeth
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.