From 8c392d2fdf9c550bac1ad26b72da96790f086abe Mon Sep 17 00:00:00 2001 From: Morten Linderud Date: Sun, 30 Jan 2022 22:39:30 +0100 Subject: [PATCH] Implement source listing from debuginfo (#2885) * service: Implement BuildID Parse the BuildID of executables and provides it over the RPC service. Signed-off-by: Morten Linderud * command: Support debuinfod for file listing Signed-off-by: Morten Linderud * debuginfod: create debuginfod package for common code We remove the duplicated code and provide our a new debuginfod package. Signed-off-by: Morten Linderud * starlark: Workaround for 'build_i_d' Signed-off-by: Morten Linderud * command: Ensure we only overwrite path when one has been found Signed-off-by: Morten Linderud * bininfo: Inline parseBuildID Signed-off-by: Morten Linderud --- Documentation/cli/starlark.md | 1 + _scripts/gen-starlark-bindings.go | 2 + pkg/proc/bininfo.go | 92 +++++++++++------------ pkg/proc/debuginfod/debuginfod.go | 28 +++++++ pkg/terminal/command.go | 11 ++- pkg/terminal/starbind/starlark_mapping.go | 12 +++ service/client.go | 3 + service/debugger/debugger.go | 4 + service/rpc2/client.go | 6 ++ service/rpc2/server.go | 12 +++ 10 files changed, 120 insertions(+), 51 deletions(-) create mode 100644 pkg/proc/debuginfod/debuginfod.go diff --git a/Documentation/cli/starlark.md b/Documentation/cli/starlark.md index 6a36ebce..ffe72244 100644 --- a/Documentation/cli/starlark.md +++ b/Documentation/cli/starlark.md @@ -20,6 +20,7 @@ Function | API Call amend_breakpoint(Breakpoint) | Equivalent to API call [AmendBreakpoint](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.AmendBreakpoint) ancestors(GoroutineID, NumAncestors, Depth) | Equivalent to API call [Ancestors](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Ancestors) attached_to_existing_process() | Equivalent to API call [AttachedToExistingProcess](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.AttachedToExistingProcess) +build_id() | Equivalent to API call [BuildID](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.BuildID) cancel_next() | Equivalent to API call [CancelNext](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.CancelNext) checkpoint(Where) | Equivalent to API call [Checkpoint](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Checkpoint) clear_breakpoint(Id, Name) | Equivalent to API call [ClearBreakpoint](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ClearBreakpoint) diff --git a/_scripts/gen-starlark-bindings.go b/_scripts/gen-starlark-bindings.go index 8706b2ea..28d8a518 100644 --- a/_scripts/gen-starlark-bindings.go +++ b/_scripts/gen-starlark-bindings.go @@ -111,6 +111,8 @@ func processServerMethods(serverMethods []*types.Func) []binding { name = "set_expr" case "command": name = "raw_command" + case "build_i_d": + name = "build_id" case "create_e_b_p_f_tracepoint": name = "create_ebpf_tracepoint" default: diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go index 4b93cbb1..382c7282 100644 --- a/pkg/proc/bininfo.go +++ b/pkg/proc/bininfo.go @@ -14,7 +14,6 @@ import ( "go/token" "io" "os" - "os/exec" "path/filepath" "sort" "strconv" @@ -31,6 +30,7 @@ import ( "github.com/go-delve/delve/pkg/dwarf/util" "github.com/go-delve/delve/pkg/goversion" "github.com/go-delve/delve/pkg/logflags" + "github.com/go-delve/delve/pkg/proc/debuginfod" "github.com/hashicorp/golang-lru/simplelru" "github.com/sirupsen/logrus" ) @@ -52,6 +52,9 @@ type BinaryInfo struct { debugInfoDirectories []string + // BuildID of this binary. + BuildID string + // Functions is a list of all DW_TAG_subprogram entries in debug_info, sorted by entry point Functions []Function // Sources is a list of all source files found in debug_line. @@ -1193,15 +1196,6 @@ func (bi *BinaryInfo) parseDebugFrameGeneral(image *Image, debugFrameBytes []byt // ELF /////////////////////////////////////////////////////////////// -// ErrNoBuildIDNote is used in openSeparateDebugInfo to signal there's no -// build-id note on the binary, so LoadBinaryInfoElf will return -// the error message coming from elfFile.DWARF() instead. -type ErrNoBuildIDNote struct{} - -func (e *ErrNoBuildIDNote) Error() string { - return "can't find build-id note on binary" -} - // openSeparateDebugInfo searches for a file containing the separate // debug info for the binary using the "build ID" method as described // in GDB's documentation [1], and if found returns two handles, one @@ -1212,11 +1206,11 @@ func (e *ErrNoBuildIDNote) Error() string { // will look in directories specified by the debug-info-directories config value. func (bi *BinaryInfo) openSeparateDebugInfo(image *Image, exe *elf.File, debugInfoDirectories []string) (*os.File, *elf.File, error) { var debugFilePath string - desc1, desc2, _ := parseBuildID(exe) + var err error for _, dir := range debugInfoDirectories { var potentialDebugFilePath string if strings.Contains(dir, "build-id") { - potentialDebugFilePath = fmt.Sprintf("%s/%s/%s.debug", dir, desc1, desc2) + potentialDebugFilePath = fmt.Sprintf("%s/%s/%s.debug", dir, bi.BuildID[:2], bi.BuildID[2:]) } else if strings.HasPrefix(image.Path, "/proc") { path, err := filepath.EvalSymlinks(image.Path) if err == nil { @@ -1234,15 +1228,8 @@ func (bi *BinaryInfo) openSeparateDebugInfo(image *Image, exe *elf.File, debugIn // We cannot find the debug information locally on the system. Try and see if we're on a system that // has debuginfod so that we can use that in order to find any relevant debug information. if debugFilePath == "" { - const debuginfodFind = "debuginfod-find" - if _, err := exec.LookPath(debuginfodFind); err == nil { - cmd := exec.Command(debuginfodFind, "debuginfo", desc1+desc2) - out, err := cmd.CombinedOutput() - if err != nil { - return nil, nil, ErrNoDebugInfoFound - } - debugFilePath = strings.TrimSpace(string(out)) - } else { + debugFilePath, err = debuginfod.GetDebuginfo(bi.BuildID) + if err != nil { return nil, nil, ErrNoDebugInfoFound } } @@ -1265,35 +1252,6 @@ func (bi *BinaryInfo) openSeparateDebugInfo(image *Image, exe *elf.File, debugIn return sepFile, elfFile, nil } -func parseBuildID(exe *elf.File) (string, string, error) { - buildid := exe.Section(".note.gnu.build-id") - if buildid == nil { - return "", "", &ErrNoBuildIDNote{} - } - - br := buildid.Open() - bh := new(buildIDHeader) - if err := binary.Read(br, binary.LittleEndian, bh); err != nil { - return "", "", errors.New("can't read build-id header: " + err.Error()) - } - - name := make([]byte, bh.Namesz) - if err := binary.Read(br, binary.LittleEndian, name); err != nil { - return "", "", errors.New("can't read build-id name: " + err.Error()) - } - - if strings.TrimSpace(string(name)) != "GNU\x00" { - return "", "", errors.New("invalid build-id signature") - } - - descBinary := make([]byte, bh.Descsz) - if err := binary.Read(br, binary.LittleEndian, descBinary); err != nil { - return "", "", errors.New("can't read build-id desc: " + err.Error()) - } - desc := hex.EncodeToString(descBinary) - return desc[:2], desc[2:], nil -} - // loadBinaryInfoElf specifically loads information from an ELF binary. func loadBinaryInfoElf(bi *BinaryInfo, image *Image, path string, addr uint64, wg *sync.WaitGroup) error { exe, err := os.OpenFile(path, 0, os.ModePerm) @@ -1330,6 +1288,7 @@ func loadBinaryInfoElf(bi *BinaryInfo, image *Image, path string, addr uint64, w dwarfFile := elfFile + bi.loadBuildID(image, elfFile) var debugInfoBytes []byte image.dwarf, err = elfFile.DWARF() if err != nil { @@ -1395,6 +1354,39 @@ func (bi *BinaryInfo) loadSymbolName(image *Image, file *elf.File, wg *sync.Wait } } +func (bi *BinaryInfo) loadBuildID(image *Image, file *elf.File) { + buildid := file.Section(".note.gnu.build-id") + if buildid == nil { + bi.logger.Error("can't find build-id note on binary") + return + } + + br := buildid.Open() + bh := new(buildIDHeader) + if err := binary.Read(br, binary.LittleEndian, bh); err != nil { + bi.logger.Errorf("can't read build-id header: %v", err) + return + } + + name := make([]byte, bh.Namesz) + if err := binary.Read(br, binary.LittleEndian, name); err != nil { + bi.logger.Errorf("can't read build-id name: %v", err) + return + } + + if strings.TrimSpace(string(name)) != "GNU\x00" { + bi.logger.Error("invalid build-id signature") + return + } + + descBinary := make([]byte, bh.Descsz) + if err := binary.Read(br, binary.LittleEndian, descBinary); err != nil { + bi.logger.Errorf("can't read build-id desc: %v", err) + return + } + bi.BuildID = hex.EncodeToString(descBinary) +} + func (bi *BinaryInfo) parseDebugFrameElf(image *Image, dwarfFile, exeFile *elf.File, debugInfoBytes []byte, wg *sync.WaitGroup) { defer wg.Done() diff --git a/pkg/proc/debuginfod/debuginfod.go b/pkg/proc/debuginfod/debuginfod.go new file mode 100644 index 00000000..a46d9806 --- /dev/null +++ b/pkg/proc/debuginfod/debuginfod.go @@ -0,0 +1,28 @@ +package debuginfod + +import ( + "os/exec" + "strings" +) + +const debuginfodFind = "debuginfod-find" + +func execFind(args ...string) (string, error) { + if _, err := exec.LookPath(debuginfodFind); err != nil { + return "", err + } + cmd := exec.Command(debuginfodFind, args...) + out, err := cmd.CombinedOutput() + if err != nil { + return "", err + } + return strings.TrimSpace(string(out)), err +} + +func GetSource(buildid, filename string) (string, error) { + return execFind("source", buildid, filename) +} + +func GetDebuginfo(buildid string) (string, error) { + return execFind("debuginfo", buildid) +} diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go index 09c03b1b..deeaa6b0 100644 --- a/pkg/terminal/command.go +++ b/pkg/terminal/command.go @@ -28,6 +28,7 @@ import ( "github.com/cosiner/argv" "github.com/go-delve/delve/pkg/config" "github.com/go-delve/delve/pkg/locspec" + "github.com/go-delve/delve/pkg/proc/debuginfod" "github.com/go-delve/delve/service" "github.com/go-delve/delve/service/api" "github.com/go-delve/delve/service/rpc2" @@ -2676,7 +2677,15 @@ func printfile(t *Term, filename string, line int, showArrow bool) error { arrowLine = line } - file, err := os.Open(t.substitutePath(filename)) + var file *os.File + path := t.substitutePath(filename) + if _, err := os.Stat(path); os.IsNotExist(err) { + foundPath, err := debuginfod.GetSource(t.client.BuildID(), filename) + if err == nil { + path = foundPath + } + } + file, err := os.OpenFile(path, 0, os.ModePerm) if err != nil { return err } diff --git a/pkg/terminal/starbind/starlark_mapping.go b/pkg/terminal/starbind/starlark_mapping.go index ee777768..daf5f1ee 100644 --- a/pkg/terminal/starbind/starlark_mapping.go +++ b/pkg/terminal/starbind/starlark_mapping.go @@ -100,6 +100,18 @@ func (env *Env) starlarkPredeclare() starlark.StringDict { } return env.interfaceToStarlarkValue(rpcRet), nil }) + r["build_id"] = starlark.NewBuiltin("build_id", 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.BuildIDIn + var rpcRet rpc2.BuildIDOut + err := env.ctx.Client().CallAPI("BuildID", &rpcArgs, &rpcRet) + if err != nil { + return starlark.None, err + } + return env.interfaceToStarlarkValue(rpcRet), nil + }) r["cancel_next"] = starlark.NewBuiltin("cancel_next", 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/client.go b/service/client.go index 4920435e..90b74d23 100644 --- a/service/client.go +++ b/service/client.go @@ -12,6 +12,9 @@ type Client interface { // Returns the pid of the process we are debugging. ProcessPid() int + // Returns the BuildID of the process' executable we are debugging. + BuildID() string + // LastModified returns the time that the process' executable was modified. LastModified() time.Time diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index 17c562ba..dd88e1d6 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -2199,6 +2199,10 @@ func (d *Debugger) Target() *proc.Target { return d.target } +func (d *Debugger) BuildID() string { + return d.target.BinInfo().BuildID +} + func (d *Debugger) GetBufferedTracepoints() []api.TracepointResult { traces := d.target.GetBufferedTracepoints() if traces == nil { diff --git a/service/rpc2/client.go b/service/rpc2/client.go index 0e348970..f01c0a97 100644 --- a/service/rpc2/client.go +++ b/service/rpc2/client.go @@ -48,6 +48,12 @@ func (c *RPCClient) ProcessPid() int { return out.Pid } +func (c *RPCClient) BuildID() string { + out := new(BuildIDOut) + c.call("BuildID", BuildIDIn{}, out) + return out.BuildID +} + func (c *RPCClient) LastModified() time.Time { out := new(LastModifiedOut) c.call("LastModified", LastModifiedIn{}, out) diff --git a/service/rpc2/server.go b/service/rpc2/server.go index afcc164a..5a01f5b4 100644 --- a/service/rpc2/server.go +++ b/service/rpc2/server.go @@ -1011,3 +1011,15 @@ func (s *RPCServer) CreateWatchpoint(arg CreateWatchpointIn, out *CreateWatchpoi out.Breakpoint, err = s.debugger.CreateWatchpoint(arg.Scope.GoroutineID, arg.Scope.Frame, arg.Scope.DeferredCall, arg.Expr, arg.Type) return err } + +type BuildIDIn struct { +} + +type BuildIDOut struct { + BuildID string +} + +func (s *RPCServer) BuildID(arg BuildIDIn, out *BuildIDOut) error { + out.BuildID = s.debugger.BuildID() + return nil +}