forked from TrueCloudLab/neoneo-go
Merge pull request #2164 from nspcc-dev/vm/slots
vm: allow to dump slots
This commit is contained in:
commit
04bbeccca6
6 changed files with 190 additions and 11 deletions
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
17
pkg/vm/vm.go
17
pkg/vm/vm.go
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue