Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
82 changes: 67 additions & 15 deletions interpreter/golabels/tls_amd64.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,43 +6,95 @@
package golabels // import "go.opentelemetry.io/ebpf-profiler/interpreter/golabels"

import (
log "github.com/sirupsen/logrus"
"errors"

"go.opentelemetry.io/ebpf-profiler/libpf/pfelf"
"go.opentelemetry.io/ebpf-profiler/nativeunwind/elfunwindinfo"
"golang.org/x/arch/x86/x86asm"
)

var (
errMissingSymbol = errors.New("failed to find runtime.stackcheck")
errUnexpectedAsm = errors.New("failed to disassemble runtime.stackcheck")
)

// virtualMemoryReader allows to mock pfelf.File for testing.
type virtualMemoryReader interface {
VirtualMemory(addr int64, size int, align int) ([]byte, error)
}

// Most normal amd64 Go binaries use -8 as offset into TLS space for
// storing the current g but "static" binaries it ends up as -80. There
// may be dynamic relocating going on so just read it from a known
// symbol if possible.
func extractTLSGOffset(f *pfelf.File) (int32, error) {
syms, err := f.ReadSymbols()
pclntab, err := elfunwindinfo.NewGopclntab(f)
if err != nil {
return 0, err
}
defer pclntab.Close()

// Dump of assembler code for function runtime.stackcheck:
// 0x0000000000470080 <+0>: mov %fs:0xfffffffffffffff8,%rax
sym, err := syms.LookupSymbol("runtime.stackcheck.abi0")
if err != nil {
// Binary must be stripped, hope default is correct and warn.
log.Warnf("Failed to find stackcheck symbol, Go labels might not work: %v", err)
return -8, nil
// Case 1 - direct offset
// mov %fs:0xfffffffffffffff8,%rax
// Case 2 - indirect offset
// mov 0x17d0c5a1(%rip),%rcx
// mov %fs:(%rcx),%rax
pc, ok := pclntab.PCForSymbol("runtime.stackcheck")
if !ok {
return 0, errMissingSymbol
}
b, err := f.VirtualMemory(int64(sym.Address), 10, 10)
// Read enough bytes for two instructions
b, err := f.VirtualMemory(int64(pc), 16, 16)
if err != nil {
return 0, err
}
return extractOffsetFromBytes(f, pc, b)
}

i, err := x86asm.Decode(b, 64)
func extractOffsetFromBytes(f virtualMemoryReader, pc uintptr, b []byte) (int32, error) {
Comment thread
florianl marked this conversation as resolved.
Outdated
i1, err := x86asm.Decode(b, 64)
if err != nil {
return 0, err
}
if i.Op == x86asm.MOV {
mem, ok := i.Args[1].(x86asm.Mem)
if ok {

// Case 1: mov %fs:0xfffffffffffffff8,%rax
if i1.Op == x86asm.MOV {
mem, ok := i1.Args[1].(x86asm.Mem)
reg, okReg := i1.Args[0].(x86asm.Reg)
if ok && okReg && mem.Segment == x86asm.FS && reg == x86asm.RAX {
return int32(mem.Disp), nil
}
}
log.Warnf("Failed to decode stackcheck symbol, Go label collection might not work")
return -8, nil

i2, err := x86asm.Decode(b[i1.Len:], 64)
if err != nil {
return 0, err
}

// Case 2: mov 0x17d0c5a1(%rip),%rcx; mov %fs:(%rcx),%rax
if i1.Op == x86asm.MOV && i2.Op == x86asm.MOV {
mem1, ok1 := i1.Args[1].(x86asm.Mem)
reg1, okReg1 := i1.Args[0].(x86asm.Reg)
mem2, ok2 := i2.Args[1].(x86asm.Mem)
reg2, okReg2 := i2.Args[0].(x86asm.Reg)
reg2base := mem2.Base
// Check for the indirect pattern
if ok1 && okReg1 && ok2 && okReg2 && reg1 == x86asm.RCX && mem2.Segment == x86asm.FS &&
reg2 == x86asm.RAX && reg2base == x86asm.RCX {
// Resolve the address loaded by the first instruction (RIP-relative)
addr := int64(pc) + int64(i1.Len) + mem1.Disp
offsetBytes, err := f.VirtualMemory(addr, 4, 4)
if err != nil {
return 0, errUnexpectedAsm
}
offset := int32(offsetBytes[0]) |
int32(offsetBytes[1])<<8 |
int32(offsetBytes[2])<<16 |
int32(offsetBytes[3])<<24
return offset, nil
}
}

return 0, errUnexpectedAsm
}
73 changes: 73 additions & 0 deletions interpreter/golabels/tls_amd64_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//go:build amd64

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package golabels

import (
"errors"
"testing"

"github.com/stretchr/testify/require"
)

// mockPfelfFile implements minimal pfelf.File for testing
// Only VirtualMemory is used in extractOffsetFromBytes

type mockPfelfFile struct {
mem map[int64][]byte
}

//nolint:gocritic
func (m *mockPfelfFile) VirtualMemory(addr int64, _ int, _ int) ([]byte, error) {
if b, ok := m.mem[addr]; ok {
return b, nil
}
return nil, errors.New("not found")
}

func TestExtractOffsetFromBytes(t *testing.T) {
tests := map[string]struct {
mockFileData map[int64][]byte
pc uintptr
ins []byte
expectedOffset int32
err error
}{
"UnexpectedASM": {
mockFileData: make(map[int64][]byte),
pc: 0x1000,
ins: []byte{0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90},
err: errUnexpectedAsm,
},
"Direct": {
mockFileData: make(map[int64][]byte),
pc: 0x1000,
ins: []byte{0x64, 0x48, 0x8b, 0x04, 0x25, 0xf8,
0xff, 0xff, 0xff},
expectedOffset: -8,
},
"Indirect": {
mockFileData: map[int64][]byte{
0x1000 + 7 + 0x10: {0xf8, 0xff, 0xff, 0xff},
},
pc: 0x1000,
ins: []byte{0x48, 0x8b, 0x0d, 0x10, 0x00,
0x00, 0x00, 0x64, 0x48, 0x8b, 0x01},
expectedOffset: -8,
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
f := &mockPfelfFile{
mem: tc.mockFileData,
}
offset, err := extractOffsetFromBytes(f, tc.pc, tc.ins)
require.Equal(t, tc.err, err)
require.Equal(t, tc.expectedOffset, offset)
})
}
}
16 changes: 16 additions & 0 deletions nativeunwind/elfunwindinfo/elfgopclntab.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,22 @@ type Gopclntab struct {
functab, funcdata, funcnametab, filetab, pctab, cutab []byte
}

// PCForSymbol returns the start PC address for the given symbol name.
func (g *Gopclntab) PCForSymbol(symbol string) (uintptr, bool) {
Comment thread
florianl marked this conversation as resolved.
Outdated
for i := 0; i < g.numFuncs; i++ {
_, funcOff := g.getFuncMapEntry(i)
pc, fun := g.getFunc(funcOff)
if fun == nil {
continue
}
name := getString(g.funcnametab, int(fun.nameOff))
if name == symbol {
return pc, true
}
}
return 0, false
}

// NewGopclntab parses and returns the parsed data for further operations.
func NewGopclntab(ef *pfelf.File) (*Gopclntab, error) {
data, err := extractGoPclntab(ef)
Expand Down
Loading