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

coredump: Add gosym sub command #234

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ The host agent code is tested with three test suites:
tests. This works great for the user-land portion of the agent, but is unable
to test any of the unwinding logic and BPF interaction.
- **coredump test suite**\
The coredump test suite (`utils/coredump`) we compile the whole BPF unwinder
The coredump test suite (`tools/coredump`) we compile the whole BPF unwinder
code into a user-mode executable, then use the information from a coredump to
simulate a realistic environment to test the unwinder code in. The coredump
suite essentially implements all required BPF helper functions in user-space,
Expand Down
38 changes: 34 additions & 4 deletions tools/coredump/coredump.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"fmt"
"os"
"runtime"
"strconv"
"strings"
"time"
"unsafe"

Expand Down Expand Up @@ -114,16 +116,44 @@ func (c *symbolizationCache) symbolize(ty libpf.FrameType, fileID libpf.FileID,
}

if data, ok := c.symbols[libpf.NewFrameID(fileID, lineNumber)]; ok {
return fmt.Sprintf("%s+%d in %s:%d",
data.FunctionName, data.FunctionOffset,
data.SourceFile, data.SourceLine), nil
return formatSymbolizedFrame(data, true), nil
}

sourceFile, ok := c.files[fileID]
if !ok {
sourceFile = fmt.Sprintf("%08x", fileID)
}
return fmt.Sprintf("%s+0x%x", sourceFile, lineNumber), nil
return formatUnsymbolizedFrame(sourceFile, lineNumber), nil
}

func formatSymbolizedFrame(frame *reporter.FrameMetadataArgs, functionOffsets bool) string {
var funcOffset string
if functionOffsets {
funcOffset = "+" + strconv.Itoa(int(frame.FunctionOffset))
}
return fmt.Sprintf("%s%s in %s:%d",
frame.FunctionName, funcOffset,
frame.SourceFile, frame.SourceLine)
}

func formatUnsymbolizedFrame(file string, addr libpf.AddressOrLineno) string {
return fmt.Sprintf("%s+0x%x", file, addr)
}

func parseUnsymbolizedFrame(frame string) (file string, addr libpf.AddressOrLineno, err error) {
fileS, addrS, found := strings.Cut(frame, "+0x")
if !found {
err = fmt.Errorf("bad frame string: %q", frame)
return
}
file = fileS
var addrU uint64
addrU, err = strconv.ParseUint(addrS, 16, 64)
if err != nil {
return
}
addr = libpf.AddressOrLineno(addrU)
return
}

