diff --git a/nativeunwind/elfunwindinfo/elfehframe.go b/nativeunwind/elfunwindinfo/elfehframe.go index b9b37c0ce..5c9b70f05 100644 --- a/nativeunwind/elfunwindinfo/elfehframe.go +++ b/nativeunwind/elfunwindinfo/elfehframe.go @@ -366,7 +366,6 @@ type cieInfo struct { // fdeInfo contains one Frame Description Entry (FDE) type fdeInfo struct { - len uint64 ciePos uint64 ipLen uintptr ipStart uintptr @@ -890,22 +889,18 @@ func isSignalTrampoline(efCode *pfelf.File, fde *fdeInfo) bool { return bytes.Equal(fdeCode, sigretCode) } -// parseFDE reads and processes one Frame Description Entry and returns the size of -// the CIE/FDE entry, and amends the intervals to deltas table. -// The FDE format is described in: -// http://dwarfstd.org/doc/DWARF5.pdf §6.4.1 -// https://refspecs.linuxfoundation.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/ehframechpt.html -func (ee *elfExtractor) parseFDE(r *reader, ef *pfelf.File, ipStart uintptr, - cieCache *lru.LRU[uint64, *cieInfo], sorted bool) (size uintptr, err error) { +// parses first fields of FDE, specifically PC Begin, PC Range +func parsesFDEHeader(r *reader, efm elf.Machine, ipStart uintptr, + cieCache *lru.LRU[uint64, *cieInfo]) (fdeLen uint64, fde fdeInfo, info *cieInfo, err error) { // Parse FDE header fdeID := r.pos - fde := fdeInfo{sorted: sorted} - fde.len, fde.ciePos, err = r.parseHDR(false) + fde = fdeInfo{} + fdeLen, fde.ciePos, err = r.parseHDR(false) if err != nil { // parseHDR returns unconditionally the CIE/FDE entry length. // Also return the size here. This is to allow walkFDEs to use // this function and skip CIEs. - return uintptr(fde.len), err + return fdeLen, fde, nil, err } // Calculate CIE location, and get and cache the CIE data @@ -916,58 +911,76 @@ func (ee *elfExtractor) parseFDE(r *reader, ef *pfelf.File, ipStart uintptr, cie = &cieInfo{} if err = cr.parseCIE(cie); err != nil { - return 0, fmt.Errorf("CIE %#x failed: %v", fde.ciePos, err) + return 0, fde, nil, fmt.Errorf("CIE %#x failed: %v", fde.ciePos, err) } // initialize vmRegs from initialState - these can be used by restore // opcode during initial CIE run - cie.initialState = newVMRegs(ef.Machine) + cie.initialState = newVMRegs(efm) // Run CIE initial opcodes st := state{ cie: cie, - cur: newVMRegs(ef.Machine), + cur: newVMRegs(efm), } if err = st.step(&cr); err != nil { - return 0, err + return 0, fde, nil, err } if !cr.isValid() { - return 0, fmt.Errorf("CIE %x parsing failed", fde.ciePos) + return 0, fde, nil, fmt.Errorf("CIE %x parsing failed", fde.ciePos) } cie.initialState = st.cur cieCache.Add(fde.ciePos, cie) } // Parse rest of FDE structure (CIE dependent part) - st := state{cie: cie, cur: cie.initialState} - fde.ipStart, err = r.ptr(st.cie.enc) + + fde.ipStart, err = r.ptr(cie.enc) if err != nil { - return 0, err + return 0, fde, nil, err } if ipStart != 0 && fde.ipStart != ipStart { - return 0, fmt.Errorf( + return 0, fde, nil, fmt.Errorf( "FDE ipStart (%x) not matching search table FDE ipStart (%x)", fde.ipStart, ipStart) } - if st.cie.enc&encIndirect != 0 { - fde.ipLen, err = r.ptr(st.cie.enc) + if cie.enc&encIndirect != 0 { + fde.ipLen, err = r.ptr(cie.enc) } else { - fde.ipLen, err = r.ptr(st.cie.enc & (encFormatMask | encSignedMask)) + fde.ipLen, err = r.ptr(cie.enc & (encFormatMask | encSignedMask)) } if err != nil { - return 0, err + return 0, fde, nil, err } - if st.cie.hasAugmentation { + if cie.hasAugmentation { r.pos += uintptr(r.uleb()) } if !r.isValid() { - return 0, fmt.Errorf("FDE %x not valid after header", fdeID) + return 0, fde, nil, fmt.Errorf("FDE %x not valid after header", fdeID) } + return fdeLen, fde, cie, nil +} + +// parseFDE reads and processes one Frame Description Entry and returns the size of +// the CIE/FDE entry, and amends the intervals to deltas table. +// The FDE format is described in: +// http://dwarfstd.org/doc/DWARF5.pdf §6.4.1 +// https://refspecs.linuxfoundation.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/ehframechpt.html +func (ee *elfExtractor) parseFDE(r *reader, ef *pfelf.File, ipStart uintptr, + cieCache *lru.LRU[uint64, *cieInfo], sorted bool) (size uintptr, err error) { + // Parse FDE header + fdeID := r.pos + fdeLen, fde, cie, err := parsesFDEHeader(r, ef.Machine, ipStart, cieCache) + if err != nil { + return uintptr(fdeLen), err + } + st := state{cie: cie, cur: cie.initialState} + fde.sorted = sorted // Process the FDE opcodes if !ee.hooks.fdeHook(st.cie, &fde) { - return uintptr(fde.len), nil + return uintptr(fdeLen), nil } st.loc = fde.ipStart @@ -1021,7 +1034,7 @@ func (ee *elfExtractor) parseFDE(r *reader, ef *pfelf.File, ipStart uintptr, Info: info, }, sorted) - return uintptr(fde.len), nil + return uintptr(fdeLen), nil } // elfRegion is a reference to a region within an ELF file. Such a region reference can be @@ -1159,51 +1172,26 @@ func findEhSections(ef *pfelf.File) ( // walkBinSearchTable parses FDEs by following all references in the binary search table in the // `.eh_frame_hdr` section. -func (ee *elfExtractor) walkBinSearchTable(parsedFile *pfelf.File, ehFrameHdrSec *elfRegion, +func (ee *elfExtractor) walkBinSearchTable(ef *pfelf.File, ehFrameHdrSec *elfRegion, ehFrameSec *elfRegion) error { - h := (*ehFrameHdr)(unsafe.Pointer(&ehFrameHdrSec.data[0])) - - // Skip header, which is immediately followed by the binary search table. The header was - // already previously validated in `validateEhFrameHdr`. - r := ehFrameHdrSec.reader(unsafe.Sizeof(*h), false) - - if _, err := r.ptr(h.ehFramePtrEnc); err != nil { - return err - } - fdeCount, err := r.ptr(h.fdeCountEnc) - if err != nil { - return err - } - - cieCache, err := lru.New[uint64, *cieInfo](cieCacheSize, hashUint64) + t, err := newEhFrameTableFromSections(ehFrameHdrSec, ehFrameSec, ef.Machine) if err != nil { return err } - - // Walk the IP search table and dump each FDE found via it - for f := uintptr(0); f < fdeCount; f++ { - ipStart, err := r.ptr(h.tableEnc) - if err != nil { - return err - } - - fdeAddr, err := r.ptr(h.tableEnc) - if err != nil { + for f := uintptr(0); f < t.fdeCount; f++ { + var ( + ipStart uintptr + fr reader + ) + ipStart, fr, entryErr := t.parseHdrEntry() + if entryErr != nil { return err } - - if fdeAddr < ehFrameSec.vaddr { - return fmt.Errorf("FDE %#x before section start %#x", - fdeAddr, ehFrameSec.vaddr) - } - - fr := ehFrameSec.reader(fdeAddr-ehFrameSec.vaddr, false) - _, err = ee.parseFDE(&fr, parsedFile, ipStart, cieCache, true) + _, err = ee.parseFDE(&fr, ef, ipStart, t.cieCache, true) if err != nil && !errors.Is(err, errEmptyEntry) { return fmt.Errorf("failed to parse FDE: %v", err) } } - return nil } diff --git a/nativeunwind/elfunwindinfo/elfehframe_test.go b/nativeunwind/elfunwindinfo/elfehframe_test.go index 32d21766c..e1b66f470 100644 --- a/nativeunwind/elfunwindinfo/elfehframe_test.go +++ b/nativeunwind/elfunwindinfo/elfehframe_test.go @@ -21,8 +21,8 @@ type ehtester struct { } func (e *ehtester) fdeHook(cie *cieInfo, fde *fdeInfo) bool { - e.t.Logf("FDE len %d, ciePos %x, ip %x...%x, ipLen %d (enc %x, cf %d, df %d, ra %d)", - fde.len, fde.ciePos, fde.ipStart, fde.ipStart+fde.ipLen, fde.ipLen, + e.t.Logf("FDE ciePos %x, ip %x...%x, ipLen %d (enc %x, cf %d, df %d, ra %d)", + fde.ciePos, fde.ipStart, fde.ipStart+fde.ipLen, fde.ipLen, cie.enc, cie.codeAlign, cie.dataAlign, cie.regRA) e.t.Logf(" LOC CFA rbp ra") return true diff --git a/nativeunwind/elfunwindinfo/elfehframetable.go b/nativeunwind/elfunwindinfo/elfehframetable.go new file mode 100644 index 000000000..8dc55e1d5 --- /dev/null +++ b/nativeunwind/elfunwindinfo/elfehframetable.go @@ -0,0 +1,144 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package elfunwindinfo // import "go.opentelemetry.io/ebpf-profiler/nativeunwind/elfunwindinfo" + +import ( + "debug/elf" + "errors" + "fmt" + "sort" + "unsafe" + + lru "github.com/elastic/go-freelru" + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" +) + +type FDE struct { + PCBegin uintptr + PCRange uintptr +} + +type EhFrameTable struct { + r reader + hdr *ehFrameHdr + fdeCount uintptr + tableStartPos uintptr + ehFrameSec *elfRegion + efm elf.Machine + cieCache *lru.LRU[uint64, *cieInfo] +} + +// NewEhFrameTable creates a new EhFrameTable from the given pfelf.File +// The returned EhFrameTable must not be used concurrently +func NewEhFrameTable(ef *pfelf.File) (*EhFrameTable, error) { + ehFrameHdrSec, ehFrameSec, err := findEhSections(ef) + if err != nil { + return nil, fmt.Errorf("failed to get EH sections: %w", err) + } + if ehFrameSec == nil { + return nil, errors.New(".eh_frame not found") + } + if ehFrameHdrSec == nil { + return nil, errors.New(".eh_frame_hdr not found") + } + return newEhFrameTableFromSections(ehFrameHdrSec, ehFrameSec, ef.Machine) +} + +// LookupFDE performs a binary search in .eh_frame_hdr for an FDE covering the given addr. +func (e *EhFrameTable) LookupFDE(addr libpf.Address) (FDE, error) { + idx := sort.Search(e.count(), func(idx int) bool { + e.position(idx) + ipStart, _, _ := e.parseHdrEntry() // ignoring error, check bounds later + return ipStart > uintptr(addr) + }) + if idx <= 0 { + return FDE{}, errors.New("FDE not found") + } + e.position(idx - 1) + ipStart, fr, entryErr := e.parseHdrEntry() + if entryErr != nil { + return FDE{}, entryErr + } + _, fde, _, err := parsesFDEHeader(&fr, e.efm, ipStart, e.cieCache) + if err != nil { + return FDE{}, err + } + if uintptr(addr) < fde.ipStart || uintptr(addr) >= fde.ipStart+fde.ipLen { + return FDE{}, errors.New("FDE not found") + } + + return FDE{ + PCBegin: fde.ipStart, + PCRange: fde.ipLen, + }, nil +} + +func newEhFrameTableFromSections(ehFrameHdrSec *elfRegion, + ehFrameSec *elfRegion, efm elf.Machine, +) (hp *EhFrameTable, err error) { + hp = &EhFrameTable{ + hdr: (*ehFrameHdr)(unsafe.Pointer(&ehFrameHdrSec.data[0])), + r: ehFrameHdrSec.reader(unsafe.Sizeof(ehFrameHdr{}), false), + } + if _, err = hp.r.ptr(hp.hdr.ehFramePtrEnc); err != nil { + return hp, err + } + if hp.fdeCount, err = hp.r.ptr(hp.hdr.fdeCountEnc); err != nil { + return hp, err + } + if hp.cieCache, err = lru.New[uint64, *cieInfo](cieCacheSize, hashUint64); err != nil { + return hp, err + } + hp.ehFrameSec = ehFrameSec + hp.tableStartPos = hp.r.pos + hp.efm = efm + return hp, nil +} + +// returns FDE count +func (e *EhFrameTable) count() int { + return int(e.fdeCount) +} + +// position adjusts the reader position to point at the table entry with idx index +func (e *EhFrameTable) position(idx int) { + tableEntrySize := formatLen(e.hdr.tableEnc) * 2 + e.r.pos = e.tableStartPos + uintptr(tableEntrySize*idx) +} + +// parseHdrEntry parsers an entry in the .eh_frame_hdr binary search table and the corresponding +// entry in the .eh_frame section +func (e *EhFrameTable) parseHdrEntry() (ipStart uintptr, fr reader, err error) { + ipStart, err = e.r.ptr(e.hdr.tableEnc) + if err != nil { + return 0, reader{}, err + } + var fdeAddr uintptr + fdeAddr, err = e.r.ptr(e.hdr.tableEnc) + if err != nil { + return 0, reader{}, err + } + if fdeAddr < e.ehFrameSec.vaddr { + return 0, reader{}, fmt.Errorf("FDE %#x before section start %#x", + fdeAddr, e.ehFrameSec.vaddr) + } + fr = e.ehFrameSec.reader(fdeAddr-e.ehFrameSec.vaddr, false) + + return ipStart, fr, err +} + +// formatLen returns the length of a field encoded with enc encoding. +func formatLen(enc encoding) int { + switch enc & encFormatMask { + case encFormatData2: + return 2 + case encFormatData4: + return 4 + case encFormatData8, encFormatNative: + return 8 + default: + return 0 + } +} diff --git a/nativeunwind/elfunwindinfo/elfehframetable_test.go b/nativeunwind/elfunwindinfo/elfehframetable_test.go new file mode 100644 index 000000000..7a093c465 --- /dev/null +++ b/nativeunwind/elfunwindinfo/elfehframetable_test.go @@ -0,0 +1,75 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package elfunwindinfo + +import ( + "bytes" + "encoding/base64" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" +) + +func TestLookupFDE(t *testing.T) { + checks := []struct { + at uintptr + expected FDE + }{ + {at: 0x0, expected: FDE{}}, + {at: 0x840, expected: FDE{}}, + {at: 0x850, expected: FDE{PCBegin: 0x850, PCRange: 0x10}}, + {at: 0x855, expected: FDE{PCBegin: 0x850, PCRange: 0x10}}, + {at: 0x859, expected: FDE{PCBegin: 0x850, PCRange: 0x10}}, + {at: 0x860, expected: FDE{PCBegin: 0x860, PCRange: 0x68}}, + {at: 0x865, expected: FDE{PCBegin: 0x860, PCRange: 0x68}}, + {at: 0x8c7, expected: FDE{PCBegin: 0x860, PCRange: 0x68}}, + {at: 0x8c8, expected: FDE{}}, + {at: 0x8c9, expected: FDE{}}, + {at: 0x8cf, expected: FDE{}}, + {at: 0x8d0, expected: FDE{PCBegin: 0x8d0, PCRange: 0x11f}}, + {at: 0x8d3, expected: FDE{PCBegin: 0x8d0, PCRange: 0x11f}}, + {at: 0x9ee, expected: FDE{PCBegin: 0x8d0, PCRange: 0x11f}}, + {at: 0x9ef, expected: FDE{}}, + {at: 0x9f0, expected: FDE{PCBegin: 0x9f0, PCRange: 0x2b}}, + {at: 0x9f1, expected: FDE{PCBegin: 0x9f0, PCRange: 0x2b}}, + {at: 0xa1a, expected: FDE{PCBegin: 0x9f0, PCRange: 0x2b}}, + {at: 0xa1b, expected: FDE{}}, + {at: 0xa1c, expected: FDE{}}, + {at: 0xb1f, expected: FDE{}}, + {at: 0xb20, expected: FDE{PCBegin: 0xb20, PCRange: 0x65}}, + {at: 0xb32, expected: FDE{PCBegin: 0xb20, PCRange: 0x65}}, + {at: 0xb84, expected: FDE{PCBegin: 0xb20, PCRange: 0x65}}, + {at: 0xb85, expected: FDE{}}, + {at: 0xb90, expected: FDE{PCBegin: 0xb90, PCRange: 0x2}}, + {at: 0xb91, expected: FDE{PCBegin: 0xb90, PCRange: 0x2}}, + {at: 0xb92, expected: FDE{}}, + {at: 0xb93, expected: FDE{}}, + {at: 0x1000, expected: FDE{}}, + {at: 0xcafe000, expected: FDE{}}, + } + buffer, err := base64.StdEncoding.DecodeString(usrBinVolname) + require.NoError(t, err) + elf, err := pfelf.NewFile(bytes.NewReader(buffer), 0, false) + require.NoError(t, err) + t.Cleanup(func() { + err = elf.Close() + require.NoError(t, err) + }) + e, err := NewEhFrameTable(elf) + require.NoError(t, err) + for _, check := range checks { + t.Run(fmt.Sprintf("%x", check.at), func(t *testing.T) { + actual, err := e.LookupFDE(libpf.Address(check.at)) + if check.expected == (FDE{}) { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, check.expected, actual) + } + }) + } +} diff --git a/nativeunwind/elfunwindinfo/stackdeltaextraction_test.go b/nativeunwind/elfunwindinfo/stackdeltaextraction_test.go index 04738ce83..107dbc82e 100644 --- a/nativeunwind/elfunwindinfo/stackdeltaextraction_test.go +++ b/nativeunwind/elfunwindinfo/stackdeltaextraction_test.go @@ -8,9 +8,8 @@ import ( "os" "testing" - sdtypes "go.opentelemetry.io/ebpf-profiler/nativeunwind/stackdeltatypes" - "github.com/stretchr/testify/require" + sdtypes "go.opentelemetry.io/ebpf-profiler/nativeunwind/stackdeltatypes" ) // Base64-encoded data from /usr/bin/volname on a stock debian box, the smallest