From a5d9dbee7958e9f6b5fdf3f80401769799dceef1 Mon Sep 17 00:00:00 2001 From: chainhelen Date: Thu, 13 Feb 2020 11:29:21 -0600 Subject: [PATCH] pkg,service: add cmd `examinemem`(`x`) for examining memory. (#1814) According to #1800 #1584 #1038, `dlv` should enable the user to dive into memory. User can print binary data in specific memory address range. But not support for sepecific variable name or structures temporarily.(Because I have no idea that modify `print` command.) Close #1584. --- Documentation/cli/README.md | 13 ++++ Documentation/cli/starlark.md | 1 + _fixtures/examinememory.go | 25 ++++++ pkg/terminal/command.go | 93 +++++++++++++++++++++-- pkg/terminal/command_test.go | 39 ++++++++++ pkg/terminal/starbind/starlark_mapping.go | 38 +++++++++ service/api/prettyprint.go | 51 +++++++++++++ service/client.go | 5 ++ service/debugger/debugger.go | 19 +++++ service/rpc2/client.go | 11 +++ service/rpc2/server.go | 24 ++++++ 11 files changed, 314 insertions(+), 5 deletions(-) create mode 100644 _fixtures/examinememory.go diff --git a/Documentation/cli/README.md b/Documentation/cli/README.md index 4b06177a..32da2428 100644 --- a/Documentation/cli/README.md +++ b/Documentation/cli/README.md @@ -24,6 +24,7 @@ Command | Description [disassemble](#disassemble) | Disassembler. [down](#down) | Move the current frame down. [edit](#edit) | Open where you are in $DELVE_EDITOR or $EDITOR +[examinemem](#examinemem) | Examine memory: [exit](#exit) | Exit the debugger. [frame](#frame) | Set the current frame, or execute command on a different frame. [funcs](#funcs) | Print list of functions. @@ -210,6 +211,18 @@ If locspec is omitted edit will open the current source file in the editor, othe Aliases: ed +## examinemem +Examine memory: + + examinemem [-fmt ] [-len ]
+ +Format represents the data format and the value is one of this list (default hex): oct(octal), hex(hexadecimal), dec(decimal), bin(binary). +Length is the number of bytes (default 1) and must be less than or equal to 1000. +Address is the memory location of the target to examine. +For example: x -fmt hex -len 20 0xc00008af38 + +Aliases: x + ## exit Exit the debugger. diff --git a/Documentation/cli/starlark.md b/Documentation/cli/starlark.md index 6e173f30..193a89c5 100644 --- a/Documentation/cli/starlark.md +++ b/Documentation/cli/starlark.md @@ -29,6 +29,7 @@ create_breakpoint(Breakpoint) | Equivalent to API call [CreateBreakpoint](https: detach(Kill) | Equivalent to API call [Detach](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Detach) disassemble(Scope, StartPC, EndPC, Flavour) | Equivalent to API call [Disassemble](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Disassemble) eval(Scope, Expr, Cfg) | Equivalent to API call [Eval](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Eval) +examine_memory(Address, Length) | Equivalent to API call [ExamineMemory](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ExamineMemory) find_location(Scope, Loc, IncludeNonExecutableLines) | Equivalent to API call [FindLocation](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.FindLocation) function_return_locations(FnName) | Equivalent to API call [FunctionReturnLocations](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.FunctionReturnLocations) get_breakpoint(Id, Name) | Equivalent to API call [GetBreakpoint](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.GetBreakpoint) diff --git a/_fixtures/examinememory.go b/_fixtures/examinememory.go new file mode 100644 index 00000000..7e6dada6 --- /dev/null +++ b/_fixtures/examinememory.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "unsafe" +) + +func main() { + l := int(51) + bs := make([]byte, l) + for i := 0; i < l; i++ { + bs[i] = byte(i + int(10)) + } + + bsp := (*byte)(unsafe.Pointer(&bs[0])) + + bspUintptr := uintptr(unsafe.Pointer(bsp)) + + fmt.Printf("%#x\n", bspUintptr) + _ = *bsp + + bs[0] = 255 + + _ = *bsp +} diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go index f01eb7ef..ad4ffd5d 100644 --- a/pkg/terminal/command.go +++ b/pkg/terminal/command.go @@ -7,6 +7,10 @@ import ( "bytes" "errors" "fmt" + "github.com/cosiner/argv" + "github.com/go-delve/delve/service" + "github.com/go-delve/delve/service/api" + "github.com/go-delve/delve/service/debugger" "go/parser" "go/scanner" "io" @@ -20,11 +24,6 @@ import ( "strconv" "strings" "text/tabwriter" - - "github.com/cosiner/argv" - "github.com/go-delve/delve/service" - "github.com/go-delve/delve/service/api" - "github.com/go-delve/delve/service/debugger" ) const optimizedFunctionWarning = "Warning: debugging optimized function" @@ -368,6 +367,15 @@ Defines as an alias to or removes an alias.`}, If locspec is omitted edit will open the current source file in the editor, otherwise it will open the specified location.`}, {aliases: []string{"libraries"}, cmdFn: libraries, helpMsg: `List loaded dynamic libraries`}, + + {aliases: []string{"examinemem", "x"}, cmdFn: examineMemoryCmd, helpMsg: `Examine memory: + + examinemem [-fmt ] [-len ]
+ +Format represents the data format and the value is one of this list (default hex): oct(octal), hex(hexadecimal), dec(decimal), bin(binary). +Length is the number of bytes (default 1) and must be less than or equal to 1000. +Address is the memory location of the target to examine. +For example: x -fmt hex -len 20 0xc00008af38`}, } if client == nil || client.Recorded() { @@ -1349,6 +1357,81 @@ func edit(t *Term, ctx callContext, args string) error { return cmd.Run() } +func examineMemoryCmd(t *Term, ctx callContext, args string) error { + v := strings.FieldsFunc(args, func(c rune) bool { + return c == ' ' + }) + + var ( + address int64 + err error + ok bool + ) + + // Default value + priFmt := byte('x') + length := 1 + + for i := 0; i < len(v); i++ { + switch v[i] { + case "-fmt": + i++ + if i >= len(v) { + return fmt.Errorf("expected argument after -fmt") + } + fmtMapToPriFmt := map[string]byte{ + "oct": 'o', + "octal": 'o', + "hex": 'x', + "hexadecimal": 'x', + "dec": 'd', + "decimal": 'd', + "bin": 'b', + "binary": 'b', + } + priFmt, ok = fmtMapToPriFmt[v[i]] + if !ok { + return fmt.Errorf("%q is not a valid format", v[i]) + } + case "-len": + i++ + if i >= len(v) { + return fmt.Errorf("expected argument after -len") + } + var err error + length, err = strconv.Atoi(v[i]) + if err != nil || length <= 0 { + return fmt.Errorf("len must be an positive integer") + } + // TODO, maybe configured by user. + if length > 1000 { + return fmt.Errorf("len must be less than or equal to 1000") + } + default: + if i != len(v)-1 { + return fmt.Errorf("unknown option %q", v[i]) + } + // TODO, maybe we can support expression. + address, err = strconv.ParseInt(v[len(v)-1], 0, 64) + if err != nil { + return fmt.Errorf("convert address into uintptr type failed, %s", err) + } + } + } + + if address == 0 { + return fmt.Errorf("no address specified") + } + + memArea, err := t.client.ExamineMemory(uintptr(address), length) + if err != nil { + return err + } + + fmt.Println(api.PrettyExamineMemory(uintptr(address), memArea, priFmt)) + return nil +} + func printVar(t *Term, ctx callContext, args string) error { if len(args) == 0 { return fmt.Errorf("not enough arguments") diff --git a/pkg/terminal/command_test.go b/pkg/terminal/command_test.go index 0c1df4ab..2f74d904 100644 --- a/pkg/terminal/command_test.go +++ b/pkg/terminal/command_test.go @@ -993,3 +993,42 @@ func TestIssue1598(t *testing.T) { } }) } + +func TestExamineMemoryCmd(t *testing.T) { + withTestTerminal("examinememory", t, func(term *FakeTerminal) { + term.MustExec("break examinememory.go:19") + term.MustExec("break examinememory.go:24") + term.MustExec("continue") + + addressStr := strings.TrimSpace(term.MustExec("p bspUintptr")) + address, err := strconv.ParseInt(addressStr, 0, 64) + if err != nil { + t.Fatalf("could convert %s into int64, err %s", addressStr, err) + } + + res := term.MustExec("examinemem -len 52 -fmt hex " + addressStr) + t.Logf("the result of examining memory \n%s", res) + // check first line + firstLine := fmt.Sprintf("%#x: 0xa 0xb 0xc 0xd 0xe 0xf 0x10 0x11", address) + if !strings.Contains(res, firstLine) { + t.Fatalf("expected first line: %s", firstLine) + } + + // check last line + lastLine := fmt.Sprintf("%#x: 0x3a 0x3b 0x3c 0x0", address+6*8) + if !strings.Contains(res, lastLine) { + t.Fatalf("expected last line: %s", lastLine) + } + + // second examining memory + term.MustExec("continue") + res = term.MustExec("x -len 52 -fmt bin " + addressStr) + t.Logf("the second result of examining memory result \n%s", res) + + // check first line + firstLine = fmt.Sprintf("%#x: 11111111 00001011 00001100 00001101", address) + if !strings.Contains(res, firstLine) { + t.Fatalf("expected first line: %s", firstLine) + } + }) +} diff --git a/pkg/terminal/starbind/starlark_mapping.go b/pkg/terminal/starbind/starlark_mapping.go index f00059a4..4bf55bbe 100644 --- a/pkg/terminal/starbind/starlark_mapping.go +++ b/pkg/terminal/starbind/starlark_mapping.go @@ -450,6 +450,44 @@ func (env *Env) starlarkPredeclare() starlark.StringDict { } return env.interfaceToStarlarkValue(rpcRet), nil }) + r["examine_memory"] = starlark.NewBuiltin("examine_memory", func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + if err := isCancelled(thread); err != nil { + return starlark.None, decorateError(thread, err) + } + var rpcArgs rpc2.ExamineMemoryIn + var rpcRet rpc2.ExaminedMemoryOut + if len(args) > 0 && args[0] != starlark.None { + err := unmarshalStarlarkValue(args[0], &rpcArgs.Address, "Address") + if err != nil { + return starlark.None, decorateError(thread, err) + } + } + if len(args) > 1 && args[1] != starlark.None { + err := unmarshalStarlarkValue(args[1], &rpcArgs.Length, "Length") + if err != nil { + return starlark.None, decorateError(thread, err) + } + } + for _, kv := range kwargs { + var err error + switch kv[0].(starlark.String) { + case "Address": + err = unmarshalStarlarkValue(kv[1], &rpcArgs.Address, "Address") + case "Length": + err = unmarshalStarlarkValue(kv[1], &rpcArgs.Length, "Length") + default: + err = fmt.Errorf("unknown argument %q", kv[0]) + } + if err != nil { + return starlark.None, decorateError(thread, err) + } + } + err := env.ctx.Client().CallAPI("ExamineMemory", &rpcArgs, &rpcRet) + if err != nil { + return starlark.None, err + } + return env.interfaceToStarlarkValue(rpcRet), nil + }) r["find_location"] = starlark.NewBuiltin("find_location", func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { if err := isCancelled(thread); err != nil { return starlark.None, decorateError(thread, err) diff --git a/service/api/prettyprint.go b/service/api/prettyprint.go index 5100bf8f..b62e37a8 100644 --- a/service/api/prettyprint.go +++ b/service/api/prettyprint.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "reflect" + "strconv" "strings" ) @@ -354,3 +355,53 @@ func (v *Variable) writeSliceOrArrayTo(buf io.Writer, newlines bool, indent stri fmt.Fprint(buf, "]") } + +func PrettyExamineMemory(address uintptr, memArea []byte, format byte) string { + cols := 8 + // Avoid emitting rows that are too long when using binary format + if format == 'b' { + cols = 4 + } + + l := len(memArea) + rows := l / cols + if l%cols != 0 { + rows++ + } + + var colFormat string + // Leading zero and occupy 8 for binary + if format == 'b' { + colFormat = " %#08b" + } else { + var maxColCharNum int + for i := 0; i < rows; i++ { + for j := 0; j < cols && i*cols+j < l; j++ { + curColCharNum := len(fmt.Sprintf("%#"+string(format), memArea[i*cols+j])) + if curColCharNum > maxColCharNum { + maxColCharNum = curColCharNum + } + } + } + colFormat = " %#-" + strconv.Itoa(maxColCharNum) + string(format) + } + + lines := "" + for i := 0; i < rows; i++ { + lines += fmt.Sprintf("%#x:", address) + for j := 0; j < cols && i*cols+j < l; j++ { + curOutput := fmt.Sprintf(colFormat, memArea[i*cols+j]) + + // Diffrent versions of golang output differently if binary. + // See https://ci.appveyor.com/project/derekparker/delve-facy3/builds/30179356. + // Remove prefix `0b` if binary in some versions of golang because it is not graceful. + if format == 'b' && strings.Contains(curOutput, "0b") { + curOutput = " " + curOutput[6:] + } + lines += curOutput + } + lines += "\n" + address += uintptr(cols) + } + return lines +} diff --git a/service/client.go b/service/client.go index 6b617cd7..5544091a 100644 --- a/service/client.go +++ b/service/client.go @@ -147,6 +147,11 @@ type Client interface { // ListDynamicLibraries returns a list of loaded dynamic libraries. ListDynamicLibraries() ([]api.Image, error) + // ExamineMemory returns the raw memory stored at the given address. + // The amount of data to be read is specified by length which must be less than or equal to 1000. + // This function will return an error if it reads less than `length` bytes. + ExamineMemory(address uintptr, length int) ([]byte, error) + // Disconnect closes the connection to the server without sending a Detach request first. // If cont is true a continue command will be sent instead. Disconnect(cont bool) error diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index c0a05f47..d508661b 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -1313,6 +1313,25 @@ func (d *Debugger) ListDynamicLibraries() []api.Image { return r } +// ExamineMemory returns the raw memory stored at the given address. +// The amount of data to be read is specified by length. +// This function will return an error if it reads less than `length` bytes. +func (d *Debugger) ExamineMemory(address uintptr, length int) ([]byte, error) { + d.processMutex.Lock() + defer d.processMutex.Unlock() + + thread := d.target.CurrentThread() + data := make([]byte, length) + n, err := thread.ReadMemory(data, address) + if err != nil { + return nil, err + } + if length != n { + return nil, errors.New("the specific range has exceeded readable area") + } + return data, nil +} + func (d *Debugger) GetVersion(out *api.GetVersionOut) error { if d.config.CoreFile != "" { if d.config.Backend == "rr" { diff --git a/service/rpc2/client.go b/service/rpc2/client.go index 6f2f31db..51edc953 100644 --- a/service/rpc2/client.go +++ b/service/rpc2/client.go @@ -413,6 +413,17 @@ func (c *RPCClient) ListDynamicLibraries() ([]api.Image, error) { return out.List, nil } +func (c *RPCClient) ExamineMemory(address uintptr, count int) ([]byte, error) { + out := &ExaminedMemoryOut{} + + err := c.call("ExamineMemory", ExamineMemoryIn{Length: count, Address: address}, out) + if err != nil { + return nil, err + } + + return out.Mem, nil +} + func (c *RPCClient) call(method string, args, reply interface{}) error { return c.client.Call("RPCServer."+method, args, reply) } diff --git a/service/rpc2/server.go b/service/rpc2/server.go index 68b84c6d..defbe212 100644 --- a/service/rpc2/server.go +++ b/service/rpc2/server.go @@ -751,3 +751,27 @@ func (s *RPCServer) ListPackagesBuildInfo(in ListPackagesBuildInfoIn, out *ListP out.List = s.debugger.ListPackagesBuildInfo(in.IncludeFiles) return nil } + +// ExamineMemoryIn holds the arguments of ExamineMemory +type ExamineMemoryIn struct { + Address uintptr + Length int +} + +// ExaminedMemoryOut holds the return values of ExamineMemory +type ExaminedMemoryOut struct { + Mem []byte +} + +func (s *RPCServer) ExamineMemory(arg ExamineMemoryIn, out *ExaminedMemoryOut) error { + if arg.Length > 1000 { + return fmt.Errorf("len must be less than or equal to 1000") + } + Mem, err := s.debugger.ExamineMemory(arg.Address, arg.Length) + if err != nil { + return err + } + + out.Mem = Mem + return nil +} -- GitLab