From d925f6b7198c90bf0f6f8e2142c568cd613150bb Mon Sep 17 00:00:00 2001 From: Alessandro Arzilli Date: Mon, 24 Feb 2020 19:47:02 +0100 Subject: [PATCH] proc,service: allow printing registers for arbitrary frames (#1875) Adds an optional scope prefix to the `regs` command which allows printing registers for any stack frame (as long as they were somehow saved). Issue #1838 is not yet to be closed since we are still not recovering the registers of a segfaulting frame. Updates #1838 --- Documentation/cli/starlark.md | 2 +- pkg/proc/amd64_arch.go | 30 ++++++----- pkg/proc/arch.go | 2 +- pkg/proc/arm64_arch.go | 24 +++++++-- pkg/proc/core/core_test.go | 30 ++++++----- pkg/terminal/command.go | 8 ++- pkg/terminal/command_test.go | 2 +- pkg/terminal/starbind/starlark_mapping.go | 11 ++++ service/api/conversions.go | 17 ++++-- service/api/types.go | 5 +- service/client.go | 6 ++- service/debugger/debugger.go | 65 +++++++++++++++++++---- service/rpc1/server.go | 2 +- service/rpc2/client.go | 10 +++- service/rpc2/server.go | 7 ++- service/test/integration2_test.go | 27 ++++++++-- 16 files changed, 187 insertions(+), 61 deletions(-) diff --git a/Documentation/cli/starlark.md b/Documentation/cli/starlark.md index 193a89c5..1de8a5ff 100644 --- a/Documentation/cli/starlark.md +++ b/Documentation/cli/starlark.md @@ -45,7 +45,7 @@ goroutines(Start, Count) | Equivalent to API call [ListGoroutines](https://godoc local_vars(Scope, Cfg) | Equivalent to API call [ListLocalVars](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListLocalVars) package_vars(Filter, Cfg) | Equivalent to API call [ListPackageVars](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListPackageVars) packages_build_info(IncludeFiles) | Equivalent to API call [ListPackagesBuildInfo](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListPackagesBuildInfo) -registers(ThreadID, IncludeFp) | Equivalent to API call [ListRegisters](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListRegisters) +registers(ThreadID, IncludeFp, Scope) | Equivalent to API call [ListRegisters](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListRegisters) sources(Filter) | Equivalent to API call [ListSources](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListSources) threads() | Equivalent to API call [ListThreads](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListThreads) types(Filter) | Equivalent to API call [ListTypes](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListTypes) diff --git a/pkg/proc/amd64_arch.go b/pkg/proc/amd64_arch.go index 31f7e332..202dc53b 100644 --- a/pkg/proc/amd64_arch.go +++ b/pkg/proc/amd64_arch.go @@ -426,30 +426,34 @@ func (a *AMD64) AddrAndStackRegsToDwarfRegisters(staticBase, pc, sp, bp, lr uint } } -func (a *AMD64) DwarfRegisterToString(name string, reg *op.DwarfRegister) string { - name = strings.ToLower(name) - switch name { +func (a *AMD64) DwarfRegisterToString(i int, reg *op.DwarfRegister) (name string, floatingPoint bool, repr string) { + name, ok := amd64DwarfToName[i] + if !ok { + name = fmt.Sprintf("unknown%d", i) + } + + switch n := strings.ToLower(name); n { case "rflags": - return eflagsDescription.Describe(reg.Uint64Val, 64) + return name, false, eflagsDescription.Describe(reg.Uint64Val, 64) case "cw", "sw", "tw", "fop": - return fmt.Sprintf("%#04x", reg.Uint64Val) + return name, true, fmt.Sprintf("%#04x", reg.Uint64Val) case "mxcsr_mask": - return fmt.Sprintf("%#08x", reg.Uint64Val) + return name, true, fmt.Sprintf("%#08x", reg.Uint64Val) case "mxcsr": - return mxcsrDescription.Describe(reg.Uint64Val, 32) + return name, true, mxcsrDescription.Describe(reg.Uint64Val, 32) default: - if reg.Bytes != nil && strings.HasPrefix(name, "xmm") { - return formatSSEReg(reg.Bytes) - } else if reg.Bytes != nil && strings.HasPrefix(name, "st(") { - return formatX87Reg(reg.Bytes) + if reg.Bytes != nil && strings.HasPrefix(n, "xmm") { + return name, true, formatSSEReg(reg.Bytes) + } else if reg.Bytes != nil && strings.HasPrefix(n, "st(") { + return name, true, formatX87Reg(reg.Bytes) } else if reg.Bytes == nil || (reg.Bytes != nil && len(reg.Bytes) <= 8) { - return fmt.Sprintf("%#016x", reg.Uint64Val) + return name, false, fmt.Sprintf("%#016x", reg.Uint64Val) } else { - return fmt.Sprintf("%#x", reg.Bytes) + return name, false, fmt.Sprintf("%#x", reg.Bytes) } } } diff --git a/pkg/proc/arch.go b/pkg/proc/arch.go index 3c27181e..28d16635 100644 --- a/pkg/proc/arch.go +++ b/pkg/proc/arch.go @@ -21,7 +21,7 @@ type Arch interface { RegSize(uint64) int RegistersToDwarfRegisters(uint64, Registers) op.DwarfRegisters AddrAndStackRegsToDwarfRegisters(uint64, uint64, uint64, uint64, uint64) op.DwarfRegisters - DwarfRegisterToString(string, *op.DwarfRegister) string + DwarfRegisterToString(int, *op.DwarfRegister) (string, bool, string) } const ( diff --git a/pkg/proc/arm64_arch.go b/pkg/proc/arm64_arch.go index 786e0251..5ab22f0f 100644 --- a/pkg/proc/arm64_arch.go +++ b/pkg/proc/arm64_arch.go @@ -411,8 +411,22 @@ func (a *ARM64) AddrAndStackRegsToDwarfRegisters(staticBase, pc, sp, bp, lr uint } } -func (a *ARM64) DwarfRegisterToString(name string, reg *op.DwarfRegister) string { - if reg.Bytes != nil && (name[0] == 'v' || name[0] == 'V') { +func (a *ARM64) DwarfRegisterToString(i int, reg *op.DwarfRegister) (name string, floatingPoint bool, repr string) { + // see arm64DwarfToHardware table for explanation + switch { + case i <= 30: + name = fmt.Sprintf("X%d", i) + case i == 31: + name = "SP" + case i == 32: + name = "PC" + case i >= 64 && i <= 95: + name = fmt.Sprintf("V%d", i-64) + default: + name = fmt.Sprintf("unknown%d", i) + } + + if reg.Bytes != nil && name[0] == 'V' { buf := bytes.NewReader(reg.Bytes) var out bytes.Buffer @@ -445,9 +459,9 @@ func (a *ARM64) DwarfRegisterToString(name string, reg *op.DwarfRegister) string } fmt.Fprintf(&out, "\tv4_float={ %g %g %g %g }", v4[0], v4[1], v4[2], v4[3]) - return out.String() + return name, true, out.String() } else if reg.Bytes == nil || (reg.Bytes != nil && len(reg.Bytes) < 16) { - return fmt.Sprintf("%#016x", reg.Uint64Val) + return name, false, fmt.Sprintf("%#016x", reg.Uint64Val) } - return fmt.Sprintf("%#x", reg.Bytes) + return name, false, fmt.Sprintf("%#x", reg.Bytes) } diff --git a/pkg/proc/core/core_test.go b/pkg/proc/core/core_test.go index 9e6be602..81ac81cf 100644 --- a/pkg/proc/core/core_test.go +++ b/pkg/proc/core/core_test.go @@ -185,6 +185,17 @@ func withCoreFile(t *testing.T, name, args string) *proc.Target { return p } +func logRegisters(t *testing.T, regs proc.Registers, arch proc.Arch) { + dregs := arch.RegistersToDwarfRegisters(0, regs) + for i, reg := range dregs.Regs { + if reg == nil { + continue + } + name, _, value := arch.DwarfRegisterToString(i, reg) + t.Logf("%s = %s", name, value) + } +} + func TestCore(t *testing.T) { if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" { return @@ -243,11 +254,7 @@ func TestCore(t *testing.T) { if err != nil { t.Fatalf("Couldn't get current thread registers: %v", err) } - regslice := regs.Slice(true) - arch := p.BinInfo().Arch - for _, reg := range regslice { - t.Logf("%s = %s", reg.Name, arch.DwarfRegisterToString(reg.Name, reg.Reg)) - } + logRegisters(t, regs, p.BinInfo().Arch) } func TestCoreFpRegisters(t *testing.T) { @@ -314,18 +321,17 @@ func TestCoreFpRegisters(t *testing.T) { } arch := p.BinInfo().Arch - for _, reg := range regs.Slice(true) { - t.Logf("%s = %s", reg.Name, arch.DwarfRegisterToString(reg.Name, reg.Reg)) - } + logRegisters(t, regs, arch) + dregs := arch.RegistersToDwarfRegisters(0, regs) for _, regtest := range regtests { found := false - for _, reg := range regs.Slice(true) { - if reg.Name == regtest.name { + for i, reg := range dregs.Regs { + regname, _, regval := arch.DwarfRegisterToString(i, reg) + if reg != nil && regname == regtest.name { found = true - regval := arch.DwarfRegisterToString(reg.Name, reg.Reg) if !strings.HasPrefix(regval, regtest.value) { - t.Fatalf("register %s expected %q got %q", reg.Name, regtest.value, regval) + t.Fatalf("register %s expected %q got %q", regname, regtest.value, regval) } } } diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go index c556365c..944d5b79 100644 --- a/pkg/terminal/command.go +++ b/pkg/terminal/command.go @@ -1638,7 +1638,13 @@ func regs(t *Term, ctx callContext, args string) error { if args == "-a" { includeFp = true } - regs, err := t.client.ListRegisters(0, includeFp) + var regs api.Registers + var err error + if ctx.Scope.GoroutineID < 0 && ctx.Scope.Frame == 0 { + regs, err = t.client.ListThreadRegisters(0, includeFp) + } else { + regs, err = t.client.ListScopeRegisters(ctx.Scope, includeFp) + } if err != nil { return err } diff --git a/pkg/terminal/command_test.go b/pkg/terminal/command_test.go index 2f74d904..a547f935 100644 --- a/pkg/terminal/command_test.go +++ b/pkg/terminal/command_test.go @@ -967,7 +967,7 @@ func TestIssue1493(t *testing.T) { ra := term.MustExec("regs -a") nra := len(strings.Split(ra, "\n")) t.Logf("regs -a: %s", ra) - if nr > nra/2 { + if nr > nra/2+1 { t.Fatalf("'regs' returned too many registers (%d) compared to 'regs -a' (%d)", nr, nra) } }) diff --git a/pkg/terminal/starbind/starlark_mapping.go b/pkg/terminal/starbind/starlark_mapping.go index 4bf55bbe..fd18029a 100644 --- a/pkg/terminal/starbind/starlark_mapping.go +++ b/pkg/terminal/starbind/starlark_mapping.go @@ -934,6 +934,15 @@ func (env *Env) starlarkPredeclare() starlark.StringDict { return starlark.None, decorateError(thread, err) } } + if len(args) > 2 && args[2] != starlark.None { + err := unmarshalStarlarkValue(args[2], &rpcArgs.Scope, "Scope") + if err != nil { + return starlark.None, decorateError(thread, err) + } + } else { + scope := env.ctx.Scope() + rpcArgs.Scope = &scope + } for _, kv := range kwargs { var err error switch kv[0].(starlark.String) { @@ -941,6 +950,8 @@ func (env *Env) starlarkPredeclare() starlark.StringDict { err = unmarshalStarlarkValue(kv[1], &rpcArgs.ThreadID, "ThreadID") case "IncludeFp": err = unmarshalStarlarkValue(kv[1], &rpcArgs.IncludeFp, "IncludeFp") + case "Scope": + err = unmarshalStarlarkValue(kv[1], &rpcArgs.Scope, "Scope") default: err = fmt.Errorf("unknown argument %q", kv[0]) } diff --git a/service/api/conversions.go b/service/api/conversions.go index 3cf1bd97..9aa1488c 100644 --- a/service/api/conversions.go +++ b/service/api/conversions.go @@ -10,6 +10,7 @@ import ( "strconv" "github.com/go-delve/delve/pkg/dwarf/godwarf" + "github.com/go-delve/delve/pkg/dwarf/op" "github.com/go-delve/delve/pkg/proc" ) @@ -328,10 +329,18 @@ func LoadConfigFromProc(cfg *proc.LoadConfig) *LoadConfig { } // ConvertRegisters converts proc.Register to api.Register for a slice. -func ConvertRegisters(in []proc.Register, arch proc.Arch) (out []Register) { - out = make([]Register, len(in)) - for i := range in { - out[i] = Register{in[i].Name, arch.DwarfRegisterToString(in[i].Name, in[i].Reg)} +func ConvertRegisters(in op.DwarfRegisters, arch proc.Arch, floatingPoint bool) (out []Register) { + out = make([]Register, 0, len(in.Regs)) + for i := range in.Regs { + reg := in.Reg(uint64(i)) + if reg == nil { + continue + } + name, fp, repr := arch.DwarfRegisterToString(i, reg) + if !floatingPoint && fp { + continue + } + out = append(out, Register{name, repr, i}) } return } diff --git a/service/api/types.go b/service/api/types.go index c401216c..d8905348 100644 --- a/service/api/types.go +++ b/service/api/types.go @@ -442,8 +442,9 @@ type SetAPIVersionOut struct { // Register holds information on a CPU register. type Register struct { - Name string - Value string + Name string + Value string + DwarfNumber int } // Registers is a list of CPU registers. diff --git a/service/client.go b/service/client.go index 5544091a..fc7c71fd 100644 --- a/service/client.go +++ b/service/client.go @@ -93,8 +93,10 @@ type Client interface { ListLocalVariables(scope api.EvalScope, cfg api.LoadConfig) ([]api.Variable, error) // ListFunctionArgs lists all arguments to the current function. ListFunctionArgs(scope api.EvalScope, cfg api.LoadConfig) ([]api.Variable, error) - // ListRegisters lists registers and their values. - ListRegisters(threadID int, includeFp bool) (api.Registers, error) + // ListThreadRegisters lists registers and their values, for the given thread. + ListThreadRegisters(threadID int, includeFp bool) (api.Registers, error) + // ListScopeRegisters lists registers and their values, for the given scope. + ListScopeRegisters(scope api.EvalScope, includeFp bool) (api.Registers, error) // ListGoroutines lists all goroutines. ListGoroutines(start, count int) ([]*api.Goroutine, int, error) diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index d508661b..494bce95 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -13,6 +13,7 @@ import ( "sync" "time" + "github.com/go-delve/delve/pkg/dwarf/op" "github.com/go-delve/delve/pkg/goversion" "github.com/go-delve/delve/pkg/logflags" "github.com/go-delve/delve/pkg/proc" @@ -949,19 +950,65 @@ func (d *Debugger) PackageVariables(threadID int, filter string, cfg proc.LoadCo } // Registers returns string representation of the CPU registers. -func (d *Debugger) Registers(threadID int, floatingPoint bool) (api.Registers, error) { +func (d *Debugger) Registers(threadID int, scope *api.EvalScope, floatingPoint bool) (api.Registers, error) { d.processMutex.Lock() defer d.processMutex.Unlock() - thread, found := d.target.FindThread(threadID) - if !found { - return nil, fmt.Errorf("couldn't find thread %d", threadID) + var dregs op.DwarfRegisters + + if scope != nil { + s, err := proc.ConvertEvalScope(d.target, scope.GoroutineID, scope.Frame, scope.DeferredCall) + if err != nil { + return nil, err + } + dregs = s.Regs + } else { + thread, found := d.target.FindThread(threadID) + if !found { + return nil, fmt.Errorf("couldn't find thread %d", threadID) + } + regs, err := thread.Registers(floatingPoint) + if err != nil { + return nil, err + } + dregs = d.target.BinInfo().Arch.RegistersToDwarfRegisters(0, regs) } - regs, err := thread.Registers(floatingPoint) - if err != nil { - return nil, err - } - return api.ConvertRegisters(regs.Slice(floatingPoint), d.target.BinInfo().Arch), err + r := api.ConvertRegisters(dregs, d.target.BinInfo().Arch, floatingPoint) + // Sort the registers in a canonical order we prefer, this is mostly + // because the DWARF register numbering for AMD64 is weird. + sort.Slice(r, func(i, j int) bool { + a, b := r[i], r[j] + an, aok := canonicalRegisterOrder[strings.ToLower(a.Name)] + bn, bok := canonicalRegisterOrder[strings.ToLower(b.Name)] + // Registers that don't appear in canonicalRegisterOrder sort after registers that do. + if !aok { + an = 1000 + } + if !bok { + bn = 1000 + } + if an == bn { + // keep registers that don't appear in canonicalRegisterOrder in DWARF order + return a.DwarfNumber < b.DwarfNumber + } + return an < bn + + }) + return r, nil +} + +var canonicalRegisterOrder = map[string]int{ + // amd64 + "rip": 0, + "rsp": 1, + "rax": 2, + "rbx": 3, + "rcx": 4, + "rdx": 5, + + // arm64 + "pc": 0, + "sp": 1, } func convertVars(pv []*proc.Variable) []api.Variable { diff --git a/service/rpc1/server.go b/service/rpc1/server.go index 46c19227..eb71e3ee 100644 --- a/service/rpc1/server.go +++ b/service/rpc1/server.go @@ -204,7 +204,7 @@ func (s *RPCServer) ListRegisters(arg interface{}, registers *string) error { return err } - regs, err := s.debugger.Registers(state.CurrentThread.ID, false) + regs, err := s.debugger.Registers(state.CurrentThread.ID, nil, false) if err != nil { return err } diff --git a/service/rpc2/client.go b/service/rpc2/client.go index 51edc953..2d9577b7 100644 --- a/service/rpc2/client.go +++ b/service/rpc2/client.go @@ -292,9 +292,15 @@ func (c *RPCClient) ListLocalVariables(scope api.EvalScope, cfg api.LoadConfig) return out.Variables, err } -func (c *RPCClient) ListRegisters(threadID int, includeFp bool) (api.Registers, error) { +func (c *RPCClient) ListThreadRegisters(threadID int, includeFp bool) (api.Registers, error) { out := new(ListRegistersOut) - err := c.call("ListRegisters", ListRegistersIn{ThreadID: threadID, IncludeFp: includeFp}, out) + err := c.call("ListRegisters", ListRegistersIn{ThreadID: threadID, IncludeFp: includeFp, Scope: nil}, out) + return out.Regs, err +} + +func (c *RPCClient) ListScopeRegisters(scope api.EvalScope, includeFp bool) (api.Registers, error) { + out := new(ListRegistersOut) + err := c.call("ListRegisters", ListRegistersIn{ThreadID: 0, IncludeFp: includeFp, Scope: &scope}, out) return out.Regs, err } diff --git a/service/rpc2/server.go b/service/rpc2/server.go index defbe212..d684d20d 100644 --- a/service/rpc2/server.go +++ b/service/rpc2/server.go @@ -368,6 +368,7 @@ func (s *RPCServer) ListPackageVars(arg ListPackageVarsIn, out *ListPackageVarsO type ListRegistersIn struct { ThreadID int IncludeFp bool + Scope *api.EvalScope } type ListRegistersOut struct { @@ -376,8 +377,10 @@ type ListRegistersOut struct { } // ListRegisters lists registers and their values. +// If ListRegistersIn.Scope is not nil the registers of that eval scope will +// be returned, otherwise ListRegistersIn.ThreadID will be used. func (s *RPCServer) ListRegisters(arg ListRegistersIn, out *ListRegistersOut) error { - if arg.ThreadID == 0 { + if arg.ThreadID == 0 && arg.Scope == nil { state, err := s.debugger.State(false) if err != nil { return err @@ -385,7 +388,7 @@ func (s *RPCServer) ListRegisters(arg ListRegistersIn, out *ListRegistersOut) er arg.ThreadID = state.CurrentThread.ID } - regs, err := s.debugger.Registers(arg.ThreadID, arg.IncludeFp) + regs, err := s.debugger.Registers(arg.ThreadID, arg.Scope, arg.IncludeFp) if err != nil { return err } diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go index 80f7afc1..29c0c24d 100644 --- a/service/test/integration2_test.go +++ b/service/test/integration2_test.go @@ -510,13 +510,28 @@ func TestClientServer_infoArgs(t *testing.T) { if state.Err != nil { t.Fatalf("Unexpected error: %v, state: %#v", state.Err, state) } - regs, err := c.ListRegisters(0, false) + regs, err := c.ListThreadRegisters(0, false) if err != nil { t.Fatalf("Unexpected error: %v", err) } if len(regs) == 0 { t.Fatal("Expected string showing registers values, got empty string") } + + regs, err = c.ListScopeRegisters(api.EvalScope{GoroutineID: -1, Frame: 0}, false) + assertNoError(err, t, "ListScopeRegisters(-1, 0)") + if len(regs) == 0 { + t.Fatal("Expected string showing registers values, got empty string") + } + t.Logf("GoroutineID: -1, Frame: 0\n%s", regs.String()) + + regs, err = c.ListScopeRegisters(api.EvalScope{GoroutineID: -1, Frame: 1}, false) + assertNoError(err, t, "ListScopeRegisters(-1, 1)") + if len(regs) == 0 { + t.Fatal("Expected string showing registers values, got empty string") + } + t.Logf("GoroutineID: -1, Frame: 1\n%s", regs.String()) + locals, err := c.ListFunctionArgs(api.EvalScope{-1, 0, 0}, normalLoadConfig) if err != nil { t.Fatalf("Unexpected error: %v", err) @@ -925,8 +940,10 @@ func TestIssue355(t *testing.T) { assertError(err, t, "ListLocalVariables()") _, err = c.ListFunctionArgs(api.EvalScope{gid, 0, 0}, normalLoadConfig) assertError(err, t, "ListFunctionArgs()") - _, err = c.ListRegisters(0, false) - assertError(err, t, "ListRegisters()") + _, err = c.ListThreadRegisters(0, false) + assertError(err, t, "ListThreadRegisters()") + _, err = c.ListScopeRegisters(api.EvalScope{gid, 0, 0}, false) + assertError(err, t, "ListScopeRegisters()") _, _, err = c.ListGoroutines(0, 0) assertError(err, t, "ListGoroutines()") _, err = c.Stacktrace(gid, 10, 0, &normalLoadConfig) @@ -1279,8 +1296,8 @@ func TestClientServer_FpRegisters(t *testing.T) { protest.AllowRecording(t) withTestClient2("fputest/", t, func(c service.Client) { <-c.Continue() - regs, err := c.ListRegisters(0, true) - assertNoError(err, t, "ListRegisters()") + regs, err := c.ListThreadRegisters(0, true) + assertNoError(err, t, "ListThreadRegisters()") t.Logf("%s", regs.String())