Skip to content
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
9 changes: 1 addition & 8 deletions .github/workflows/unit-test-on-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
run: make test TARGET_ARCH=${{ matrix.target_arch }}

check-binary-blobs:
name: Check for differences in the eBPF and Rust binary blobs
name: Check for differences in the eBPF binary blobs
runs-on: ubuntu-24.04
container: otel/opentelemetry-ebpf-profiler-dev:latest@sha256:acce547f366150eb25392e1aff270df430ef6b759baeb4292999116018e70e6e
defaults:
Expand All @@ -83,18 +83,11 @@ jobs:
- name: Hash binary blobs
run: |
sha256sum support/ebpf/tracer.ebpf.release.* > binary-blobs.hash
sha256sum target/x86_64-unknown-linux-musl/release/libsymblib_capi.a >> binary-blobs.hash
sha256sum target/aarch64-unknown-linux-musl/release/libsymblib_capi.a >> binary-blobs.hash
- name: Rebuild eBPF blobs
run: |
rm support/ebpf/tracer.ebpf.release.*
make amd64 -C support/ebpf
make arm64 -C support/ebpf
- name: Rebuild Rust blobs
run: |
rm -rf target/
make rust-components TARGET_ARCH=amd64
make rust-components TARGET_ARCH=arm64
- name: Check for differences
run: |
if ! sha256sum --check binary-blobs.hash; then
Expand Down
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,3 @@ ebpf-profiler
ci-kernels
# Ignore target directory
target/*
# But not these specific paths
!target/x86_64-unknown-linux-musl/release/libsymblib_capi.a
!target/aarch64-unknown-linux-musl/release/libsymblib_capi.a
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ generate:
ebpf: generate
$(MAKE) $(EBPF_FLAGS) -C support/ebpf

ebpf-profiler: generate ebpf rust-components
ebpf-profiler: generate ebpf
go build $(GO_FLAGS) -tags $(GO_TAGS)

rust-targets:
Expand Down
114 changes: 28 additions & 86 deletions interpreter/go/go.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,16 @@

package golang // import "go.opentelemetry.io/ebpf-profiler/interpreter/go"

/*
#cgo CFLAGS: -g -Wall
#include "../../rust-crates/symblib-capi/c/symblib.h"
#include <stdlib.h>
*/
import "C"

