Merge pull request #2164 from nspcc-dev/vm/slots

vm: allow to dump slots
This commit is contained in:
Roman Khimov 2021-09-09 15:42:16 +03:00 committed by GitHub
commit 04bbeccca6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 190 additions and 11 deletions

View file

@ -37,6 +37,7 @@ NEO-GO-VM >
NEO-GO-VM > help NEO-GO-VM > help
Commands: Commands:
aslot Show arguments slot contents
break Place a breakpoint break Place a breakpoint
clear clear the screen clear clear the screen
cont Continue execution of the current loaded script 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 loadgo Compile and load a Go file with the manifest into the VM
loadhex Load a hex-encoded script string into the VM loadhex Load a hex-encoded script string into the VM
loadnef Load a NEF-consistent script 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 ops Dump opcodes of the current loaded program
parse Parse provided argument and convert it into other possible formats parse Parse provided argument and convert it into other possible formats
run Execute the current loaded script run Execute the current loaded script
sslot Show static slot contents
step Step (n) instruction in the program step Step (n) instruction in the program
stepinto Stepinto instruction to take in the debugger stepinto Stepinto instruction to take in the debugger
stepout Stepout 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. There is one more stack that you can inspect.
- `istack` invocation stack - `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.

View file

@ -73,6 +73,24 @@ var commands = []*ishell.Cmd{
LongHelp: "Show invocation stack contents", LongHelp: "Show invocation stack contents",
Func: handleXStack, 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", Name: "loadnef",
Help: "Load a NEF-consistent script into the VM", Help: "Load a NEF-consistent script into the VM",
@ -283,7 +301,39 @@ func handleBreak(c *ishell.Context) {
func handleXStack(c *ishell.Context) { func handleXStack(c *ishell.Context) {
v := getVMFromContext(c) 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) {
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) { func handleLoadNEF(c *ishell.Context) {
@ -430,7 +480,7 @@ func runVMWithHandling(c *ishell.Context, v *vm.VM) {
case v.HasFailed(): case v.HasFailed():
message = "" // the error will be printed on return message = "" // the error will be printed on return
case v.HasHalted(): case v.HasHalted():
message = v.Stack("estack") message = v.DumpEStack()
case v.AtBreakpoint(): case v.AtBreakpoint():
ctx := v.Context() ctx := v.Context()
if ctx.NextIP() < ctx.LenInstr() { if ctx.NextIP() < ctx.LenInstr() {

View file

@ -25,6 +25,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/vm" "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/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/atomic" "go.uber.org/atomic"
) )
@ -129,6 +130,37 @@ func (e *executor) checkStack(t *testing.T, items ...interface{}) {
require.NoError(t, err) 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) { func TestLoad(t *testing.T) {
script := []byte{byte(opcode.PUSH3), byte(opcode.PUSH4), byte(opcode.ADD)} script := []byte{byte(opcode.PUSH3), byte(opcode.PUSH4), byte(opcode.ADD)}
t.Run("loadhex", func(t *testing.T) { t.Run("loadhex", func(t *testing.T) {
@ -376,6 +408,55 @@ func TestBreakpoint(t *testing.T) {
e.checkStack(t, 9) 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) { func TestStep(t *testing.T) {
script := hex.EncodeToString([]byte{ script := hex.EncodeToString([]byte{
byte(opcode.PUSH0), byte(opcode.PUSH1), byte(opcode.PUSH2), byte(opcode.PUSH3), byte(opcode.PUSH0), byte(opcode.PUSH1), byte(opcode.PUSH2), byte(opcode.PUSH3),

View file

@ -2,6 +2,7 @@ package vm
import ( import (
"encoding/binary" "encoding/binary"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"math/big" "math/big"
@ -278,6 +279,27 @@ func (c *Context) IsDeployed() bool {
return c.NEF != nil 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 // getContextScriptHash returns script hash of the invocation stack element
// number n. // number n.
func (v *VM) getContextScriptHash(n int) util.Uint160 { func (v *VM) getContextScriptHash(n int) util.Uint160 {

View file

@ -1,6 +1,8 @@
package vm package vm
import ( import (
"encoding/json"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
) )
@ -66,3 +68,16 @@ func (s *Slot) Size() int {
} }
return len(s.storage) 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)
}

View file

@ -334,15 +334,18 @@ func (v *VM) PopResult() interface{} {
return v.estack.Pop().Value() return v.estack.Pop().Value()
} }
// Stack returns json formatted representation of the given stack. // DumpIStack returns json formatted representation of the invocation stack.
func (v *VM) Stack(n string) string { func (v *VM) DumpIStack() string {
var s *Stack return dumpStack(&v.istack)
if n == "istack" {
s = &v.istack
} }
if n == "estack" {
s = v.estack // 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, "", " ") b, _ := json.MarshalIndent(s, "", " ")
return string(b) return string(b)
} }