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

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

import (
"debug/elf"
"fmt"

"go.opentelemetry.io/ebpf-profiler/asm/amd"
"go.opentelemetry.io/ebpf-profiler/asm/arm"
"go.opentelemetry.io/ebpf-profiler/internal/log"
"go.opentelemetry.io/ebpf-profiler/libpf"
"go.opentelemetry.io/ebpf-profiler/libpf/pfelf"
)

// extractEcTLSOffset extracts the direct TP-relative TLS offset for ruby_current_ec
// by disassembling rb_current_ec_noinline. This is used for statically-linked Ruby
// binaries where TLS descriptors are not available.
//
// The function uses the same TLS extraction infrastructure as Python 3.13+
// (asm/amd.ExtractTLSOffset and asm/arm.ExtractTLSOffset).
func extractEcTLSOffset(ef *pfelf.File) (int64, error) {
symbolName := libpf.SymbolName("rb_current_ec_noinline")
sym, code, err := ef.SymbolData(symbolName, 2048)
if err != nil {
// Fallback: try VisitSymbols for binaries with local symbols not in .dynsym
sym = &libpf.Symbol{}
found := false
if visitErr := ef.VisitSymbols(func(s libpf.Symbol) bool {
if s.Name == symbolName {
data, readErr := ef.VirtualMemory(int64(s.Address), int(s.Size), 2048)
if readErr != nil {
log.Errorf("Failed to read memory for %s: %v", symbolName, readErr)
} else {
code = data
sym.Address = s.Address
found = true
}
return false
}
return true
}); visitErr != nil {
return 0, fmt.Errorf("failed to visit symbols: %w", visitErr)
}
if !found {
return 0, fmt.Errorf("symbol %s not found", symbolName)
}
}

if len(code) < 4 {
return 0, fmt.Errorf("%s function too small (%d bytes)", symbolName, len(code))
}

var offset int32
switch ef.Machine {
case elf.EM_X86_64:
offset, err = amd.ExtractTLSOffset(code, uint64(sym.Address), nil)
case elf.EM_AARCH64:
offset, err = arm.ExtractTLSOffset(code, uint64(sym.Address), ef)
default:
return 0, fmt.Errorf("unsupported architecture: %s", ef.Machine)
}
if err != nil {
return 0, fmt.Errorf("failed to extract TLS offset from %s: %w", symbolName, err)
}

return int64(offset), nil
}
77 changes: 77 additions & 0 deletions interpreter/ruby/ec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package ruby

import (
"debug/elf"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/ebpf-profiler/asm/amd"
"go.opentelemetry.io/ebpf-profiler/asm/arm"
)

func TestExtractEcTLSOffset(t *testing.T) {
tests := map[string]struct {
machine elf.Machine
code []byte
offset int32
}{
// rb_current_ec_noinline for statically-linked ruby 4.0 on x86_64:
// mov %fs:0xffffffffffffff88,%rax
// ret
"ruby 4.0 static / x86_64": {
machine: elf.EM_X86_64,
code: []byte{
0x64, 0x48, 0x8b, 0x04, 0x25, 0x88, 0xff, 0xff, 0xff,
0xc3,
},
offset: -120,
},
// rb_current_ec_noinline for statically-linked ruby 3.4.7 on x86_64:
// mov %fs:0xfffffffffffffff8,%rax
// ret
"ruby 3.4.7 static / x86_64": {
machine: elf.EM_X86_64,
code: []byte{
0x64, 0x48, 0x8b, 0x04, 0x25, 0xf8, 0xff, 0xff, 0xff,
0xc3,
},
offset: -8,
},
// rb_current_ec_noinline for statically-linked ruby 3.4.7 on aarch64:
// mrs x0, tpidr_el0
// add x0, x0, #0x0, lsl #12
// add x0, x0, #0x38
// ldr x0, [x0]
// ret
"ruby 3.4.7 static / aarch64": {
machine: elf.EM_AARCH64,
code: []byte{
0x40, 0xd0, 0x3b, 0xd5, // mrs x0, tpidr_el0
0x00, 0x00, 0x40, 0x91, // add x0, x0, #0x0, lsl #12
0x00, 0xe0, 0x00, 0x91, // add x0, x0, #0x38
0x00, 0x00, 0x40, 0xf9, // ldr x0, [x0]
0xc0, 0x03, 0x5f, 0xd6, // ret
},
offset: 56,
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
var offset int32
var err error
switch tc.machine {
case elf.EM_X86_64:
offset, err = amd.ExtractTLSOffset(tc.code, 0, nil)
case elf.EM_AARCH64:
offset, err = arm.ExtractTLSOffset(tc.code, 0, nil)
}
require.NoError(t, err)
assert.Equal(t, tc.offset, offset, "wrong ruby EC TLS offset")
})
}
}
40 changes: 33 additions & 7 deletions interpreter/ruby/ruby.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,10 @@ const (
)

