Skip to content

Commit 787e225

Browse files
committed
Read EC offset from disassembly for static ruby
1 parent 5a10187 commit 787e225

8 files changed

Lines changed: 263 additions & 7 deletions

File tree

interpreter/ruby/ec.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package ruby // import "go.opentelemetry.io/ebpf-profiler/interpreter/ruby"
2+
3+
import (
4+
"debug/elf"
5+
"fmt"
6+
7+
"go.opentelemetry.io/ebpf-profiler/internal/log"
8+
"go.opentelemetry.io/ebpf-profiler/libpf"
9+
"go.opentelemetry.io/ebpf-profiler/libpf/pfelf"
10+
)
11+
12+
// extractEcOffset extracts the ec offset from disassembly
13+
func extractEcOffset(ef *pfelf.File) (int64, error) {
14+
symbolName := libpf.SymbolName("rb_current_ec_noinline")
15+
_, code, err := ef.SymbolData(symbolName, 2048)
16+
if err != nil {
17+
found := false
18+
if err = ef.VisitSymbols(func(s libpf.Symbol) bool {
19+
if s.Name == symbolName {
20+
data, err := ef.VirtualMemory(int64(s.Address), int(s.Size), 2048)
21+
if err != nil {
22+
log.Errorf("Failed to read memory for %s, %v", symbolName, err)
23+
} else {
24+
code = data
25+
found = true
26+
}
27+
return false
28+
}
29+
return true
30+
}); err != nil {
31+
log.Warnf("failed to visit symbols: %v", err)
32+
}
33+
34+
if !found {
35+
return 0, fmt.Errorf("unable to read 'rb_current_ec_noinline': %s", err)
36+
}
37+
}
38+
if len(code) < 8 {
39+
return 0, fmt.Errorf("rb_current_ec_noinline function size is %d", len(code))
40+
}
41+
var offset int64
42+
switch ef.Machine {
43+
case elf.EM_X86_64:
44+
offset, err = extractRubyECOffsetX86(code)
45+
case elf.EM_AARCH64:
46+
offset, err = extractRubyECOffsetARM(code)
47+
default:
48+
return 0, fmt.Errorf("unsupported arch %s", ef.Machine.String())
49+
}
50+
return offset, nil
51+
}

interpreter/ruby/ec_aarch64.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package ruby // import "go.opentelemetry.io/ebpf-profiler/interpreter/ruby"
2+
3+
import (
4+
"errors"
5+
6+
ah "go.opentelemetry.io/ebpf-profiler/armhelpers"
7+
aa "golang.org/x/arch/arm64/arm64asm"
8+
)
9+
10+
// extractRubyECOffsetARM extracts the Ruby EC offset from aarch64 assembly
11+
func extractRubyECOffsetARM(code []byte) (int64, error) {
12+
const (
13+
Unspec int = iota
14+
ECBase
15+
)
16+
17+
type regState struct {
18+
status int
19+
offset int64
20+
}
21+
22+
// Track all registers
23+
var regs [32]regState
24+
25+
for offs := 0; offs < len(code); offs += 4 {
26+
inst, err := aa.Decode(code[offs:])
27+
if err != nil {
28+
continue
29+
}
30+
if inst.Op == aa.RET {
31+
break
32+
}
33+
34+
destReg, ok := ah.Xreg2num(inst.Args[0])
35+
if !ok {
36+
continue
37+
}
38+
39+
switch inst.Op {
40+
case aa.MRS:
41+
// MRS X0, tpidr_el0 (S3_3_C13_C0_2)
42+
if inst.Args[1].String() == "S3_3_C13_C0_2" {
43+
regs[destReg] = regState{
44+
status: ECBase,
45+
offset: 0,
46+
}
47+
}
48+
case aa.ADD:
49+
srcReg, ok := ah.Xreg2num(inst.Args[1])
50+
if !ok {
51+
continue
52+
}
53+
if regs[srcReg].status == ECBase {
54+
i, ok := ah.DecodeImmediate(inst.Args[2])
55+
if !ok {
56+
continue
57+
}
58+
regs[destReg] = regState{
59+
status: ECBase,
60+
offset: regs[srcReg].offset + i,
61+
}
62+
}
63+
case aa.LDR:
64+
// LDR doesn't change the offset we're calculating
65+
continue
66+
}
67+
}
68+
69+
// The offset should be in X0 (return register)
70+
if regs[0].status != ECBase {
71+
return 0, errors.New("could not extract Ruby EC offset from ARM code")
72+
}
73+
74+
return regs[0].offset, nil
75+
}

