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
168 changes: 168 additions & 0 deletions interpreter/beam/beam.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

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

// BEAM VM Unwinder support code

// The BEAM VM is an interpreter for Erlang, as well as several other languages
// that share the same bytecode, such as Elixir and Gleam.

import (
"fmt"
"regexp"
"unsafe"

"go.opentelemetry.io/ebpf-profiler/internal/log"

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

var (
// regex for matching the process name
beamRegex = regexp.MustCompile(`(^|\/)beam\.smp`)
_ interpreter.Data = &beamData{}
_ interpreter.Instance = &beamInstance{}
)

type beamData struct {
otpRelease string
ertsVersion string
}

type beamInstance struct {
interpreter.InstanceStubs

pid libpf.PID
data *beamData
rm remotememory.RemoteMemory
bias libpf.Address

// prefixes is indexed by the prefix added to ebpf maps (to be cleaned up) to its generation
prefixes map[lpm.Prefix]uint32
// mappingGeneration is the current generation (so old entries can be pruned)
mappingGeneration uint32
}

func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) {
matches := beamRegex.FindStringSubmatch(info.FileName())
if matches == nil {
return nil, nil
}

ef, err := info.GetELF()
if err != nil {
return nil, err
}

_, otpRelease, err := ef.SymbolData("etp_otp_release", 4)
if err != nil {
return nil, fmt.Errorf("failed to read OTP release: %v", err)
}

_, ertsVersion, err := ef.SymbolData("etp_erts_version", 64)
if err != nil {
return nil, fmt.Errorf("failed to read ERTS version: %v", err)
}

d := &beamData{
otpRelease: string(otpRelease[:len(otpRelease)-1]),
ertsVersion: string(ertsVersion[:len(ertsVersion)-1]),
}

return d, nil
}

func (d *beamData) String() string {
return fmt.Sprintf("BEAM OTP %s, ERTS %s", d.otpRelease, d.ertsVersion)
}

func (d *beamData) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, bias libpf.Address, rm remotememory.RemoteMemory) (interpreter.Instance, error) {
Comment thread
GregMefford marked this conversation as resolved.
log.Debugf("BEAM attaching, OTP %s, ERTS %s, bias: 0x%x", d.otpRelease, d.ertsVersion, bias)

data := support.BEAMProcInfo{
Bias: uint64(bias),
}

if err := ebpf.UpdateProcData(libpf.BEAM, pid, unsafe.Pointer(&data)); err != nil {
return nil, err
}

return &beamInstance{
pid: pid,
data: d,
rm: rm,
bias: bias,
prefixes: make(map[lpm.Prefix]uint32),
}, nil
}

func (d *beamData) Unload(_ interpreter.EbpfHandler) {
}

func (i *beamInstance) SynchronizeMappings(ebpf interpreter.EbpfHandler, _ reporter.ExecutableReporter, pr process.Process, mappings []process.Mapping) error {
pid := pr.PID()
i.mappingGeneration++
for idx := range mappings {
m := &mappings[idx]
if !m.IsExecutable() || !m.IsAnonymous() {
continue
}

// Just assume all anonymous and executable mappings are BEAM for now
log.Debugf("Enabling BEAM for %#x/%#x", m.Vaddr, m.Length)

prefixes, err := lpm.CalculatePrefixList(m.Vaddr, m.Vaddr+m.Length)
if err != nil {
return fmt.Errorf("new anonymous mapping lpm failure %#x/%#x", m.Vaddr, m.Length)
}

for _, prefix := range prefixes {
_, exists := i.prefixes[prefix]
if !exists {
err := ebpf.UpdatePidInterpreterMapping(pid, prefix, support.ProgUnwindBEAM, 0, 0)
if err != nil {
return err
}
}
i.prefixes[prefix] = i.mappingGeneration
}
}

// Remove prefixes not seen
for prefix, generation := range i.prefixes {
if generation == i.mappingGeneration {
continue
}
log.Debugf("Delete BEAM prefix %#v", prefix)
_ = ebpf.DeletePidInterpreterMapping(pid, prefix)
delete(i.prefixes, prefix)
}

return nil
}

func (i *beamInstance) Detach(interpreter.EbpfHandler, libpf.PID) error {
return nil
}