func ExtractTraces(ctx context.Context, pr process.Process, debug bool,
Expand Down
162 changes: 162 additions & 0 deletions tools/coredump/gosym.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package main

import (
"context"
"debug/elf"
"debug/gosym"
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"

"github.com/peterbourgon/ff/v3/ffcli"
"go.opentelemetry.io/ebpf-profiler/libpf"
"go.opentelemetry.io/ebpf-profiler/reporter"
"go.opentelemetry.io/ebpf-profiler/tools/coredump/modulestore"
)

type gosymCmd struct {
store *modulestore.Store
casePath string
}

func newGosymCmd(store *modulestore.Store) *ffcli.Command {
args := &gosymCmd{store: store}

set := flag.NewFlagSet("gosym", flag.ExitOnError)
set.StringVar(&args.casePath, "case", "", "Path of the test case to debug")

return &ffcli.Command{
Name: "gosym",
Exec: args.exec,
ShortUsage: "gosym",
ShortHelp: "Symbolize go test case",
FlagSet: set,
}
}

func (cmd *gosymCmd) exec(context.Context, []string) (err error) {
// Validate arguments.
if cmd.casePath == "" {
return errors.New("please specify `-case`")
}

var test *CoredumpTestCase
test, err = readTestCase(cmd.casePath)
if err != nil {
return fmt.Errorf("failed to read test case: %w", err)
}

module, addrs, err := goModuleAddrs(test)
if err != nil {
return fmt.Errorf("failed to find go module addresses: %w", err)
}

goBinary, err := cmd.store.OpenReadAt(module.Ref)
if err != nil {
return fmt.Errorf("failed to open module: %w", err)
}
defer goBinary.Close()

locs, err := goSymbolize(goBinary, addrs)
if err != nil {
return fmt.Errorf("failed to symbolize: %w", err)
}

for addr, frame := range locs {
for _, originFrame := range addrs[addr] {
*originFrame = formatSymbolizedFrame(frame, false) + " (" + *originFrame + ")"
}
}

return writeTestCaseJSON(os.Stdout, test)
}

// goModuleAddrs returns the go module and the addresses to symbolize for it
// mapped to pointers to the frames in c that reference them.
func goModuleAddrs(c *CoredumpTestCase) (*ModuleInfo, map[libpf.AddressOrLineno][]*string, error) {
type moduleAddrs struct {
module *ModuleInfo
addrs map[libpf.AddressOrLineno][]*string
}

moduleNames := map[string]*moduleAddrs{}
for i, module := range c.Modules {
moduleName := filepath.Base(module.LocalPath)
if _, ok := moduleNames[moduleName]; ok {
return nil, nil, fmt.Errorf("ambiguous module name: %q", moduleName)
}
moduleNames[moduleName] = &moduleAddrs{
module: &c.Modules[i],
addrs: map[libpf.AddressOrLineno][]*string{},
}
}

// maxAddrs is the module with the most addresses to symbolize. We use this
// as a heuristic to determine which module is the Go module we're
// interested in.
// TODO(fg) alternatively we could extract all modules and run some check on
// them to see if they are go binaries. But this is more complex, so the
// current heuristic should be good enough for now.
felixge marked this conversation as resolved.
Show resolved Hide resolved
var maxAddrs *moduleAddrs
for _, thread := range c.Threads {
for i, frame := range thread.Frames {
moduleName, addr, err := parseUnsymbolizedFrame(frame)
if err != nil {
continue
}

moduleAddrs, ok := moduleNames[moduleName]
if !ok {
return nil, nil, fmt.Errorf("module not found: %q", moduleName)
}

moduleAddrs.addrs[addr] = append(moduleAddrs.addrs[addr], &thread.Frames[i])
if maxAddrs == nil || len(moduleAddrs.addrs[addr]) > len(maxAddrs.addrs[addr]) {
maxAddrs = moduleAddrs
}
}
}
return maxAddrs.module, maxAddrs.addrs, nil
}

type addrSet[T any] map[libpf.AddressOrLineno]T

func goSymbolize[T any](goBinary io.ReaderAt, addrs addrSet[T]) (map[libpf.AddressOrLineno]*reporter.FrameMetadataArgs, error) {
exe, err := elf.NewFile(goBinary)
if err != nil {
return nil, err
}

lineTableData, err := exe.Section(".gopclntab").Data()
felixge marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}
lineTable := gosym.NewLineTable(lineTableData, exe.Section(".text").Addr)
felixge marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {

Check failure on line 138 in tools/coredump/gosym.go

View workflow job for this annotation

GitHub Actions / Lint (amd64)

nilness: impossible condition: nil != nil (govet)
return nil, err
}

symTableData, err := exe.Section(".gosymtab").Data()
felixge marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}

symTable, err := gosym.NewTable(symTableData, lineTable)
if err != nil {
return nil, err
}

frames := map[libpf.AddressOrLineno]*reporter.FrameMetadataArgs{}
for addr, _ := range addrs {
file, line, fn := symTable.PCToLine(uint64(addr))
frames[addr] = &reporter.FrameMetadataArgs{
FunctionName: fn.Name,
SourceFile: file,
SourceLine: libpf.SourceLineno(line),
}
}
return frames, nil
}
9 changes: 7 additions & 2 deletions tools/coredump/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package main
import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
Expand Down Expand Up @@ -73,13 +74,17 @@ func writeTestCase(path string, c *CoredumpTestCase, allowOverwrite bool) error
return fmt.Errorf("failed to create JSON file: %w", err)
}

enc := json.NewEncoder(jsonFile)
return writeTestCaseJSON(jsonFile, c)
}

// writeTestCaseJSON writes a test case to the given writer as JSON.
func writeTestCaseJSON(w io.Writer, c *CoredumpTestCase) error {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
enc.SetEscapeHTML(false)
if err := enc.Encode(c); err != nil {
return fmt.Errorf("JSON Marshall failed: %w", err)
}

return nil
}

Expand Down
1 change: 1 addition & 0 deletions tools/coredump/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func main() {
newRebaseCmd(store),
newUploadCmd(store),
newGdbCmd(store),
newGosymCmd(store),
},
Exec: func(context.Context, []string) error {
return flag.ErrHelp
Expand Down
Loading