Skip to content
Merged
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
114 changes: 51 additions & 63 deletions nativeunwind/elfunwindinfo/elfehframe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Comment thread
korniltsev marked this conversation as resolved.
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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not directly related to this PR, but it seems like, there is a possible endless loop.
There are cases, e.g. here, here or here, where (*reader) parseHDR(..) returns 0 as fdeLen/hlen. This could cause a potential endless loop.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// 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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
4 changes: 2 additions & 2 deletions nativeunwind/elfunwindinfo/elfehframe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
144 changes: 144 additions & 0 deletions nativeunwind/elfunwindinfo/elfehframetable.go
Original file line number Diff line number Diff line change
@@ -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)
}
Comment on lines +105 to +109
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

last minute nit: this makes EhFrameTable stateful and non-concurrent. Not a problem currently, but would be worth documenting, or fixing this. Simple fix would be to remove this and add index to parseHdrEntry.


// 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
}
}
Loading