func (i *beamInstance) Symbolize(frame *host.Frame, frames *libpf.Frames) error {
if !frame.Type.IsInterpType(libpf.BEAM) {
return interpreter.ErrMismatchInterpreterType
}

frames.Append(&libpf.Frame{
Type: libpf.BEAMFrame,
SourceFile: libpf.Intern("Unknown File"),
SourceLine: libpf.SourceLineno(frame.Lineno),
})

return nil
}
2 changes: 2 additions & 0 deletions libpf/frametype.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ const (
DotnetFrame FrameType = support.FrameMarkerDotnet
// GoFrame identifies Go frames.
GoFrame FrameType = support.FrameMarkerGo
// BEAMFrame identifies the BEAM interpreter frames.
BEAMFrame FrameType = support.FrameMarkerBEAM
// AbortFrame identifies frames that report that further unwinding was aborted due to an error.
AbortFrame FrameType = support.FrameMarkerAbort
)
Expand Down
3 changes: 3 additions & 0 deletions libpf/interpretertype.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const (
Dotnet InterpreterType = support.FrameMarkerDotnet
// Go identifies Go code.
Go InterpreterType = support.FrameMarkerGo
// BEAM identifies the BEAM interpreter.
BEAM InterpreterType = support.FrameMarkerBEAM
)

