Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pkg/proc: enable basic debug functionality for stripped ELF binaries #3408

Merged
merged 6 commits into from
Jun 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 0 additions & 103 deletions cmd/dlv/dlv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
"github.com/go-delve/delve/pkg/terminal"
"github.com/go-delve/delve/service/dap"
"github.com/go-delve/delve/service/dap/daptest"
"github.com/go-delve/delve/service/debugger"
"github.com/go-delve/delve/service/rpc2"
godap "github.com/google/go-dap"
"golang.org/x/tools/go/packages"
Expand Down Expand Up @@ -282,57 +281,6 @@ func TestContinue(t *testing.T) {
cmd.Wait()
}

// TestChildProcessExitWhenNoDebugInfo verifies that the child process exits when dlv launch the binary without debug info
func TestChildProcessExitWhenNoDebugInfo(t *testing.T) {
noDebugFlags := protest.LinkStrip
// -s doesn't strip symbols on Mac, use -w instead
if runtime.GOOS == "darwin" {
noDebugFlags = protest.LinkDisableDWARF
}

if _, err := exec.LookPath("ps"); err != nil {
t.Skip("test skipped, `ps` not found")
}

dlvbin := getDlvBin(t)

fix := protest.BuildFixture("http_server", noDebugFlags)

// dlv exec the binary file and expect error.
out, err := exec.Command(dlvbin, "exec", "--headless", "--log", fix.Path).CombinedOutput()
t.Log(string(out))
if err == nil {
t.Fatalf("Expected err when launching the binary without debug info, but got nil")
}
// Test only for dlv's prefix of the error like "could not launch process: could not open debug info"
if !strings.Contains(string(out), "could not launch process") || !strings.Contains(string(out), debugger.NoDebugWarning) {
t.Fatalf("Expected logged error 'could not launch process: ... - %s'", debugger.NoDebugWarning)
}

// search the running process named fix.Name
cmd := exec.Command("ps", "-aux")
stdout, err := cmd.StdoutPipe()
assertNoError(err, t, "stdout pipe")
defer stdout.Close()

assertNoError(cmd.Start(), t, "start `ps -aux`")

var foundFlag bool
scan := bufio.NewScanner(stdout)
for scan.Scan() {
t.Log(scan.Text())
if strings.Contains(scan.Text(), fix.Name) {
foundFlag = true
break
}
}
cmd.Wait()

if foundFlag {
t.Fatalf("Expected child process exited, but found it running")
}
}