var (
// regex to identify the Ruby interpreter executable
rubyRegex = regexp.MustCompile(`^(?:.*/)?libruby(?:-.*)?\.so\.(\d)\.(\d)\.(\d)$`)
// regex to identify the Ruby interpreter shared library
libRubyRegex = regexp.MustCompile(`^(?:.*/)?libruby(?:-.*)?\.so\.(\d)\.(\d)\.(\d)$`)
// regex to identify a statically-linked Ruby binary
binRubyRegex = regexp.MustCompile(`^(?:.*/)?(?:bin/)?ruby$`)
// regex to extract a version from a string
rubyVersionRegex = regexp.MustCompile(`^(\d)\.(\d)\.(\d)$`)

Expand All @@ -123,6 +125,10 @@ type rubyData struct {
// Address to the ruby_current_ec variable in TLS, as an offset from tpbase
currentEcTpBaseTlsOffset libpf.Address

// For statically-linked ruby, the direct TP-relative offset to ruby_current_ec
// extracted from disassembly of rb_current_ec_noinline
staticTLSOffset int64

// Address to global symbols, for id to string mappings
globalSymbolsAddr libpf.Address
// version of the currently used Ruby interpreter.
Expand Down Expand Up @@ -295,10 +301,14 @@ func (r *rubyData) String() string {
func (r *rubyData) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, bias libpf.Address,
rm remotememory.RemoteMemory,
) (interpreter.Instance, error) {
var tlsOffset uint64
if r.currentEcTpBaseTlsOffset != 0 {
var tlsOffset int64
if r.staticTLSOffset != 0 {
// For statically-linked ruby, use the direct TP-relative offset
// extracted from disassembly of rb_current_ec_noinline.
tlsOffset = r.staticTLSOffset
} else if r.currentEcTpBaseTlsOffset != 0 {
// Read TLS offset from the TLS descriptor.
tlsOffset = rm.Uint64(bias + r.currentEcTpBaseTlsOffset + 8)
tlsOffset = int64(rm.Uint64(bias + r.currentEcTpBaseTlsOffset + 8))
}

cdata := support.RubyProcInfo{
Expand Down Expand Up @@ -1238,7 +1248,8 @@ func determineRubyVersion(ef *pfelf.File) (uint32, error) {
}

func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) {
if !rubyRegex.MatchString(info.FileName()) {
isBinRuby := binRubyRegex.MatchString(info.FileName())
if !libRubyRegex.MatchString(info.FileName()) && !isBinRuby {
return nil, nil
}

Expand Down Expand Up @@ -1363,11 +1374,26 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr
log.Warnf("failed to locate TLS descriptor: %v", err)
}

log.Debugf("Discovered EC tls tpbase offset %x, fallback ctx %x, interp ranges: %v, global symbols: %x", currentEcTpBaseTlsOffset, currentCtxPtr, interpRanges, globalSymbols)
// For statically-linked ruby, extract the direct TP-relative offset from
// rb_current_ec_noinline disassembly. This is the same pattern Python 3.13+
// uses for _PyThreadState_GetCurrent.
var staticTLSOffset int64
if isBinRuby {
offset, ecErr := extractEcTLSOffset(ef)
if ecErr != nil {
log.Warnf("failed to extract EC TLS offset for static ruby: %v", ecErr)
} else {
staticTLSOffset = offset
}
}

log.Debugf("Discovered EC tls tpbase offset %x, static tls offset %d, fallback ctx %x, interp ranges: %v, global symbols: %x",
currentEcTpBaseTlsOffset, staticTLSOffset, currentCtxPtr, interpRanges, globalSymbols)

rid := &rubyData{
version: version,
currentEcTpBaseTlsOffset: libpf.Address(currentEcTpBaseTlsOffset),
staticTLSOffset: staticTLSOffset,
currentCtxPtr: libpf.Address(currentCtxPtr),
hasGlobalSymbols: globalSymbols != 0,
globalSymbolsAddr: libpf.Address(globalSymbols),
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.
5 changes: 3 additions & 2 deletions support/ebpf/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -468,8 +468,9 @@ typedef struct RubyProcInfo {
// version of the Ruby interpreter.
u32 version;

// tls_offset holds TLS base + ruby_current_ec tls symbol, as an offset from tpbase
u64 current_ec_tpbase_tls_offset;
// tls_offset holds TLS base + ruby_current_ec tls symbol, as an offset from tpbase.
// Signed because static TLS offsets (local exec model) are negative on x86_64.
s64 current_ec_tpbase_tls_offset;

// current_ctx_ptr holds the address of the symbol ruby_current_execution_context_ptr.
u64 current_ctx_ptr;
Expand Down
2 changes: 1 addition & 1 deletion support/types.go

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

89 changes: 89 additions & 0 deletions tools/coredump/testdata/amd64/ruby-3.4.7-static-loop.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{
"coredump-ref": "17a69422179148059ca9465a0cbc351a03da993c93cc9cebbd2fe0e4a661c132",
"threads": [
{
"lwp": 18596,
"frames": [
"Object#is_prime+0 in /home/dalehamel/opentelemetry-ebpf-profiler/tools/coredump/testsources/ruby/loop.rb:14",
"ruby+0x203060",
"ruby+0x207e97",
"ruby+0x11a9cd",
"ruby+0x1f380b",
"ruby+0x1fa1d0",
"ruby+0x214344",
"Range#each+0 in <cfunc>:0",
"Object#is_prime+0 in /home/dalehamel/opentelemetry-ebpf-profiler/tools/coredump/testsources/ruby/loop.rb:14",
"Object#sum_of_primes+0 in /home/dalehamel/opentelemetry-ebpf-profiler/tools/coredump/testsources/ruby/loop.rb:24",
"block (2 levels) in <main>+0 in /home/dalehamel/opentelemetry-ebpf-profiler/tools/coredump/testsources/ruby/loop.rb:34",
"ruby+0x203231",
"ruby+0x207e97",
"ruby+0x11a8bd",
"ruby+0x1f24f4",
"ruby+0x1fa1d0",
"ruby+0x214344",
"Range#each+0 in <cfunc>:0",
"block in <main>+0 in /home/dalehamel/opentelemetry-ebpf-profiler/tools/coredump/testsources/ruby/loop.rb:33",
"Kernel#loop+0 in <internal:kernel>:168",
"<main>+0 in /home/dalehamel/opentelemetry-ebpf-profiler/tools/coredump/testsources/ruby/loop.rb:32",
"ruby+0x2031b4",
"ruby+0x406f8",
"ruby+0x4263c",
"ruby+0x3d171",
"libc.so.6+0x27249",
"libc.so.6+0x27304",
"ruby+0x3d1b0"
]
},
{
"lwp": 18598,
"frames": [
"libc.so.6+0x108f26",
"ruby+0x1b414e",
"libc.so.6+0x891f4",
"libc.so.6+0x1098db"
]
}
],
"modules": [
{
"ref": "6363d41d2a3a7e65b6f44a2fa55234b3bd8ad1497d1b6c5892635b4cbacbaa24",
"local-path": "/home/dalehamel/.rubies/ruby-3.4.7/lib/ruby/3.4.0/x86_64-linux/enc/trans/transdb.so"
},
{
"ref": "7f2ca87f652f56b094462474b076749e90e689d0ecb9cb63c7679820b271b4e7",
"local-path": "/usr/lib/x86_64-linux-gnu/libm.so.6"
},
{
"ref": "5db18e8a8894ef4746eb8230855b638a5e52e782b2f10deede5f1dad846178bb",
"local-path": "/usr/lib/x86_64-linux-gnu/libcrypt.so.1.1.0"
},
{
"ref": "7376c9af0afd6e7698a64ee19de3c8a0199418664974384c70435a51c7ff7f3f",
"local-path": "/usr/lib/x86_64-linux-gnu/libgmp.so.10.4.1"
},
{
"ref": "7e2a72b4c4b38c61e6962de6e3f4a5e9ae692e732c68deead10a7ce2135a7f68",
"local-path": "/usr/lib/x86_64-linux-gnu/libz.so.1.2.13"
},
{
"ref": "3159c3955f3aa3d599518107297a01b1548518c8e111a2dd32a6b1410d67a723",
"local-path": "/home/dalehamel/.rubies/ruby-3.4.7/lib/ruby/3.4.0/x86_64-linux/enc/encdb.so"
},
{
"ref": "ff54e20c46ea00625e912916143f1ab0973426d153d343a766d650a323fb6deb",
"local-path": "/home/dalehamel/.rubies/ruby-3.4.7/bin/ruby"
},
{
"ref": "0e9275bc9b81736220d63e9876de3050dfcae20e8b29beb46d3d54d1e4d8625b",
"local-path": "/home/dalehamel/.rubies/ruby-3.4.7/lib/ruby/3.4.0/x86_64-linux/monitor.so"
},
{
"ref": "bff8750fe719e6000791b88b11747dce8772c37118d0b2348044b70819d13835",
"local-path": "/usr/lib/x86_64-linux-gnu/libc.so.6"
},
{
"ref": "593bb1d5355658e645f36e6b1f49832691b24e177209765914e4cce51499dbb4",
"local-path": "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2"
}
]
}
Loading
Loading