diff --git a/asm/amd/insn.go b/asm/amd/insn.go index 784b1d931..0c0da32fc 100644 --- a/asm/amd/insn.go +++ b/asm/amd/insn.go @@ -2,7 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 package amd // import "go.opentelemetry.io/ebpf-profiler/asm/amd" -import "bytes" +import ( + "bytes" + + "go.opentelemetry.io/ebpf-profiler/libpf" + "golang.org/x/arch/x86/x86asm" +) // https://www.felixcloutier.com/x86/endbr64 var opcodeEndBr64 = []byte{0xf3, 0x0f, 0x1e, 0xfa} @@ -18,3 +23,67 @@ func DecodeSkippable(code []byte) (ok bool, size int) { return false, 0 } } + +// FindExternalJump decodes every instruction in the sym function and searches for +// a relative jump outside itself - to an address not covered by the sym. +// FindExternalJump returns the destination address of the relative jump outside the function or 0. +func FindExternalJump(code []byte, f *libpf.Symbol) (libpf.Address, error) { + var ( + err error + inst x86asm.Inst + rip = int64(f.Address) + ) + for len(code) > 0 { + if ok, l := DecodeSkippable(code); ok { + inst = x86asm.Inst{Op: x86asm.NOP, Len: l} + } else { + inst, err = x86asm.Decode(code, 64) + if err != nil { + return 0, err + } + } + rip += int64(inst.Len) + code = code[inst.Len:] + if !isJump(inst.Op) { + continue + } + if rel, ok := inst.Args[0].(x86asm.Rel); !ok { + continue + } else { + dst := rip + int64(rel) + if dst >= int64(f.Address) && dst < int64(f.Address)+int64(f.Size) { + continue + } + return libpf.Address(dst), nil + } + } + return 0, nil +} + +func isJump(op x86asm.Op) bool { + switch op { + case x86asm.JA, + x86asm.JAE, + x86asm.JB, + x86asm.JBE, + x86asm.JCXZ, + x86asm.JE, + x86asm.JECXZ, + x86asm.JG, + x86asm.JGE, + x86asm.JL, + x86asm.JLE, + x86asm.JMP, + x86asm.JNE, + x86asm.JNO, + x86asm.JNP, + x86asm.JNS, + x86asm.JO, + x86asm.JP, + x86asm.JRCXZ, + x86asm.JS: + return true + default: + return false + } +} diff --git a/interpreter/php/opcache.go b/interpreter/php/opcache.go index a7de6bf34..6c9617af5 100644 --- a/interpreter/php/opcache.go +++ b/interpreter/php/opcache.go @@ -313,12 +313,12 @@ func getOpcacheJITInfo(ef *pfelf.File) (dasmBuf, dasmSize libpf.Address, err err // and ARM64. // We should only need 64 bytes, since this should be early in the instruction sequence. - addr, code, err := ef.SymbolData("zend_jit_unprotect", 64) + sym, code, err := ef.SymbolData("zend_jit_unprotect", 64) if err != nil { return 0, 0, fmt.Errorf("unable to read 'zend_jit_unprotect': %w", err) } - dasmBufPtr, dasmSizePtr, err := retrieveJITBufferPtrWrapper(code, addr) + dasmBufPtr, dasmSizePtr, err := retrieveJITBufferPtrWrapper(code, sym.Address) if err != nil { return 0, 0, fmt.Errorf("failed to extract DASM pointers: %w", err) } diff --git a/interpreter/php/php.go b/interpreter/php/php.go index ddb9dcccd..45d3bc9c3 100644 --- a/interpreter/php/php.go +++ b/interpreter/php/php.go @@ -205,13 +205,13 @@ func recoverExecuteExJumpLabelAddress(ef *pfelf.File) (libpf.SymbolValue, error) // The address we care about varies from being 47 bytes in to about 107 bytes in, // so we'll cap at 128 bytes. This might need to be adjusted up in future. - addr, code, err := ef.SymbolData("execute_ex", 128) + sym, code, err := ef.SymbolData("execute_ex", 128) if err != nil { return libpf.SymbolValueInvalid, fmt.Errorf("unable to read 'execute_ex': %w", err) } - returnAddress, err := retrieveExecuteExJumpLabelAddressWrapper(code, addr) + returnAddress, err := retrieveExecuteExJumpLabelAddressWrapper(code, sym.Address) if err != nil { return libpf.SymbolValueInvalid, fmt.Errorf("reading the return address from execute_ex failed (%w)", diff --git a/interpreter/python/python.go b/interpreter/python/python.go index 65f9ceadd..0d1c491ce 100644 --- a/interpreter/python/python.go +++ b/interpreter/python/python.go @@ -19,6 +19,8 @@ import ( "unsafe" log "github.com/sirupsen/logrus" + "go.opentelemetry.io/ebpf-profiler/asm/amd" + "go.opentelemetry.io/ebpf-profiler/nativeunwind/elfunwindinfo" "github.com/elastic/go-freelru" @@ -659,17 +661,17 @@ func (d *pythonData) readIntrospectionData(ef *pfelf.File, symbol libpf.SymbolNa func decodeStub(ef *pfelf.File, memoryBase libpf.SymbolValue, symbolName libpf.SymbolName) (libpf.SymbolValue, error) { // Read and decode the code for the symbol - addr, code, err := ef.SymbolData(symbolName, 64) + sym, code, err := ef.SymbolData(symbolName, 64) if err != nil { return libpf.SymbolValueInvalid, fmt.Errorf("unable to read '%s': %v", symbolName, err) } - value, err := decodeStubArgumentWrapper(code, addr, memoryBase) + value, err := decodeStubArgumentWrapper(code, sym.Address, memoryBase) // Sanity check the value range and alignment if err != nil || value%4 != 0 { return libpf.SymbolValueInvalid, fmt.Errorf("decode stub %s 0x%x %s failed (0x%x): %v", - symbolName, addr, hex.Dump(code), value, err) + symbolName, sym.Address, hex.Dump(code), value, err) } // If base symbol (_PyRuntime) is not provided, accept any found value. if memoryBase == 0 && value != 0 { @@ -680,7 +682,7 @@ func decodeStub(ef *pfelf.File, memoryBase libpf.SymbolValue, return value, nil } return libpf.SymbolValueInvalid, fmt.Errorf("decode stub %s 0x%x %s failed (0x%x)", - symbolName, addr, hex.Dump(code), value) + symbolName, sym.Address, hex.Dump(code), value) } func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) { @@ -752,7 +754,7 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr autoTLSKey += 4 } - interpRanges, err := findInterpreterRanges(info) + interpRanges, err := findInterpreterRanges(info, ef) if err != nil { return nil, err } @@ -830,7 +832,8 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr return pd, nil } -func findInterpreterRanges(info *interpreter.LoaderInfo) (interpRanges []util.Range, err error) { +func findInterpreterRanges(info *interpreter.LoaderInfo, ef *pfelf.File, +) (interpRanges []util.Range, err error) { // The Python main interpreter loop history in CPython git is: // //nolint:lll @@ -839,14 +842,54 @@ func findInterpreterRanges(info *interpreter.LoaderInfo) (interpRanges []util.Ra // 0b72b23fb0c v3.9 2020-03-12 _PyEval_EvalFrameDefault(PyThreadState*,PyFrameObject*,int) // 3cebf938727 v3.6 2016-09-05 _PyEval_EvalFrameDefault(PyFrameObject*,int) // 49fd7fa4431 v3.0 2006-04-21 PyEval_EvalFrameEx(PyFrameObject*,int) - if interpRanges, err = info.GetSymbolAsRanges("_PyEval_EvalFrameDefault"); err != nil { - interpRanges, _ = info.GetSymbolAsRanges("PyEval_EvalFrameEx") + var interp *libpf.Symbol + var code []byte + const maxCodeSize = 128 * 1024 // observed ~65k in the wild + if interp, code, err = ef.SymbolData("_PyEval_EvalFrameDefault", maxCodeSize); err != nil { + interp, code, err = ef.SymbolData("PyEval_EvalFrameEx", maxCodeSize) } - if len(interpRanges) == 0 { + if err != nil { return nil, errors.New("no _PyEval_EvalFrameDefault/PyEval_EvalFrameEx symbol found") } - // TODO(korniltsev): find cold ranges - // see tools/coredump/testdata/amd64/python312-alpine320-nobuildid.json - // https://github.com/open-telemetry/opentelemetry-ebpf-profiler/issues/416 + interpRanges = make([]util.Range, 0, 2) + interpRanges = append(interpRanges, util.Range{ + Start: uint64(interp.Address), + End: uint64(interp.Address) + interp.Size, + }) + coldRange, err := findColdRange(ef, code, interp) + if err != nil { + log.WithError(err).Warnf("failed to recover python ranges %s", + info.FileName()) + } + if coldRange != (util.Range{}) { + interpRanges = append(interpRanges, coldRange) + } return interpRanges, nil } + +// findColdRange finds a relative jump from the _PyEval_EvalFrameDefault outside itself +// (to _PyEval_EvalFrameDefault.cold symbol) and then recovers the range of the .cold +// symbol using an instance of elfunwindinfo.EhFrameTable. +// findColdRange returns the util.Range of the `.cold` symbol or an empty util.Range +// https://github.com/open-telemetry/opentelemetry-ebpf-profiler/issues/416 +func findColdRange(ef *pfelf.File, code []byte, interp *libpf.Symbol) (util.Range, error) { + if ef.Machine != elf.EM_X86_64 { + return util.Range{}, nil + } + dst, err := amd.FindExternalJump(code, interp) + if err != nil || dst == 0 { + return util.Range{}, err + } + t, err := elfunwindinfo.NewEhFrameTable(ef) + if err != nil { + return util.Range{}, err + } + fde, err := t.LookupFDE(dst) + if err != nil { + return util.Range{}, err + } + return util.Range{ + Start: uint64(fde.PCBegin), + End: uint64(fde.PCBegin + fde.PCRange), + }, nil +} diff --git a/libpf/pfelf/file.go b/libpf/pfelf/file.go index fb39b3682..b75c60429 100644 --- a/libpf/pfelf/file.go +++ b/libpf/pfelf/file.go @@ -488,10 +488,10 @@ func (f *File) VirtualMemory(addr int64, sz, maxSize int) ([]byte, error) { // SymbolData returns the data associated with given dynamic symbol. // The backing mmapped data is returned if possible, otherwise a maximum of // maxCopy bytes of the symbol data will read to newly allocated buffer. -func (f *File) SymbolData(name libpf.SymbolName, maxCopy int) (libpf.SymbolValue, []byte, error) { +func (f *File) SymbolData(name libpf.SymbolName, maxCopy int) (*libpf.Symbol, []byte, error) { sym, err := f.LookupSymbol(name) if err != nil { - return 0, nil, err + return nil, nil, err } symSize := int(sym.Size) if symSize > maxCopy { @@ -501,7 +501,7 @@ func (f *File) SymbolData(name libpf.SymbolName, maxCopy int) (libpf.SymbolValue } } data, err := f.VirtualMemory(int64(sym.Address), symSize, maxCopy) - return sym.Address, data, err + return sym, data, err } // ReadVirtualMemory reads bytes from given virtual address diff --git a/tools/coredump/testdata/amd64/python312-alpine320-nobuildid.json b/tools/coredump/testdata/amd64/python312-alpine320-nobuildid.json index 69047ff02..b8aecc8f5 100644 --- a/tools/coredump/testdata/amd64/python312-alpine320-nobuildid.json +++ b/tools/coredump/testdata/amd64/python312-alpine320-nobuildid.json @@ -1,5 +1,4 @@ { - "skip": "https://github.com/open-telemetry/opentelemetry-ebpf-profiler/issues/416", "coredump-ref": "3eb6bae4e0089983f436d6bbd4a0b7ee0d72738eac29f15495494f53bc82263d", "threads": [ { diff --git a/tools/coredump/testdata/amd64/python312-alpine320.json b/tools/coredump/testdata/amd64/python312-alpine320.json index 564b30b10..61e00b91a 100644 --- a/tools/coredump/testdata/amd64/python312-alpine320.json +++ b/tools/coredump/testdata/amd64/python312-alpine320.json @@ -1,5 +1,4 @@ { - "skip": "https://github.com/open-telemetry/opentelemetry-ebpf-profiler/issues/416", "coredump-ref": "4652115623df00987a1a480431a360edbd67b0795e9529543d40be190e37c74d", "threads": [ {