interpreter/ruby/ec_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package ruby
2+
3+
import (
4+
"debug/elf"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestExtractEcOffset(t *testing.T) {
11+
testCases := map[string]struct {
12+
machine elf.Machine
13+
code []byte
14+
offset int64
15+
}{
16+
"ruby 4.0 static / x86_64": {
17+
machine: elf.EM_X86_64,
18+
code: []byte{
19+
// mov %fs:0xffffffffffffff88,%rax
20+
// ret
21+
0x64, 0x48, 0x8b, 0x04, 0x25, 0x88, 0xff, 0xff, 0xff,
22+
0xc3,
23+
},
24+
offset: -120,
25+
},
26+
"ruby 3.4.7 static / x86_64": {
27+
//machine: elf.EM_AARCH64,
28+
machine: elf.EM_X86_64,
29+
code: []byte{
30+
// mov %fs:0xfffffffffffffff8,%rax
31+
// ret
32+
0x64, 0x48, 0x8b, 0x04, 0x25, 0xf8, 0xff, 0xff, 0xff,
33+
0xc3,
34+
},
35+
offset: -8,
36+
},
37+
"ruby 3.4.7 static / aarch64": {
38+
machine: elf.EM_AARCH64,
39+
code: []byte{
40+
0x40, 0xd0, 0x3b, 0xd5, // mrs x0, tpidr_el0
41+
0x00, 0x00, 0x40, 0x91, // add x0, x0, #0x0, lsl #12
42+
0x00, 0xe0, 0x00, 0x91, // add x0, x0, #0x38
43+
0x00, 0x00, 0x40, 0xf9, // ldr x0, [x0]
44+
0xc0, 0x03, 0x5f, 0xd6, // ret
45+
},
46+
offset: 56,
47+
},
48+
}
49+
50+
for name, test := range testCases {
51+
t.Run(name, func(t *testing.T) {
52+
var offset int64
53+
var err error
54+
switch test.machine {
55+
case elf.EM_X86_64:
56+
offset, err = extractRubyECOffsetX86(test.code)
57+
case elf.EM_AARCH64:
58+
offset, err = extractRubyECOffsetARM(test.code)
59+
}
60+
if assert.NoError(t, err) {
61+
assert.Equal(t, test.offset, offset, "Wrong ruby EC offset extraction")
62+
}
63+
})
64+
}
65+
}

interpreter/ruby/ec_x86_64.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package ruby // import "go.opentelemetry.io/ebpf-profiler/interpreter/ruby"
2+
3+
import (
4+
"errors"
5+
//"fmt"
6+
7+
"go.opentelemetry.io/ebpf-profiler/asm/amd"
8+
e "go.opentelemetry.io/ebpf-profiler/asm/expression"
9+
"golang.org/x/arch/x86/x86asm"
10+
)
11+
12+
// extractRubyECOffsetX86 extracts the Ruby EC (execution context) offset from x86_64 assembly
13+
func extractRubyECOffsetX86(code []byte) (int64, error) {
14+
it := amd.NewInterpreterWithCode(code)
15+
_, err := it.LoopWithBreak(func(op x86asm.Inst) bool {
16+
return op.Op == x86asm.RET
17+
})
18+
if err != nil {
19+
return 0, err
20+
}
21+
res := it.Regs.Get(amd.RAX)
22+
23+
offset := e.NewImmediateCapture("offset")
24+
25+
// Match: mov %fs:offset,%rax
26+
// The result should be a memory read from FS segment
27+
expected := e.Mem8(
28+
e.Add(
29+
e.MemWithSegment8(x86asm.FS, e.Imm(0)),
30+
offset,
31+
),
32+
)
33+
34+
if res.Match(expected) {
35+
return int64(int32(offset.CapturedValue())), nil
36+
}
37+
38+
// Try direct segment access pattern
39+
expected = e.MemWithSegment8(x86asm.FS, offset)
40+
if res.Match(expected) {
41+
return int64(int32(offset.CapturedValue())), nil
42+
}
43+
44+
return 0, errors.New("could not extract Ruby EC offset from x86_64 code")
45+
}

interpreter/ruby/ruby.go

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ const (
8686

8787
var (
8888
// regex to identify the Ruby interpreter executable
89-
rubyRegex = regexp.MustCompile(`^(?:.*/)?libruby(?:-.*)?\.so\.(\d)\.(\d)\.(\d)$`)
89+
libRubyRegex = regexp.MustCompile(`^(?:.*/)?libruby(?:-.*)?\.so\.(\d)\.(\d)\.(\d)$`)
90+
binRubyRegex = regexp.MustCompile(`^(?:.*/)?(?:bin/)?ruby$`)
9091
// regex to extract a version from a string
9192
rubyVersionRegex = regexp.MustCompile(`^(\d)\.(\d)\.(\d)$`)
9293

@@ -114,6 +115,9 @@ type rubyData struct {
114115
// Address to the ruby_current_ec variable in TLS, as an offset from tpbase
115116
currentEcTpBaseTlsOffset libpf.Address
116117

118+
// In local exec / static mode, the EC is an absolute offset from tpbase
119+
absoluteTpBaseOffset int64
120+
117121
// Address to global symbols, for id to string mappings
118122
globalSymbolsAddr libpf.Address
119123

@@ -291,10 +295,14 @@ func (r *rubyData) String() string {
291295
func (r *rubyData) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, bias libpf.Address,
292296
rm remotememory.RemoteMemory,
293297
) (interpreter.Instance, error) {
294-
var tlsOffset uint64
295-
if r.currentEcTpBaseTlsOffset != 0 {
298+
var tlsOffset int64
299+
if r.absoluteTpBaseOffset == 0 && r.currentEcTpBaseTlsOffset != 0 {
296300
// Read TLS offset from the TLS descriptor.
297-
tlsOffset = rm.Uint64(bias + r.currentEcTpBaseTlsOffset + 8)
301+
tlsOffset = int64(rm.Uint64(bias + r.currentEcTpBaseTlsOffset + 8))
302+
}
303+
304+
if r.absoluteTpBaseOffset != 0 {
305+
tlsOffset = r.absoluteTpBaseOffset
298306
}
299307

300308
var modId uint64
@@ -1371,7 +1379,8 @@ func determineRubyVersion(ef *pfelf.File) (uint32, error) {
13711379
}
13721380

13731381
func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) {
1374-
if !rubyRegex.MatchString(info.FileName()) {
1382+
var isBinRuby = binRubyRegex.MatchString(info.FileName())
1383+
if !libRubyRegex.MatchString(info.FileName()) && !isBinRuby {
13751384
return nil, nil
13761385
}
13771386

@@ -1414,6 +1423,7 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr
14141423

14151424
var globalSymbols libpf.SymbolValue
14161425
var currentEcTpBaseTlsOffset libpf.Address
1426+
var absoluteTpBaseOffset int64
14171427
var interpRanges []util.Range
14181428

14191429
globalSymbolsName := libpf.SymbolName("ruby_global_symbols")
@@ -1512,12 +1522,21 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr
15121522
log.Warnf("failed to mod offset: %v", err)
15131523
}
15141524

1525+
if isBinRuby {
1526+
offset, err := extractEcOffset(ef)
1527+
if err != nil {
1528+
return nil, fmt.Errorf("unable to extract ec offset for static ruby %v", err)
1529+
}
1530+
absoluteTpBaseOffset = offset
1531+
}
1532+
15151533
log.Debugf("Discovered EC tls tpbase offset %x, fallback ctx %x, interp ranges: %v, global symbols: %x", currentEcTpBaseTlsOffset, currentCtxPtr, interpRanges, globalSymbols)
15161534

15171535
rid := &rubyData{
15181536
version: version,
15191537
currentEcTpBaseTlsOffset: libpf.Address(currentEcTpBaseTlsOffset),
15201538
tlsModuleIdOffset: tlsModuleIdOffset,
1539+
absoluteTpBaseOffset: absoluteTpBaseOffset,
15211540
currentEcTlsOffset: libpf.Address(currentEcSymbolAddress),
15221541
currentCtxPtr: libpf.Address(currentCtxPtr),
15231542
hasGlobalSymbols: globalSymbols != 0,

support/ebpf/ruby_tracer.ebpf.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,7 @@ static EBPF_INLINE int unwind_ruby(struct pt_regs *ctx)
573573

574574
u64 tls_current_ec_addr = tsd_base + rubyinfo->current_ec_tpbase_tls_offset;
575575

576+
DEBUG_PRINT("ruby: reading EC from TLS %lld", rubyinfo->current_ec_tpbase_tls_offset);
576577
if (bpf_probe_read_user(
577578
&current_ctx_addr, sizeof(current_ctx_addr), (void *)(tls_current_ec_addr))) {
578579
goto exit;

support/ebpf/types.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ typedef struct RubyProcInfo {
484484
u32 version;
485485

486486
// tls_offset holds TLS base + ruby_current_ec tls symbol, as an offset from tpbase
487-
u64 current_ec_tpbase_tls_offset;
487+
s64 current_ec_tpbase_tls_offset;
488488

489489
// current_ec_tls_offset is the offset of the current EC within the TLS
490490
u64 current_ec_tls_offset;

support/types.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)