// TestRedirect verifies that redirecting stdin works
func TestRedirect(t *testing.T) {
const listenAddr = "127.0.0.1:40573"
Expand Down Expand Up @@ -711,57 +659,6 @@ func TestDAPCmd(t *testing.T) {
cmd.Wait()
}

func TestDAPCmdWithNoDebugBinary(t *testing.T) {
const listenAddr = "127.0.0.1:40579"

dlvbin := getDlvBin(t)

cmd := exec.Command(dlvbin, "dap", "--log", "--listen", listenAddr)
stdout, err := cmd.StdoutPipe()
assertNoError(err, t, "stdout pipe")
defer stdout.Close()
stderr, err := cmd.StderrPipe()
assertNoError(err, t, "stderr pipe")
defer stderr.Close()
assertNoError(cmd.Start(), t, "start dap instance")

scanOut := bufio.NewScanner(stdout)
scanErr := bufio.NewScanner(stderr)
// Wait for the debug server to start
scanOut.Scan()
listening := "DAP server listening at: " + listenAddr
if scanOut.Text() != listening {
cmd.Process.Kill() // release the port
t.Fatalf("Unexpected stdout:\ngot %q\nwant %q", scanOut.Text(), listening)
}
go func() { // Capture logging
for scanErr.Scan() {
t.Log(scanErr.Text())
}
}()

// Exec the stripped debuggee and expect things to fail
noDebugFlags := protest.LinkStrip
// -s doesn't strip symbols on Mac, use -w instead
if runtime.GOOS == "darwin" {
noDebugFlags = protest.LinkDisableDWARF
}
fixture := protest.BuildFixture("increment", noDebugFlags)
go func() {
for scanOut.Scan() {
t.Errorf("Unexpected stdout: %s", scanOut.Text())
}
}()
client := daptest.NewClient(listenAddr)
client.LaunchRequest("exec", fixture.Path, false)
client.ExpectErrorResponse(t)
client.DisconnectRequest()
client.ExpectDisconnectResponse(t)
client.ExpectTerminatedEvent(t)
client.Close()
cmd.Wait()
}

func newDAPRemoteClient(t *testing.T, addr string, isDlvAttach bool, isMulti bool) *daptest.Client {
c := daptest.NewClient(addr)
c.AttachRequest(map[string]interface{}{"mode": "remote", "stopOnEntry": true})
Expand Down
83 changes: 71 additions & 12 deletions pkg/proc/bininfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"debug/dwarf"
"debug/elf"
"debug/gosym"
"debug/macho"
"debug/pe"
"encoding/binary"
Expand Down Expand Up @@ -332,7 +333,7 @@ func FindFunctionLocation(p Process, funcName string, lineOffset int) ([]uint64,

if lineOffset > 0 {
fn := origfns[0]
filename, lineno := fn.cu.lineInfo.PCToLine(fn.Entry, fn.Entry)
filename, lineno := bi.EntryLineForFunc(fn)
return FindFileLocation(p, filename, lineno+lineOffset)
}

Expand Down Expand Up @@ -364,14 +365,16 @@ func FindFunctionLocation(p Process, funcName string, lineOffset int) ([]uint64,
// If sameline is set FirstPCAfterPrologue will always return an
// address associated with the same line as fn.Entry.
func FirstPCAfterPrologue(p Process, fn *Function, sameline bool) (uint64, error) {
pc, _, line, ok := fn.cu.lineInfo.PrologueEndPC(fn.Entry, fn.End)
if ok {
if !sameline {
return pc, nil
}
_, entryLine := fn.cu.lineInfo.PCToLine(fn.Entry, fn.Entry)
if entryLine == line {
return pc, nil
if fn.cu.lineInfo != nil {
pc, _, line, ok := fn.cu.lineInfo.PrologueEndPC(fn.Entry, fn.End)
if ok {
if !sameline {
return pc, nil
}
_, entryLine := p.BinInfo().EntryLineForFunc(fn)
if entryLine == line {
return pc, nil
}
}
}

Expand All @@ -380,7 +383,7 @@ func FirstPCAfterPrologue(p Process, fn *Function, sameline bool) (uint64, error
return fn.Entry, err
}

if pc == fn.Entry {
if pc == fn.Entry && fn.cu.lineInfo != nil {
// Look for the first instruction with the stmt flag set, so that setting a
// breakpoint with file:line and with the function name always result on
// the same instruction being selected.
Expand Down Expand Up @@ -601,6 +604,25 @@ func (fn *Function) PrologueEndPC() uint64 {
return pc
}

func (fn *Function) AllPCs(excludeFile string, excludeLine int) ([]uint64, error) {
if !fn.cu.image.Stripped() {
return fn.cu.lineInfo.AllPCsBetween(fn.Entry, fn.End-1, excludeFile, excludeLine)
}
var pcs []uint64
fnFile, lastLine, _ := fn.cu.image.symTable.PCToLine(fn.Entry)
for pc := fn.Entry; pc < fn.End; pc++ {
f, line, pcfn := fn.cu.image.symTable.PCToLine(pc)
if pcfn == nil {
continue
}
if f == fnFile && line > lastLine {
lastLine = line
pcs = append(pcs, pc)
}
}
return pcs, nil
}

// From $GOROOT/src/runtime/traceback.go:597
// exportedRuntime reports whether the function is an exported runtime function.
// It is only for runtime functions, so ASCII A-Z is fine.
Expand Down Expand Up @@ -719,6 +741,9 @@ func (bi *BinaryInfo) LastModified() time.Time {

// DwarfReader returns a reader for the dwarf data
func (so *Image) DwarfReader() *reader.Reader {
if so.dwarf == nil {
return nil
}
return reader.New(so.dwarf)
}

Expand All @@ -731,13 +756,26 @@ func (bi *BinaryInfo) Types() ([]string, error) {
return types, nil
}

func (bi *BinaryInfo) EntryLineForFunc(fn *Function) (string, int) {
return bi.pcToLine(fn, fn.Entry)
}

func (bi *BinaryInfo) pcToLine(fn *Function, pc uint64) (string, int) {
if fn.cu.lineInfo == nil {
f, l, _ := fn.cu.image.symTable.PCToLine(pc)
return f, l
}
f, l := fn.cu.lineInfo.PCToLine(fn.Entry, pc)
return f, l
}

// PCToLine converts an instruction address to a file/line/function.
func (bi *BinaryInfo) PCToLine(pc uint64) (string, int, *Function) {
fn := bi.PCToFunc(pc)
if fn == nil {
return "", 0, nil
}
f, ln := fn.cu.lineInfo.PCToLine(fn.Entry, pc)
f, ln := bi.pcToLine(fn, pc)
return f, ln, fn
}

Expand Down Expand Up @@ -810,6 +848,8 @@ type Image struct {
debugAddr *godwarf.DebugAddrSection
debugLineStr []byte

symTable *gosym.Table

typeCache map[dwarf.Offset]godwarf.Type

compileUnits []*compileUnit // compileUnits is sorted by increasing DWARF offset
Expand All @@ -835,6 +875,10 @@ func (image *Image) registerRuntimeTypeToDIE(entry *dwarf.Entry, ardr *reader.Re
}
}

func (image *Image) Stripped() bool {
return image.dwarf == nil
}

// AddImage adds the specified image to bi, loading data asynchronously.
// Addr is the relocated entry point for the executable and staticBase (i.e.
// the relocation offset) for all other images.
Expand Down Expand Up @@ -1405,7 +1449,22 @@ func loadBinaryInfoElf(bi *BinaryInfo, image *Image, path string, addr uint64, w
var serr error
sepFile, dwarfFile, serr = bi.openSeparateDebugInfo(image, elfFile, bi.DebugInfoDirectories)
if serr != nil {
return serr
fmt.Fprintln(os.Stderr, "Warning: no debug info found, some functionality will be missing such as stack traces and variable evaluation.")
symTable, err := readPcLnTableElf(elfFile, path)
derekparker marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return fmt.Errorf("could not create symbol table from %s ", path)
}
image.symTable = symTable
for _, f := range image.symTable.Funcs {
cu := &compileUnit{}
cu.image = image
fn := Function{Name: f.Name, Entry: f.Entry, End: f.End, cu: cu}
bi.Functions = append(bi.Functions, fn)
}
for f := range image.symTable.Files {
bi.Sources = append(bi.Sources, f)
}
return nil
}
image.sepDebugCloser = sepFile
image.dwarf, err = dwarfFile.DWARF()
Expand Down
2 changes: 1 addition & 1 deletion pkg/proc/breakpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ func (t *Target) setEBPFTracepointOnFunc(fn *Function, goidOffset int64) error {
if t.BinInfo().Producer() != "" && goversion.ProducerAfterOrEqual(t.BinInfo().Producer(), 1, 15) {
variablesFlags |= reader.VariablesTrustDeclLine
}
_, l, _ := t.BinInfo().PCToLine(fn.Entry)
_, l := t.BinInfo().EntryLineForFunc(fn)

var args []ebpf.UProbeArgMap
varEntries := reader.Variables(dwarfTree, fn.Entry, l, variablesFlags)
Expand Down
2 changes: 1 addition & 1 deletion pkg/proc/fncall.go
Original file line number Diff line number Diff line change
Expand Up @@ -1053,7 +1053,7 @@ func readStackVariable(t *Target, thread Thread, regs Registers, off uint64, typ
func fakeFunctionEntryScope(scope *EvalScope, fn *Function, cfa int64, sp uint64) error {
scope.PC = fn.Entry
scope.Fn = fn
scope.File, scope.Line, _ = scope.BinInfo.PCToLine(fn.Entry)
scope.File, scope.Line = scope.BinInfo.EntryLineForFunc(fn)

scope.Regs.CFA = cfa
scope.Regs.Reg(scope.Regs.SPRegNum).Uint64Val = sp
Expand Down
3 changes: 3 additions & 0 deletions pkg/proc/goroutine_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ func (gcache *goroutineCache) init(bi *BinaryInfo) {

exeimage := bi.Images[0]
rdr := exeimage.DwarfReader()
if rdr == nil {
return
}

gcache.allglenAddr, _ = rdr.AddrFor("runtime.allglen", exeimage.StaticBase, bi.Arch.PtrSize())

Expand Down
75 changes: 75 additions & 0 deletions pkg/proc/pclntab.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package proc

import (
"bytes"
"debug/buildinfo"
"debug/elf"
"debug/gosym"
"encoding/binary"
"fmt"
"strings"
)

// From go/src/debug/gosym/pclntab.go
const (
go12magic = 0xfffffffb
go116magic = 0xfffffffa
go118magic = 0xfffffff0
go120magic = 0xfffffff1
)

// Select the magic number based on the Go version
func magicNumber(goVersion string) []byte {
bs := make([]byte, 4)
var magic uint32
if strings.Compare(goVersion, "go1.20") >= 0 {
magic = go120magic
} else if strings.Compare(goVersion, "go1.18") >= 0 {
magic = go118magic
} else if strings.Compare(goVersion, "go1.16") >= 0 {
magic = go116magic
} else {
magic = go12magic
}
binary.LittleEndian.PutUint32(bs, magic)
return bs
}

func readPcLnTableElf(exe *elf.File, path string) (*gosym.Table, error) {
// Default section label is .gopclntab
sectionLabel := ".gopclntab"

section := exe.Section(sectionLabel)
if section == nil {
// binary may be built with -pie
sectionLabel = ".data.rel.ro"
section = exe.Section(sectionLabel)
if section == nil {
return nil, fmt.Errorf("could not read section .gopclntab")
}
}
tableData, err := section.Data()
if err != nil {
return nil, fmt.Errorf("found section but could not read .gopclntab")
}

bi, err := buildinfo.ReadFile(path)
if err != nil {
return nil, err
}

// Find .gopclntab by magic number even if there is no section label
magic := magicNumber(bi.GoVersion)
pclntabIndex := bytes.Index(tableData, magic)
if pclntabIndex < 0 {
return nil, fmt.Errorf("could not find magic number in %s ", path)
}
tableData = tableData[pclntabIndex:]
addr := exe.Section(".text").Addr
lineTable := gosym.NewLineTable(tableData, addr)
symTable, err := gosym.NewTable([]byte{}, lineTable)
if err != nil {
return nil, fmt.Errorf("could not create symbol table from %s ", path)
}
return symTable, nil
}
Loading