Skip to content

Commit 93d0726

Browse files
committed
Initial support for BEAM interpreter
1 parent 03e458b commit 93d0726

File tree

10 files changed

+202
-0
lines changed

10 files changed

+202
-0
lines changed

interpreter/beam/beam.go

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package beam // import "go.opentelemetry.io/ebpf-profiler/interpreter/beam"
5+
6+
// BEAM VM Unwinder support code
7+
8+
// The BEAM VM is an interpreter for Erlang, as well as several other languages
9+
// that share the same bytecode, such as Elixir and Gleam.
10+
11+
import (
12+
"fmt"
13+
"regexp"
14+
"strconv"
15+
16+
log "github.com/sirupsen/logrus"
17+
18+
"go.opentelemetry.io/ebpf-profiler/host"
19+
"go.opentelemetry.io/ebpf-profiler/interpreter"
20+
"go.opentelemetry.io/ebpf-profiler/libpf"
21+
"go.opentelemetry.io/ebpf-profiler/libpf/pfelf"
22+
"go.opentelemetry.io/ebpf-profiler/remotememory"
23+
"go.opentelemetry.io/ebpf-profiler/reporter"
24+
"go.opentelemetry.io/ebpf-profiler/support"
25+
)
26+
27+
var (
28+
// regex for matching the process name
29+
beamRegex = regexp.MustCompile(`beam.smp`)
30+
_ interpreter.Data = &beamData{}
31+
_ interpreter.Instance = &beamInstance{}
32+
)
33+
34+
type beamData struct {
35+
version uint32
36+
}
37+
38+
type beamInstance struct {
39+
interpreter.InstanceStubs
40+
41+
data *beamData
42+
rm remotememory.RemoteMemory
43+
}
44+
45+
func readSymbolValue(ef *pfelf.File, name libpf.SymbolName) ([]byte, error) {
46+
sym, err := ef.LookupSymbol(name)
47+
if err != nil {
48+
return nil, fmt.Errorf("symbol not found: %v", err)
49+
}
50+
51+
memory := make([]byte, sym.Size)
52+
if _, err := ef.ReadVirtualMemory(memory, int64(sym.Address)); err != nil {
53+
return nil, fmt.Errorf("failed to read process memory at 0x%x:%v", sym.Address, err)
54+
}
55+
56+
log.Infof("read symbol value %s: %s", sym.Name, memory)
57+
return memory, nil
58+
}
59+
func readReleaseVersion(ef *pfelf.File) (uint32, []byte, error) {
60+
otp_release, err := readSymbolValue(ef, "etp_otp_release")
61+
if err != nil {
62+
return 0, nil, fmt.Errorf("failed to read OTP release: %v", err)
63+
}
64+
65+
// Slice off the null termination before converting
66+
otp_major, err := strconv.Atoi(string(otp_release[:len(otp_release)-1]))
67+
if err != nil {
68+
return 0, nil, fmt.Errorf("failed to parse OTP version: %v", err)
69+
}
70+
71+
erts_version, err := readSymbolValue(ef, "etp_erts_version")
72+
if err != nil {
73+
return 0, nil, fmt.Errorf("failed to read erts version: %v", err)
74+
}
75+
76+
return uint32(otp_major), erts_version, nil
77+
}
78+
79+
func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) {
80+
matches := beamRegex.FindStringSubmatch(info.FileName())
81+
if matches == nil {
82+
return nil, nil
83+
}
84+
log.Infof("BEAM interpreter found: %v", matches)
85+
86+
ef, err := info.GetELF()
87+
if err != nil {
88+
return nil, err
89+
}
90+
91+
otp_version, _, err := readReleaseVersion(ef)
92+
if err != nil {
93+
return nil, err
94+
}
95+
96+
symbolName := libpf.SymbolName("process_main")
97+
interpRanges, err := info.GetSymbolAsRanges(symbolName)
98+
if err != nil {
99+
return nil, err
100+
}
101+
102+
if err = ebpf.UpdateInterpreterOffsets(support.ProgUnwindBEAM, info.FileID(), interpRanges); err != nil {
103+
return nil, err
104+
}
105+
106+
d := &beamData{
107+
version: otp_version,
108+
}
109+
110+
log.Infof("BEAM loaded, otp_version: %d, interpRanges: %v", otp_version, interpRanges)
111+
//d.loadIntrospectionData()
112+
113+
return d, nil
114+
}
115+
116+
func (d *beamData) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, bias libpf.Address, rm remotememory.RemoteMemory) (interpreter.Instance, error) {
117+
log.Infof("BEAM interpreter attaching")
118+
return &beamInstance{
119+
data: d,
120+
rm: rm,
121+
}, nil
122+
}
123+
124+
func (r *beamInstance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error {
125+
return nil
126+
}
127+
128+
func (r *beamInstance) Symbolize(symbolReporter reporter.SymbolReporter, frame *host.Frame, trace *libpf.Trace) error {
129+
if !frame.Type.IsInterpType(libpf.BEAM) {
130+
log.Warnf("BEAM failed to symbolize")
131+
return interpreter.ErrMismatchInterpreterType
132+
}
133+
log.Infof("BEAM symbolizing")
134+
return nil
135+
}

libpf/frametype.go

+2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ const (
4949
V8Frame FrameType = support.FrameMarkerV8
5050
// DotnetFrame identifies the Dotnet interpreter frames.
5151
DotnetFrame FrameType = support.FrameMarkerDotnet
52+
// BEAMFrame identifies the BEAM interpreter frames.
53+
BEAMFrame FrameType = support.FrameMarkerBEAM
5254
// AbortFrame identifies frames that report that further unwinding was aborted due to an error.
5355
AbortFrame FrameType = support.FrameMarkerAbort
5456
)

libpf/interpretertype.go

+3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ const (
3131
V8 InterpreterType = support.FrameMarkerV8
3232
// Dotnet identifies the Dotnet interpreter.
3333
Dotnet InterpreterType = support.FrameMarkerDotnet
34+
// BEAM identifies the BEAM interpreter.
35+
BEAM InterpreterType = support.FrameMarkerBEAM
3436
)
3537

3638
// Pseudo-interpreters without a corresponding frame type.
@@ -64,6 +66,7 @@ var interpreterTypeToString = map[InterpreterType]string{
6466
Perl: "perl",
6567
V8: "v8js",
6668
Dotnet: "dotnet",
69+
BEAM: "beam",
6770
APMInt: "apm-integration",
6871
}
6972

processmanager/ebpf/ebpf.go

+9
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ type ebpfMapsImpl struct {
102102
phpProcs *cebpf.Map
103103
rubyProcs *cebpf.Map
104104
v8Procs *cebpf.Map
105+
beamProcs *cebpf.Map
105106
apmIntProcs *cebpf.Map
106107

107108
// Stackdelta and process related eBPF maps
@@ -199,6 +200,12 @@ func LoadMaps(ctx context.Context, maps map[string]*cebpf.Map) (EbpfHandler, err
199200
}
200201
impl.v8Procs = v8Procs
201202

203+
beamProcs, ok := maps["beam_procs"]
204+
if !ok {
205+
log.Fatalf("Map beam_procs is not available")
206+
}
207+
impl.beamProcs = beamProcs
208+
202209
apmIntProcs, ok := maps["apm_int_procs"]
203210
if !ok {
204211
log.Fatalf("Map apm_int_procs is not available")
@@ -294,6 +301,8 @@ func (impl *ebpfMapsImpl) getInterpreterTypeMap(typ libpf.InterpreterType) (*ceb
294301
return impl.rubyProcs, nil
295302
case libpf.V8:
296303
return impl.v8Procs, nil
304+
case libpf.BEAM:
305+
return impl.beamProcs, nil
297306
case libpf.APMInt:
298307
return impl.apmIntProcs, nil
299308
default:

processmanager/execinfomanager/manager.go

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"go.opentelemetry.io/ebpf-profiler/host"
1919
"go.opentelemetry.io/ebpf-profiler/interpreter"
2020
"go.opentelemetry.io/ebpf-profiler/interpreter/apmint"
21+
"go.opentelemetry.io/ebpf-profiler/interpreter/beam"
2122
"go.opentelemetry.io/ebpf-profiler/interpreter/dotnet"
2223
"go.opentelemetry.io/ebpf-profiler/interpreter/hotspot"
2324
"go.opentelemetry.io/ebpf-profiler/interpreter/nodev8"
@@ -124,6 +125,9 @@ func NewExecutableInfoManager(
124125
if includeTracers.Has(types.DotnetTracer) {
125126
interpreterLoaders = append(interpreterLoaders, dotnet.Loader)
126127
}
128+
if includeTracers.Has(types.BEAMTracer) {
129+
interpreterLoaders = append(interpreterLoaders, beam.Loader)
130+
}
127131

128132
interpreterLoaders = append(interpreterLoaders, apmint.Loader)
129133

support/ebpf/beam_tracer.ebpf.c

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#include "bpfdefs.h"
2+
#include "tracemgmt.h"
3+
#include "types.h"
4+
5+
bpf_map_def SEC("maps") beam_procs = {
6+
.type = BPF_MAP_TYPE_HASH,
7+
.key_size = sizeof(pid_t),
8+
.value_size = sizeof(BEAMProcInfo),
9+
.max_entries = 1024,
10+
};
11+
12+
SEC("perf_event/unwind_beam")
13+
int unwind_beam(struct pt_regs *ctx) {
14+
static const char fmt[] = "Unwinding BEAM stack";
15+
bpf_trace_printk(fmt, sizeof(fmt));
16+
DEBUG_PRINT("Unwinding BEAM stack");
17+
18+
PerCPURecord *record = get_per_cpu_record();
19+
if (!record) {
20+
return -1;
21+
}
22+
23+
int unwinder = get_next_unwinder_after_interpreter(record);
24+
u32 pid = record->trace.pid;
25+
26+
BEAMProcInfo *beaminfo = bpf_map_lookup_elem(&beam_procs, &pid);
27+
if (!beaminfo) {
28+
DEBUG_PRINT("No BEAM introspection data");
29+
goto exit;
30+
}
31+
32+
DEBUG_PRINT("==== unwind_beam stack_len: %d, pid: %d ====", record->trace.stack_len, record->trace.pid);
33+
34+
exit:
35+
tail_call(ctx, unwinder);
36+
return -1;
37+
}

support/ebpf/frametypes.h

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
#define FRAME_MARKER_PHP_JIT 0x9
3434
// Indicates a Dotnet frame
3535
#define FRAME_MARKER_DOTNET 0xA
36+
// Indicates a BEAM frame
37+
#define FRAME_MARKER_BEAM 0xB
3638

3739
// Indicates a frame containing information about a critical unwinding error
3840
// that caused further unwinding to be aborted.

support/ebpf/types.h

+6
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ typedef enum TracePrograms {
328328
PROG_UNWIND_RUBY,
329329
PROG_UNWIND_V8,
330330
PROG_UNWIND_DOTNET,
331+
PROG_UNWIND_BEAM,
331332
NUM_TRACER_PROGS,
332333
} TracePrograms;
333334

@@ -477,6 +478,11 @@ typedef struct V8ProcInfo {
477478
u8 codekind_shift, codekind_mask, codekind_baseline;
478479
} V8ProcInfo;
479480

481+
// BEAMProcInfo is a container for the data needed to build a stack trace for a BEAM process.
482+
typedef struct BEAMProcInfo {
483+
u32 version;
484+
} BEAMProcInfo;
485+
480486
// COMM_LEN defines the maximum length we will receive for the comm of a task.
481487
#define COMM_LEN 16
482488

support/types.go

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const (
2424
FrameMarkerPerl = C.FRAME_MARKER_PERL
2525
FrameMarkerV8 = C.FRAME_MARKER_V8
2626
FrameMarkerDotnet = C.FRAME_MARKER_DOTNET
27+
FrameMarkerBEAM = C.FRAME_MARKER_BEAM
2728
FrameMarkerAbort = C.FRAME_MARKER_ABORT
2829
)
2930

@@ -37,6 +38,7 @@ const (
3738
ProgUnwindPerl = C.PROG_UNWIND_PERL
3839
ProgUnwindV8 = C.PROG_UNWIND_V8
3940
ProgUnwindDotnet = C.PROG_UNWIND_DOTNET
41+
ProgUnwindBEAM = C.PROG_UNWIND_BEAM
4042
)
4143

4244
const (

tracer/types/parse.go

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const (
2222
RubyTracer
2323
V8Tracer
2424
DotnetTracer
25+
BEAMTracer
2526

2627
// maxTracers indicates the max. number of different tracers
2728
maxTracers
@@ -35,6 +36,7 @@ var tracerTypeToName = map[tracerType]string{
3536
RubyTracer: "ruby",
3637
V8Tracer: "v8",
3738
DotnetTracer: "dotnet",
39+
BEAMTracer: "beam",
3840
}
3941

4042
var tracerNameToType = make(map[string]tracerType, maxTracers)

0 commit comments

Comments
 (0)