From 6da458365df473263d8ba5c12dd357239673a2f6 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 8 Sep 2021 17:27:11 +0300 Subject: [PATCH 1/2] vm CLI: allow to dump slots --- docs/vm.md | 8 +++++ pkg/vm/cli/cli.go | 40 +++++++++++++++++++++ pkg/vm/cli/cli_test.go | 81 ++++++++++++++++++++++++++++++++++++++++++ pkg/vm/context.go | 22 ++++++++++++ pkg/vm/slot.go | 15 ++++++++ 5 files changed, 166 insertions(+) diff --git a/docs/vm.md b/docs/vm.md index 9510e2d49..68f403f00 100644 --- a/docs/vm.md +++ b/docs/vm.md @@ -37,6 +37,7 @@ NEO-GO-VM > NEO-GO-VM > help Commands: + aslot Show arguments slot contents break Place a breakpoint clear clear the screen cont Continue execution of the current loaded script @@ -49,9 +50,11 @@ Commands: loadgo Compile and load a Go file with the manifest into the VM loadhex Load a hex-encoded script string into the VM loadnef Load a NEF-consistent script into the VM + lslot Show local slot contents ops Dump opcodes of the current loaded program parse Parse provided argument and convert it into other possible formats run Execute the current loaded script + sslot Show static slot contents step Step (n) instruction in the program stepinto Stepinto instruction to take in the debugger stepout Stepout instruction to take in the debugger @@ -224,3 +227,8 @@ NEO-GO-VM > estack There is one more stack that you can inspect. - `istack` invocation stack +There are slots that you can inspect. +- `aslot` dumps arguments slot contents. +- `lslot` dumps local slot contents. +- `sslot` dumps static slot contents. + diff --git a/pkg/vm/cli/cli.go b/pkg/vm/cli/cli.go index 0045f12f4..e5e6e915a 100644 --- a/pkg/vm/cli/cli.go +++ b/pkg/vm/cli/cli.go @@ -73,6 +73,24 @@ var commands = []*ishell.Cmd{ LongHelp: "Show invocation stack contents", Func: handleXStack, }, + { + Name: "sslot", + Help: "Show static slot contents", + LongHelp: "Show static slot contents", + Func: handleSlots, + }, + { + Name: "lslot", + Help: "Show local slot contents", + LongHelp: "Show local slot contents", + Func: handleSlots, + }, + { + Name: "aslot", + Help: "Show arguments slot contents", + LongHelp: "Show arguments slot contents", + Func: handleSlots, + }, { Name: "loadnef", Help: "Load a NEF-consistent script into the VM", @@ -286,6 +304,28 @@ func handleXStack(c *ishell.Context) { c.Println(v.Stack(c.Cmd.Name)) } +func handleSlots(c *ishell.Context) { + v := getVMFromContext(c) + vmCtx := v.Context() + if vmCtx == nil { + c.Err(errors.New("no program loaded")) + return + } + var rawSlot string + switch c.Cmd.Name { + case "sslot": + rawSlot = vmCtx.DumpStaticSlot() + case "lslot": + rawSlot = vmCtx.DumpLocalSlot() + case "aslot": + rawSlot = vmCtx.DumpArgumentsSlot() + default: + c.Err(errors.New("unknown slot")) + return + } + c.Println(rawSlot) +} + func handleLoadNEF(c *ishell.Context) { v := getVMFromContext(c) if len(c.Args) < 2 { diff --git a/pkg/vm/cli/cli_test.go b/pkg/vm/cli/cli_test.go index 0ff8b0cd3..246e83ad5 100644 --- a/pkg/vm/cli/cli_test.go +++ b/pkg/vm/cli/cli_test.go @@ -25,6 +25,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" "go.uber.org/atomic" ) @@ -129,6 +130,37 @@ func (e *executor) checkStack(t *testing.T, items ...interface{}) { require.NoError(t, err) } +func (e *executor) checkSlot(t *testing.T, items ...interface{}) { + d := json.NewDecoder(e.out) + var actual interface{} + require.NoError(t, d.Decode(&actual)) + rawActual, err := json.Marshal(actual) + require.NoError(t, err) + + expected := make([]json.RawMessage, len(items)) + for i := range items { + if items[i] == nil { + expected[i] = []byte("null") + continue + } + data, err := stackitem.ToJSONWithTypes(stackitem.Make(items[i])) + require.NoError(t, err) + expected[i] = data + } + rawExpected, err := json.MarshalIndent(expected, "", " ") + require.NoError(t, err) + require.JSONEq(t, string(rawExpected), string(rawActual)) + + // Decoder has it's own buffer, we need to return unread part to the output. + outRemain := e.out.String() + e.out.Reset() + _, err = gio.Copy(e.out, d.Buffered()) + require.NoError(t, err) + e.out.WriteString(outRemain) + _, err = e.out.ReadString('\n') + require.NoError(t, err) +} + func TestLoad(t *testing.T) { script := []byte{byte(opcode.PUSH3), byte(opcode.PUSH4), byte(opcode.ADD)} t.Run("loadhex", func(t *testing.T) { @@ -376,6 +408,55 @@ func TestBreakpoint(t *testing.T) { e.checkStack(t, 9) } +func TestDumpSSlot(t *testing.T) { + w := io.NewBufBinWriter() + emit.Opcodes(w.BinWriter, opcode.INITSSLOT, 2, // init static slot with size=2 + opcode.PUSH5, opcode.STSFLD, 1, // put `int(5)` to sslot[1]; sslot[0] is nil + opcode.LDSFLD1) // put sslot[1] to the top of estack + e := newTestVMCLI(t) + e.runProg(t, + "loadhex "+hex.EncodeToString(w.Bytes()), + "break 5", + "step", "sslot", + "cont", "estack", + ) + e.checkNextLine(t, "READY: loaded 6 instructions") + e.checkNextLine(t, "breakpoint added at instruction 5") + + e.checkNextLine(t, "at breakpoint 5.*LDSFLD1") + e.checkSlot(t, nil, 5) + + e.checkStack(t, 5) +} + +func TestDumpLSlot_DumpASlot(t *testing.T) { + w := io.NewBufBinWriter() + emit.Opcodes(w.BinWriter, opcode.PUSH4, opcode.PUSH5, opcode.PUSH6, // items for args slot + opcode.INITSLOT, 2, 3, // init local slot with size=2 and args slot with size 3 + opcode.PUSH7, opcode.STLOC1, // put `int(7)` to lslot[1]; lslot[0] is nil + opcode.LDLOC, 1) // put lslot[1] to the top of estack + e := newTestVMCLI(t) + e.runProg(t, + "loadhex "+hex.EncodeToString(w.Bytes()), + "break 6", + "break 8", + "cont", "aslot", + "cont", "lslot", + "cont", "estack", + ) + e.checkNextLine(t, "READY: loaded 10 instructions") + e.checkNextLine(t, "breakpoint added at instruction 6") + e.checkNextLine(t, "breakpoint added at instruction 8") + + e.checkNextLine(t, "at breakpoint 6.*PUSH7") + e.checkSlot(t, 6, 5, 4) // args slot + + e.checkNextLine(t, "at breakpoint 8.*LDLOC") + e.checkSlot(t, nil, 7) // local slot + + e.checkStack(t, 7) +} + func TestStep(t *testing.T) { script := hex.EncodeToString([]byte{ byte(opcode.PUSH0), byte(opcode.PUSH1), byte(opcode.PUSH2), byte(opcode.PUSH3), diff --git a/pkg/vm/context.go b/pkg/vm/context.go index 1b0f1630b..fc5388e02 100644 --- a/pkg/vm/context.go +++ b/pkg/vm/context.go @@ -2,6 +2,7 @@ package vm import ( "encoding/binary" + "encoding/json" "errors" "fmt" "math/big" @@ -278,6 +279,27 @@ func (c *Context) IsDeployed() bool { return c.NEF != nil } +// DumpStaticSlot returns json formatted representation of the given slot. +func (c *Context) DumpStaticSlot() string { + return dumpSlot(c.static) +} + +// DumpLocalSlot returns json formatted representation of the given slot. +func (c *Context) DumpLocalSlot() string { + return dumpSlot(c.local) +} + +// DumpArgumentsSlot returns json formatted representation of the given slot. +func (c *Context) DumpArgumentsSlot() string { + return dumpSlot(c.arguments) +} + +// dumpSlot returns json formatted representation of the given slot. +func dumpSlot(s *Slot) string { + b, _ := json.MarshalIndent(s, "", " ") + return string(b) +} + // getContextScriptHash returns script hash of the invocation stack element // number n. func (v *VM) getContextScriptHash(n int) util.Uint160 { diff --git a/pkg/vm/slot.go b/pkg/vm/slot.go index 75601b309..634891f18 100644 --- a/pkg/vm/slot.go +++ b/pkg/vm/slot.go @@ -1,6 +1,8 @@ package vm import ( + "encoding/json" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) @@ -66,3 +68,16 @@ func (s *Slot) Size() int { } return len(s.storage) } + +// MarshalJSON implements JSON marshalling interface. +func (s *Slot) MarshalJSON() ([]byte, error) { + items := s.storage + arr := make([]json.RawMessage, len(items)) + for i := range items { + data, err := stackitem.ToJSONWithTypes(items[i]) + if err == nil { + arr[i] = data + } + } + return json.Marshal(arr) +} From 3b04b6d23852cd43768aeac8bf55bc829e878258 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 8 Sep 2021 18:51:34 +0300 Subject: [PATCH 2/2] vm: refactor stack dump commands --- pkg/vm/cli/cli.go | 14 ++++++++++++-- pkg/vm/vm.go | 21 ++++++++++++--------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/pkg/vm/cli/cli.go b/pkg/vm/cli/cli.go index e5e6e915a..671693834 100644 --- a/pkg/vm/cli/cli.go +++ b/pkg/vm/cli/cli.go @@ -301,7 +301,17 @@ func handleBreak(c *ishell.Context) { func handleXStack(c *ishell.Context) { v := getVMFromContext(c) - c.Println(v.Stack(c.Cmd.Name)) + var stackDump string + switch c.Cmd.Name { + case "estack": + stackDump = v.DumpEStack() + case "istack": + stackDump = v.DumpIStack() + default: + c.Err(errors.New("unknown stack")) + return + } + c.Println(stackDump) } func handleSlots(c *ishell.Context) { @@ -470,7 +480,7 @@ func runVMWithHandling(c *ishell.Context, v *vm.VM) { case v.HasFailed(): message = "" // the error will be printed on return case v.HasHalted(): - message = v.Stack("estack") + message = v.DumpEStack() case v.AtBreakpoint(): ctx := v.Context() if ctx.NextIP() < ctx.LenInstr() { diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 5b2d83254..0211ce79e 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -334,15 +334,18 @@ func (v *VM) PopResult() interface{} { return v.estack.Pop().Value() } -// Stack returns json formatted representation of the given stack. -func (v *VM) Stack(n string) string { - var s *Stack - if n == "istack" { - s = &v.istack - } - if n == "estack" { - s = v.estack - } +// DumpIStack returns json formatted representation of the invocation stack. +func (v *VM) DumpIStack() string { + return dumpStack(&v.istack) +} + +// DumpEStack returns json formatted representation of the execution stack. +func (v *VM) DumpEStack() string { + return dumpStack(v.estack) +} + +// dumpStack returns json formatted representation of the given stack. +func dumpStack(s *Stack) string { b, _ := json.MarshalIndent(s, "", " ") return string(b) }