import (
"errors"
"fmt"
"hash/fnv"
"os"
"sync/atomic"
"unsafe"

"go.opentelemetry.io/ebpf-profiler/host"
"go.opentelemetry.io/ebpf-profiler/interpreter"
"go.opentelemetry.io/ebpf-profiler/libpf"
"go.opentelemetry.io/ebpf-profiler/metrics"
"go.opentelemetry.io/ebpf-profiler/nativeunwind/elfunwindinfo"
"go.opentelemetry.io/ebpf-profiler/remotememory"
"go.opentelemetry.io/ebpf-profiler/reporter"
"go.opentelemetry.io/ebpf-profiler/successfailurecounter"
Expand All @@ -34,7 +25,7 @@ var (
)

type goData struct {
goExecutable *C.SymblibPointResolver
pclntab *elfunwindinfo.Gopclntab
}

type goInstance struct {
Expand All @@ -47,19 +38,6 @@ type goInstance struct {
d *goData
}

func mapSymblibError(status C.SymblibStatus) error {
switch status {
case C.SYMBLIB_ERR_IOFILENOTFOUND:
return os.ErrNotExist
case C.SYMBLIB_ERR_OBJFILE:
return errors.New("failed to read object file")
case C.SYMBLIB_ERR_DWARF:
return errors.New("failed to parse DWARF")
default:
return fmt.Errorf("error %d", status)
}
}

func Loader(_ interpreter.EbpfHandler, info *interpreter.LoaderInfo) (
interpreter.Data, error) {
ef, err := info.GetELF()
Expand All @@ -70,23 +48,14 @@ func Loader(_ interpreter.EbpfHandler, info *interpreter.LoaderInfo) (
return nil, nil
}

exec, err := info.ExtractAsFile()
if err != nil {
pclntab, err := elfunwindinfo.NewGopclntab(ef)
if pclntab == nil {
return nil, err
}

executablePath := C.CString(exec)
defer C.free(unsafe.Pointer(executablePath))

gd := &goData{}

//nolint:gocritic
status := C.symblib_goruntime_new(executablePath, &gd.goExecutable)
if status != C.SYMBLIB_OK {
return nil, fmt.Errorf("failed to create point resolver: %w", mapSymblibError(status))
}

return gd, nil
return &goData{
pclntab: pclntab,
}, nil
}

func (g *goData) Attach(_ interpreter.EbpfHandler, _ libpf.PID,
Expand All @@ -97,10 +66,7 @@ func (g *goData) Attach(_ interpreter.EbpfHandler, _ libpf.PID,
}

func (g *goData) Unload(_ interpreter.EbpfHandler) {
if g.goExecutable != nil {
C.symblib_goruntime_free(g.goExecutable)
g.goExecutable = nil
}
_ = g.pclntab.Close()
}

func (g *goInstance) GetAndResetMetrics() ([]metrics.Metric, error) {
Expand Down Expand Up @@ -129,55 +95,31 @@ func (g *goInstance) Symbolize(symbolReporter reporter.SymbolReporter, frame *ho
sfCounter := successfailurecounter.New(&g.successCount, &g.failCount)
defer sfCounter.DefaultToFailure()

if g.d.goExecutable == nil {
return errors.New("point resolver is out of scope")
sourceFile, lineNo, fn := g.d.pclntab.Symbolize(uintptr(frame.Lineno))
if fn == "" {
return fmt.Errorf("failed to symbolize 0x%x", frame.Lineno)
}

var symbols *C.SymblibSlice_SymblibResolvedSymbol
defer C.symblib_slice_symblibresolved_symbol_free(symbols)

//nolint:gocritic
status := C.symblib_point_resolver_symbols_for_pc(g.d.goExecutable,
C.uint64_t(frame.Lineno), &symbols)
if status != C.SYMBLIB_OK {
return fmt.Errorf("failed to do point lookup at 0x%x: %d",
frame.Lineno, status)
// The fnv hash Write() method calls cannot fail, so it's safe to ignore the errors.
h := fnv.New128a()
_, _ = h.Write([]byte(frame.File.StringNoQuotes()))
_, _ = h.Write([]byte(fn))
_, _ = h.Write([]byte(sourceFile))
fileID, err := libpf.FileIDFromBytes(h.Sum(nil))
if err != nil {
return fmt.Errorf("failed to create a file ID: %v", err)
}

// Access resolved symbols
symbolsSlice := unsafe.Slice((*C.SymblibResolvedSymbol)(unsafe.Pointer(symbols.data)),
symbols.len)
if len(symbolsSlice) == 0 {
return fmt.Errorf("failed to symbolize 0x%x", frame.Lineno)
}
frameID := libpf.NewFrameID(fileID, libpf.AddressOrLineno(lineNo))

frameFileBytes := []byte(frame.File.StringNoQuotes())
for i := 0; i < len(symbolsSlice); i++ {
lineNo := libpf.SourceLineno(symbolsSlice[i].line_number)
funcName := C.GoString(symbolsSlice[i].function_name)
sourceFile := C.GoString(symbolsSlice[i].file_name)

// The fnv hash Write() method calls cannot fail, so it's safe to ignore the errors.
h := fnv.New128a()
_, _ = h.Write(frameFileBytes)
_, _ = h.Write([]byte(funcName))
_, _ = h.Write([]byte(sourceFile))
fileID, err := libpf.FileIDFromBytes(h.Sum(nil))
if err != nil {
return fmt.Errorf("failed to create a file ID: %v", err)
}

frameID := libpf.NewFrameID(fileID, libpf.AddressOrLineno(lineNo))

trace.AppendFrameID(libpf.GoFrame, frameID)

symbolReporter.FrameMetadata(&reporter.FrameMetadataArgs{
FrameID: frameID,
FunctionName: funcName,
SourceFile: sourceFile,
SourceLine: lineNo,
})
}
trace.AppendFrameID(libpf.GoFrame, frameID)

symbolReporter.FrameMetadata(&reporter.FrameMetadataArgs{
FrameID: frameID,
FunctionName: fn,
SourceFile: sourceFile,
SourceLine: libpf.SourceLineno(lineNo),
})

sfCounter.ReportSuccess()
return nil
Expand Down
11 changes: 0 additions & 11 deletions interpreter/go/go_amd64.go

This file was deleted.

11 changes: 0 additions & 11 deletions interpreter/go/go_arm64.go

This file was deleted.

110 changes: 110 additions & 0 deletions interpreter/go/go_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package golang

import (
"os"
"runtime"
"strings"
"testing"

"go.opentelemetry.io/ebpf-profiler/host"
"go.opentelemetry.io/ebpf-profiler/interpreter"
"go.opentelemetry.io/ebpf-profiler/libpf"
"go.opentelemetry.io/ebpf-profiler/libpf/pfelf"
"go.opentelemetry.io/ebpf-profiler/process"
"go.opentelemetry.io/ebpf-profiler/remotememory"
"go.opentelemetry.io/ebpf-profiler/reporter"
"go.opentelemetry.io/ebpf-profiler/util"
)

// mockReporter implements reporter.SymbolReporter for testing
type mockReporter struct {
b *testing.B
frameMetadata map[libpf.FrameID]*reporter.FrameMetadataArgs
}

func newMockReporter(b *testing.B) *mockReporter {
return &mockReporter{
b: b,
frameMetadata: make(map[libpf.FrameID]*reporter.FrameMetadataArgs),
}
}

func (m *mockReporter) CompareFunctionName(fn string) {
if len(m.frameMetadata) != 1 {
m.b.Fatalf("Expected a single entry but got %d", len(m.frameMetadata))
}
for _, v := range m.frameMetadata {
// The returned anonymous function has the suffic 'func1'.
// Therefore check only for a matching prefix.
if !strings.HasPrefix(v.FunctionName, fn) {
m.b.Fatalf("Expected '%s()' but got '%s()'", fn, v.FunctionName)
}
}
}

func (m *mockReporter) FrameMetadata(args *reporter.FrameMetadataArgs) {
m.frameMetadata[args.FrameID] = args
}

func (m *mockReporter) ExecutableMetadata(args *reporter.ExecutableMetadataArgs) {
// Not used in this test
}

func (m *mockReporter) FrameKnown(frameID libpf.FrameID) bool {
_, exists := m.frameMetadata[frameID]
return exists
}

func (m *mockReporter) ExecutableKnown(fileID libpf.FileID) bool {
return false
}

func BenchmarkGolang(b *testing.B) {
pc, _, _, ok := runtime.Caller(1)
if !ok {
b.Fatal("Failed to get PC from runtime")
}
fn := runtime.FuncForPC(pc)
exec, err := os.Executable()
if err != nil {
b.Fatalf("Failed to get the executable: %v", err)
}

libpfPID := libpf.PID(os.Getpid())
pid := process.New(libpfPID, libpfPID)

elfRef := pfelf.NewReference(exec, pid)
hostFileID, err := host.FileIDFromBytes([]byte{0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55})
if err != nil {
b.Fatalf("Failed to create hostID: %v", err)
}
loaderInfo := interpreter.NewLoaderInfo(hostFileID, elfRef, []util.Range{})
rm := remotememory.NewProcessVirtualMemory(libpfPID)
symReporter := newMockReporter(b)

b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
gD, err := Loader(nil, loaderInfo)
if err != nil {
b.Fatalf("Failed to create loader: %v", err)
}

gI, err := gD.Attach(nil, libpfPID, 0x0, rm)
if err != nil {
b.Fatalf("Failed to create instance: %v", err)
}

trace := libpf.Trace{}

if err := gI.Symbolize(symReporter, &host.Frame{
File: hostFileID,
Lineno: libpf.AddressOrLineno(pc),
Type: libpf.FrameType(libpf.Native),
}, &trace); err != nil {
b.Fatalf("Failed to symbolize 0x%x: %v", pc, err)
}

symReporter.CompareFunctionName(fn.Name())
}
}
21 changes: 20 additions & 1 deletion libpf/pfelf/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func (f *File) Close() (err error) {
err = f.closer.Close()
f.closer = nil
}
return
return err
}

// NewFile creates a new ELF file object that borrows the given reader.
Expand Down Expand Up @@ -330,6 +330,25 @@ func getString(section []byte, start int) (string, bool) {
return string(section[start : start+slen]), true
}

// NoMmapCloser is a no-op io.Closer which is returned from Take() when
// the File is not memory mapped.
type NoMmapCloser libpf.Void

// Close implements io.Closer interface.
func (_ NoMmapCloser) Close() error {
return nil
}

// Take takes a reference on the backing mmapped data. This allows callers to
// keep slices returned by Section.Data() and Prog.Data() after File has been
// GCd. The returned Close() will release the reference on data.
func (f *File) Take() io.Closer {
if mapping, ok := f.elfReader.(*mmap.ReaderAt); ok {
return mapping.Take()
}
return NoMmapCloser{}
}

// LoadSections loads the ELF file sections
func (f *File) LoadSections() error {
if f.InsideCore {
Expand Down
Loading