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 <morten@linderud.pw>

* command: Support debuinfod for file listing

Signed-off-by: Morten Linderud <morten@linderud.pw>

* debuginfod: create debuginfod package for common code

We remove the duplicated code and provide our a new debuginfod package.

Signed-off-by: Morten Linderud <morten@linderud.pw>

* starlark: Workaround for 'build_i_d'

Signed-off-by: Morten Linderud <morten@linderud.pw>

* command: Ensure we only overwrite path when one has been found

Signed-off-by: Morten Linderud <morten@linderud.pw>

* bininfo: Inline parseBuildID

Signed-off-by: Morten Linderud <morten@linderud.pw>
This commit is contained in:
Morten Linderud 2022-01-30 22:39:30 +01:00 committed by GitHub
parent 5b6f8ec03a
commit 8c392d2fdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 120 additions and 51 deletions

@ -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)

@ -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:

@ -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()

@ -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)
}

@ -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
}

@ -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)

@ -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

@ -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 {

@ -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)

@ -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
}