-
Notifications
You must be signed in to change notification settings - Fork 399
feat(elfunwindinfo): add LookupFDE #545
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0896d47
3a7d2a7
1709ab4
5a6295f
fa19621
fad438c
4990073
e9163f1
e8d9533
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
@@ -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 | ||
| } | ||
|
|
||
|
|
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. last minute nit: this makes |
||
|
|
||
| // 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 | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.