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
139 changes: 139 additions & 0 deletions tools/coredump/gosym.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package main

import (
"context"
"debug/elf"
"debug/gosym"
"errors"
"flag"
"fmt"
"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)
}

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

for addr, originFrames := range addrs {
file, line, fn := symTable.PCToLine(uint64(addr))
for _, originFrame := range originFrames {
frame := reporter.FrameMetadataArgs{
FunctionName: fn.Name,
SourceFile: file,
SourceLine: libpf.SourceLineno(line),
}
*originFrame = formatSymbolizedFrame(&frame, false) + " (" + *originFrame + ")"
}
}

return writeTestCaseJSON(os.Stdout, test)
}

// goModuleAddrs returns the symtable for the go module of test case and the
// addresses to symbolize for it mapped to pointers to the frames in the test
// case that reference them.
func goModuleAddrs(store *modulestore.Store, c *CoredumpTestCase) (*gosym.Table, map[libpf.AddressOrLineno][]*string, error) {
var symTable *gosym.Table
var module *ModuleInfo
for i := range c.Modules {
if table, err := gosymTable(store, &c.Modules[i]); err != nil {
continue
} else if symTable != nil {
return nil, nil, fmt.Errorf("multiple go modules found")

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

View workflow job for this annotation

GitHub Actions / Lint (arm64)

fmt.Errorf can be replaced with errors.New (perfsprint)
} else {

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

View workflow job for this annotation

GitHub Actions / Lint (arm64)

indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) (revive)
symTable = table
module = &c.Modules[i]
}
}

addrs := map[libpf.AddressOrLineno][]*string{}
moduleName := filepath.Base(module.LocalPath)
for _, thread := range c.Threads {
for i, frame := range thread.Frames {
frameModuleName, addr, err := parseUnsymbolizedFrame(frame)
if err != nil {
continue
}

if frameModuleName != moduleName {
continue
}

addrs[addr] = append(addrs[addr], &thread.Frames[i])
}
}
return symTable, addrs, nil
}

func gosymTable(store *modulestore.Store, module *ModuleInfo) (*gosym.Table, error) {
reader, err := store.OpenReadAt(module.Ref)
if err != nil {
return nil, fmt.Errorf("failed to open module: %w", err)
}
defer reader.Close()

exe, err := elf.NewFile(reader)
if err != nil {
return nil, err
}

textSection := exe.Section(".text")
felixge marked this conversation as resolved.
Show resolved Hide resolved
if textSection == nil {
return nil, errors.New("missing .text section")
}

pclntab := exe.Section(".gopclntab")
if pclntab == nil {
return nil, errors.New("missing .gopclntab section")
}

lineTableData, err := pclntab.Data()
if err != nil {
return nil, err
}
lineTable := gosym.NewLineTable(lineTableData, textSection.Addr)
if err != nil {

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

View workflow job for this annotation

GitHub Actions / Lint (arm64)

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

return gosym.NewTable(nil, lineTable)
}
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