// Pseudo-interpreters without a corresponding frame type.
Expand Down Expand Up @@ -69,6 +71,7 @@ var interpreterTypeToString = map[InterpreterType]string{
Perl: "perl",
V8: "v8js",
Dotnet: "dotnet",
BEAM: "beam",
APMInt: "apm-integration",
Go: "go",
GoLabels: "go-labels",
Expand Down
3 changes: 3 additions & 0 deletions processmanager/ebpf/ebpf.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type ebpfMapsImpl struct {
PhpProcs *cebpf.Map `name:"php_procs"`
RubyProcs *cebpf.Map `name:"ruby_procs"`
V8Procs *cebpf.Map `name:"v8_procs"`
BeamProcs *cebpf.Map `name:"beam_procs"`
ApmIntProcs *cebpf.Map `name:"apm_int_procs"`
GoLabelsProcs *cebpf.Map `name:"go_labels_procs"`

Expand Down Expand Up @@ -153,6 +154,8 @@ func (impl *ebpfMapsImpl) getInterpreterTypeMap(typ libpf.InterpreterType) (*ceb
return impl.RubyProcs, nil
case libpf.V8:
return impl.V8Procs, nil
case libpf.BEAM:
return impl.BeamProcs, nil
case libpf.APMInt:
return impl.ApmIntProcs, nil
case libpf.GoLabels:
Expand Down
4 changes: 4 additions & 0 deletions processmanager/execinfomanager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"go.opentelemetry.io/ebpf-profiler/host"
"go.opentelemetry.io/ebpf-profiler/interpreter"
"go.opentelemetry.io/ebpf-profiler/interpreter/apmint"
"go.opentelemetry.io/ebpf-profiler/interpreter/beam"
"go.opentelemetry.io/ebpf-profiler/interpreter/dotnet"
golang "go.opentelemetry.io/ebpf-profiler/interpreter/go"
"go.opentelemetry.io/ebpf-profiler/interpreter/golabels"
Expand Down Expand Up @@ -120,6 +121,9 @@ func NewExecutableInfoManager(
if includeTracers.Has(types.GoTracer) {
interpreterLoaders = append(interpreterLoaders, golang.Loader)
}
if includeTracers.Has(types.BEAMTracer) {
interpreterLoaders = append(interpreterLoaders, beam.Loader)
}

interpreterLoaders = append(interpreterLoaders, apmint.Loader)
if includeTracers.Has(types.Labels) {
Expand Down
53 changes: 53 additions & 0 deletions support/ebpf/beam_tracer.ebpf.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#include "bpfdefs.h"
#include "tracemgmt.h"
#include "types.h"

struct beam_procs_t {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, pid_t);
__type(value, BEAMProcInfo);
__uint(max_entries, 256);
} beam_procs SEC(".maps");

// unwind_beam is the entry point for tracing when invoked from the native tracer
// or interpreter dispatcher. It does not reset the trace object and will append the
// BEAM stack frames to the trace object for the current CPU.
static EBPF_INLINE int unwind_beam(struct pt_regs *ctx)
{

int unwinder = PROG_UNWIND_STOP;
ErrorCode error = ERR_OK;

PerCPURecord *record = get_per_cpu_record();
if (!record) {
DEBUG_PRINT("beam: no PerCPURecord found");
return -1;
}

Trace *trace = &record->trace;
UnwindState *state = &record->state;
u32 pid = trace->pid;

BEAMProcInfo *info = bpf_map_lookup_elem(&beam_procs, &pid);

if (!info) {
Comment thread
GregMefford marked this conversation as resolved.
DEBUG_PRINT("beam: no BEAMProcInfo for this pid");
error = ERR_BEAM_NO_PROC_INFO;
goto exit;
}

unwinder_mark_nonleaf_frame(state);
_push_with_return_address(trace, 0LL, state->pc, FRAME_MARKER_BEAM, state->return_address);
// Pretend that there was an error unwinding for now,
// so that we don't have an infinite loop,
// since we're not actually unwinding / updating the state.
error = ERR_BEAM_PC_INVALID;

exit:
record->state.unwind_error = error;
tail_call(ctx, unwinder);
DEBUG_PRINT("beam: tail call for next frame unwinder (%d) failed", unwinder);
return -1;
}

MULTI_USE_FUNC(unwind_beam)
8 changes: 7 additions & 1 deletion support/ebpf/errors.h
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,13 @@ typedef enum ErrorCode {
ERR_DOTNET_CODE_HEADER = 6002,

// Dotnet: Code object was too large to unwind in eBPF
ERR_DOTNET_CODE_TOO_LARGE = 6003
ERR_DOTNET_CODE_TOO_LARGE = 6003,

// BEAM: No entry for this process exists in the BEAM process info array
ERR_BEAM_NO_PROC_INFO = 7000,

// BEAM: PC was outside the expected address range
ERR_BEAM_PC_INVALID = 7001
} ErrorCode;

#endif // OPTI_ERRORS_H
1 change: 1 addition & 0 deletions support/ebpf/extmaps.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ extern struct go_labels_procs_t go_labels_procs;
// are needed only for testing.

extern struct apm_int_procs_t apm_int_procs;
extern struct beam_procs_t beam_procs;
extern struct exe_id_to_8_stack_deltas_t exe_id_to_8_stack_deltas;
extern struct exe_id_to_9_stack_deltas_t exe_id_to_9_stack_deltas;
extern struct exe_id_to_10_stack_deltas_t exe_id_to_10_stack_deltas;
Expand Down
2 changes: 2 additions & 0 deletions support/ebpf/frametypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
#define FRAME_MARKER_DOTNET 0xA
// Indicates a Go frame
#define FRAME_MARKER_GO 0xB
// Indicates a BEAM frame
#define FRAME_MARKER_BEAM 0xC

// Indicates a frame containing information about a critical unwinding error
// that caused further unwinding to be aborted.
Expand Down
Binary file modified support/ebpf/tracer.ebpf.amd64
Binary file not shown.
Binary file modified support/ebpf/tracer.ebpf.arm64
Binary file not shown.
6 changes: 6 additions & 0 deletions support/ebpf/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ typedef enum TracePrograms {
PROG_UNWIND_V8,
PROG_UNWIND_DOTNET,
PROG_GO_LABELS,
PROG_UNWIND_BEAM,
NUM_TRACER_PROGS,
} TracePrograms;

Expand Down Expand Up @@ -513,6 +514,11 @@ typedef struct V8ProcInfo {
u8 codekind_shift, codekind_mask, codekind_baseline;
} V8ProcInfo;

// BEAMProcInfo is a container for the data needed to build a stack trace for a BEAM process.
typedef struct BEAMProcInfo {
u64 bias;
} BEAMProcInfo;

// COMM_LEN defines the maximum length we will receive for the comm of a task.
#define COMM_LEN 16

Expand Down
5 changes: 5 additions & 0 deletions support/types.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions support/types_def.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const (
FrameMarkerV8 = C.FRAME_MARKER_V8
FrameMarkerDotnet = C.FRAME_MARKER_DOTNET
FrameMarkerGo = C.FRAME_MARKER_GO
FrameMarkerBEAM = C.FRAME_MARKER_BEAM
FrameMarkerAbort = C.FRAME_MARKER_ABORT
)

Expand All @@ -45,6 +46,7 @@ const (
ProgUnwindV8 = C.PROG_UNWIND_V8
ProgUnwindDotnet = C.PROG_UNWIND_DOTNET
ProgGoLabels = C.PROG_GO_LABELS
ProgUnwindBEAM = C.PROG_UNWIND_BEAM
)

const (
Expand Down Expand Up @@ -121,6 +123,7 @@ type Trace C.Trace
type UnwindInfo C.UnwindInfo

type ApmIntProcInfo C.ApmIntProcInfo
type BEAMProcInfo C.BEAMProcInfo
type DotnetProcInfo C.DotnetProcInfo
type GoLabelsOffsets C.GoLabelsOffsets
type HotspotProcInfo C.HotspotProcInfo
Expand Down
Loading