Current section

Files

Jump to
evm lib evm debugger.ex
Raw

lib/evm/debugger.ex

defmodule EVM.Debugger do
@moduledoc """
A first-class debugger for the EVM. We are able to set
breakpoints and walk through code execution.
"""
alias EVM.Debugger.Breakpoint
alias EVM.Debugger.Command
@commands [
%Command{command: :next, name: "next", shortcut: "n", description: "Run this operation and break on next operation."},
%Command{command: :continue, name: "continue", shortcut: "c", description: "Stop debugging and continue operation of program."},
%Command{command: :debug, name: "debug", shortcut: "d", description: "Show detailed debug environment information."},
%Command{command: :stack, name: "stack", shortcut: "s", description: "Show current program stack."},
%Command{command: :pc, name: "pc", shortcut: "p", description: "Show current program pc."},
%Command{command: :help, name: "help", shortcut: "h", description: "Show this help screen."},
%Command{command: :where, name: "where", shortcut: "w", description: "Show where pc is in the currently executing code. (optional args: `where [length]`)"},
%Command{command: :machine_state, name: "machine", description: "Prints the current machine state."},
%Command{command: :memory, name: "memory", shortcut: "m", description: "Prints the current machine's memory."},
]
@current_instruction_start 3
@current_instruction_length 10
@chunk_size 10
@doc """
Enables the debugger.
Note: the debugger should only be run when debugging; it may
seriously degrade performance of the VM during normal
operations.
## Examples
iex> EVM.Debugger.enable
:ok
iex> EVM.Debugger.is_enabled?
true
iex> EVM.Debugger.disable
:ok
iex> EVM.Debugger.enable
:ok
iex> EVM.Debugger.disable
:ok
"""
@spec enable() :: :ok
def enable() do
Application.put_env(__MODULE__, :enabled, true)
EVM.Debugger.Breakpoint.init()
end
@doc """
Disables the debugger.
## Examples
iex> EVM.Debugger.disable
:ok
iex> EVM.Debugger.enable
:ok
iex> EVM.Debugger.disable
:ok
"""
@spec disable() :: :ok
def disable() do
Application.put_env(__MODULE__, :enabled, false)
end
@doc """
Returns true only if debugging is currently enabled.
This is set in the application config.
## Examples
iex> Application.put_env(EVM.Debugger, :enabled, true)
iex> EVM.Debugger.is_enabled?
true
iex> Application.put_env(EVM.Debugger, :enabled, false)
:ok
iex> Application.put_env(EVM.Debugger, :enabled, false)
iex> EVM.Debugger.is_enabled?
false
iex> EVM.Debugger.is_enabled?
false
"""
@spec is_enabled?() :: boolean()
def is_enabled? do
Application.get_env(__MODULE__, :enabled, false)
end
@doc """
Sets a new breakpoint based on supplied conditions.
## Examples
iex> id = EVM.Debugger.break_on(address: <<188, 31, 252, 22, 32, 218, 20, 104, 98, 74, 89, 108, 184, 65, 211, 94, 107, 47, 31, 182>>)
iex> EVM.Debugger.Breakpoint.get_breakpoint(id) |> Map.put(:id, nil)
%EVM.Debugger.Breakpoint{conditions: [address: <<188, 31, 252, 22, 32, 218, 20, 104, 98, 74, 89, 108, 184, 65, 211, 94, 107, 47, 31, 182>>], pc: :start}
"""
@spec break_on(keyword(Breakpoint.conditions)) :: Breakpoint.t
def break_on(conditions) do
Breakpoint.set_breakpoint(%Breakpoint{conditions: conditions, pc: :start})
end
@doc """
Return true if the currently line of code is tripped by
any previously set breakpoint.
## Examples
iex> EVM.Debugger.Breakpoint.set_breakpoint(%EVM.Debugger.Breakpoint{conditions: [address: <<25::160>>], pc: :next})
iex> state = <<>>
iex> machine_state = %EVM.MachineState{}
iex> sub_state = %EVM.SubState{}
iex> exec_env = %EVM.ExecEnv{address: <<25::160>>}
iex> EVM.Debugger.is_breakpoint?(state, machine_state, sub_state, exec_env) |> Map.put(:id, nil)
%EVM.Debugger.Breakpoint{conditions: [address: <<25::160>>], pc: :next}
iex> EVM.Debugger.Breakpoint.set_breakpoint(%EVM.Debugger.Breakpoint{conditions: [address: <<25::160>>], enabled: false})
iex> state = <<>>
iex> machine_state = %EVM.MachineState{}
iex> sub_state = %EVM.SubState{}
iex> exec_env = %EVM.ExecEnv{address: <<26::160>>}
iex> EVM.Debugger.is_breakpoint?(state, machine_state, sub_state, exec_env)
:continue
"""
@spec is_breakpoint?(EVM.state, MachineState.t, SubState.t, ExecEnv.t) :: :continue | Breakpoint.t
def is_breakpoint?(state, machine_state, sub_state, exec_env) do
Enum.find(Breakpoint.get_breakpoints(), :continue, fn breakpoint ->
Breakpoint.matches?(breakpoint, state, machine_state, sub_state, exec_env)
end)
end
@doc """
Breaks execution and begins a REPL to interact with the currently executing code.
## Examples
iex> id = EVM.Debugger.Breakpoint.set_breakpoint(%EVM.Debugger.Breakpoint{conditions: [address: <<25::160>>], enabled: false})
iex> breakpoint = EVM.Debugger.Breakpoint.get_breakpoint(id)
iex> state = <<>>
iex> machine_state = %EVM.MachineState{}
iex> sub_state = %EVM.SubState{}
iex> exec_env = %EVM.ExecEnv{address: <<26::160>>}
iex> EVM.Debugger.break(breakpoint, state, machine_state, sub_state, exec_env, ["continue"])
{ <<>>, %EVM.MachineState{}, %EVM.SubState{}, %EVM.ExecEnv{address: <<26::160>>} }
iex> id = EVM.Debugger.Breakpoint.set_breakpoint(%EVM.Debugger.Breakpoint{conditions: [address: <<25::160>>], enabled: false})
iex> breakpoint = EVM.Debugger.Breakpoint.get_breakpoint(id)
iex> state = <<>>
iex> machine_state = %EVM.MachineState{}
iex> sub_state = %EVM.SubState{}
iex> exec_env = %EVM.ExecEnv{address: <<26::160>>}
iex> EVM.Debugger.break(breakpoint, state, machine_state, sub_state, exec_env, ["zzzzzzz", "continue"])
{ <<>>, %EVM.MachineState{}, %EVM.SubState{}, %EVM.ExecEnv{address: <<26::160>>} }
"""
@spec break(Breakpoint.t, EVM.state, MachineState.t, SubState.t, ExecEnv.t, [String.t]) :: { EVM.state, MachineState.t, SubState.t, ExecEnv.t }
def break(breakpoint, state, machine_state, sub_state, exec_env, input_sequence \\ []) do
Breakpoint.clear_pc_if_one_time_break(breakpoint.id)
if breakpoint.pc == :start do
# If we're not next, we're likely a freshly hit breakpoint and should display a helpful prompt
# to the user.
IO.puts("\n\n-- Breakpoint ##{breakpoint.id} triggered with conditions #{Breakpoint.describe(breakpoint)} --")
end
IO.puts("")
print_machine_state(machine_state)
IO.puts("")
print_current_instruction(exec_env, machine_state)
if breakpoint.pc == :start do
IO.puts("Enter a debug command or type `h` for help.")
IO.puts("")
end
prompt(breakpoint, state, machine_state, sub_state, exec_env, input_sequence)
end
@spec print_machine_state(MachineState.t) :: any()
def print_machine_state(machine_state) do
IO.puts "gas: #{machine_state.gas} | pc: #{machine_state.program_counter} | memory: #{machine_state.memory |> byte_size} | words: #{machine_state.active_words} | # stack: #{machine_state.stack |> Enum.count}"
end
@spec print_current_instruction(MachineState.t, ExecEnv.t, integer(), integer()) :: any()
defp print_current_instruction(exec_env, machine_state, start \\ @current_instruction_start, len \\ @current_instruction_length) do
if is_nil(machine_state.program_counter) do
IO.puts("??? invalid pc ???")
IO.puts("")
else
machine_code = EVM.MachineCode.decompile(exec_env.machine_code)
index_width = if machine_code == [], do: 0, else: machine_code |> Enum.count |> :math.log10 |> :math.ceil |> round
machine_code
|> Enum.with_index
|> Enum.slice(max(machine_state.program_counter - start, 0), len)
|> Enum.each(fn {instruction, index} ->
pointer = if machine_state.program_counter == index, do: "----> ", else: " "
instruction_str = instruction |> to_string
index_str = "[#{index |> to_string |> String.pad_leading(index_width)}] "
IO.puts("#{pointer}#{index_str}#{instruction_str}")
end)
IO.puts("")
end
end
@spec prompt(Breakpoint.t, EVM.state, MachineState.t, SubState.t, ExecEnv.t, [String.t]) :: { EVM.state, MachineState.t, SubState.t, ExecEnv.t }
defp prompt(breakpoint, state, machine_state, sub_state, exec_env, [input | rest]), do: handle_input(input, breakpoint, state, machine_state, sub_state, exec_env, rest)
defp prompt(breakpoint, state, machine_state, sub_state, exec_env, []) do
IO.gets(">> ")
|> handle_input(breakpoint, state, machine_state, sub_state, exec_env, [])
end
@spec handle_input(String.t, Breakpoint.t, EVM.state, MachineState.t, SubState.t, ExecEnv.t, [String.t]) :: { EVM.state, MachineState.t, SubState.t, ExecEnv.t }
def handle_input(input, breakpoint, state, machine_state, sub_state, exec_env, input_sequence) do
case String.split(input |> String.trim, " ", trim: true) do
[] -> prompt(breakpoint, state, machine_state, sub_state, exec_env, input_sequence)
[command_string|args] ->
command = Enum.find(@commands, fn command ->
command.name == command_string or command.shortcut == String.first(command_string)
end)
if command do
handle_prompt(command.command, args, breakpoint, state, machine_state, sub_state, exec_env, input_sequence)
else
IO.puts("unknown command: `#{command_string}`")
prompt(breakpoint, state, machine_state, sub_state, exec_env, input_sequence)
end
end
end
# Handle prompt passes the prompt information bac
@spec handle_prompt(atom(), [String.t], Breakpoint.t, EVM.state, MachineState.t, SubState.t, ExecEnv.t, [String.t]) :: { EVM.state, MachineState.t, SubState.t, ExecEnv.t }
defp handle_prompt(command, args, breakpoint, state, machine_state, sub_state, exec_env, input_sequence)
defp handle_prompt(:help, _args, breakpoint, state, machine_state, sub_state, exec_env, input_sequence) do
# TODO: Handle args, make nicer
IO.puts("")
IO.puts("The EVM Debugger helps you understand a message or contract call in the EVM. Please choose a command from the following list of options:")
IO.puts("")
for command <- @commands do
[
command.name,
(if command.shortcut, do: "(#{command.shortcut})", else: nil),
"-",
command.description
]
|> Enum.filter(fn s -> not is_nil(s) end)
|> Enum.join(" ")
|> IO.puts
end
IO.puts("")
prompt(breakpoint, state, machine_state, sub_state, exec_env, input_sequence)
end
defp handle_prompt(:debug, _args, breakpoint, state, machine_state, sub_state, exec_env, input_sequence) do
IO.inspect([state, machine_state, sub_state, exec_env], limit: :infinity)
prompt(breakpoint, state, machine_state, sub_state, exec_env, input_sequence)
end
defp handle_prompt(:stack, _args, breakpoint, state, machine_state, sub_state, exec_env, input_sequence) do
IO.puts("")
IO.puts("Machine Stack")
stack_length = machine_state.stack |> Enum.count
tail = stack_length - 1
case machine_state.stack do
[] -> IO.puts("<empty>")
stack ->
width =
machine_state.stack
|> Enum.map(fn v -> v |> to_string |> String.length end)
|> Enum.max
for {v, i} <- stack |> Enum.with_index do
heading = case i do
0 -> "HEAD ->"
^tail -> "TAIL ->"
_ -> " "
end
IO.puts("#{heading} #{String.pad_leading(to_string(v), width)}")
end
end
IO.puts("")
prompt(breakpoint, state, machine_state, sub_state, exec_env, input_sequence)
end
defp handle_prompt(:memory, _args, breakpoint, state, machine_state=%EVM.MachineState{memory: memory}, sub_state, exec_env, input_sequence) do
total_size = byte_size(memory)
size_width = if total_size > 0, do: total_size |> :math.log10 |> :math.ceil |> round, else: 0
IO.puts("")
IO.puts("Memory (#{total_size} #{if total_size == 1, do: "byte", else: "bytes"})")
IO.puts("")
for {chunk, n} <- Enum.chunk(memory |> String.codepoints, @chunk_size, @chunk_size, []) |> Enum.with_index do
offset = n * @chunk_size
ascii =
chunk
|> Enum.map(fn codepoint -> if codepoint |> String.to_charlist |> List.first |> printable, do: codepoint, else: "." end)
|> Enum.join
|> String.pad_leading(@chunk_size)
hex =
chunk
|> Enum.map(fn codepoint -> Base.encode16(codepoint) end)
|> Enum.join(" ")
|> String.pad_leading(@chunk_size * 3, " ")
IO.puts "[#{String.pad_leading(offset |> to_string, size_width)}] #{hex} #{ascii}"
end
prompt(breakpoint, state, machine_state, sub_state, exec_env, input_sequence)
end
defp handle_prompt(:machine_state, _args, breakpoint, state, machine_state, sub_state, exec_env, input_sequence) do
print_machine_state(machine_state)
prompt(breakpoint, state, machine_state, sub_state, exec_env, input_sequence)
end
defp handle_prompt(:pc, _args, breakpoint, state, machine_state, sub_state, exec_env, input_sequence) do
IO.puts("The current program count is: #{machine_state.program_counter}")
prompt(breakpoint, state, machine_state, sub_state, exec_env, input_sequence)
end
defp handle_prompt(:where, args, breakpoint, state, machine_state, sub_state, exec_env, input_sequence) do
len = case Enum.at(args, 0, nil) do
nil -> @current_instruction_length
str ->
case Integer.parse(str) do
{len, ""} -> len
_els -> @current_instruction_length
end
end
print_current_instruction(exec_env, machine_state, @current_instruction_start, len * 2)
prompt(breakpoint, state, machine_state, sub_state, exec_env, input_sequence)
end
defp handle_prompt(:next, _args, breakpoint, state, machine_state, sub_state, exec_env, _input_sequence) do
Breakpoint.set_next(breakpoint.id)
{ state, machine_state, sub_state, exec_env }
end
defp handle_prompt(:continue, _args, _breakpoint, state, machine_state, sub_state, exec_env, _input_sequence) do
{ state, machine_state, sub_state, exec_env }
end
@spec printable(integer()) :: boolean()
defp printable(x) when x in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789', do: true
defp printable(_), do: false
end