diff --git a/ebpf/go.mod b/ebpf/go.mod index 9853ab2c92..039b36badc 100644 --- a/ebpf/go.mod +++ b/ebpf/go.mod @@ -19,6 +19,7 @@ require ( github.com/prometheus/prometheus v0.51.2 github.com/samber/lo v1.38.1 github.com/stretchr/testify v1.9.0 + github.com/ulikunitz/xz v0.5.12 golang.org/x/sys v0.25.0 ) diff --git a/ebpf/go.sum b/ebpf/go.sum index 6790270251..d718c06242 100644 --- a/ebpf/go.sum +++ b/ebpf/go.sum @@ -72,6 +72,8 @@ github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= +github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= diff --git a/ebpf/symtab/elf.go b/ebpf/symtab/elf.go index df33bb1ee6..675fe693e9 100644 --- a/ebpf/symtab/elf.go +++ b/ebpf/symtab/elf.go @@ -176,9 +176,30 @@ func (et *ElfTable) createSymbolTable(me *elf2.MMapedElfFile) (SymbolNameResolve symbolOptions.FilterFrom = goTable.Index.Entry.Get(0) symbolOptions.FilterTo = goTable.Index.End } - symTable, symErr := me.NewSymbolTable(&symbolOptions) + origSymTable, origErr := me.NewSymbolTable(&symbolOptions) + + var symTable elf2.SymbolTableInterface + var symErr error + if origErr == nil && origSymTable.HasSection(elf.SHT_SYMTAB) { + symTable = origSymTable + symErr = nil + } else { + miniSymTable, miniErr := me.NewMiniDebugInfoSymbolTable(&symbolOptions) + if origErr != nil && miniErr != nil { + symTable = nil + symErr = fmt.Errorf("o: %s m: %s", origErr.Error(), miniErr.Error()) + } else { + tab := &elf2.SymbolTableWithMiniDebugInfo{ + Primary: origSymTable, + MiniDebug: miniSymTable, + } + symTable = tab + symErr = nil + } + } + if symErr != nil && goErr != nil { - return nil, fmt.Errorf("s: %s g: %s", symErr.Error(), goErr.Error()) + return nil, fmt.Errorf("s: {%s} g: {%s}", symErr.Error(), goErr.Error()) } if symErr == nil && goErr == nil { return &elf2.GoTableWithFallback{ diff --git a/ebpf/symtab/elf/elf_sym.go b/ebpf/symtab/elf/elf_sym.go index 6f1cc1c3d5..81a604a439 100644 --- a/ebpf/symtab/elf/elf_sym.go +++ b/ebpf/symtab/elf/elf_sym.go @@ -35,7 +35,7 @@ type SymbolsOptions struct { } // todo consider using ReaderAt here, same as in gopcln -func (f *MMapedElfFile) getSymbols(typ elf.SectionType, opt *SymbolsOptions) ([]SymbolIndex, uint32, error) { +func (f *InMemElfFile) getSymbols(typ elf.SectionType, opt *SymbolsOptions) ([]SymbolIndex, uint32, error) { switch f.Class { case elf.ELFCLASS64: return f.getSymbols64(typ, opt) @@ -51,7 +51,7 @@ func (f *MMapedElfFile) getSymbols(typ elf.SectionType, opt *SymbolsOptions) ([] // if there is no such section in the File. var ErrNoSymbols = errors.New("no symbol section") -func (f *MMapedElfFile) getSymbols64(typ elf.SectionType, opt *SymbolsOptions) ([]SymbolIndex, uint32, error) { +func (f *InMemElfFile) getSymbols64(typ elf.SectionType, opt *SymbolsOptions) ([]SymbolIndex, uint32, error) { symtabSection := f.sectionByType(typ) if symtabSection == nil { return nil, 0, ErrNoSymbols @@ -108,7 +108,7 @@ func (f *MMapedElfFile) getSymbols64(typ elf.SectionType, opt *SymbolsOptions) ( return symbols[:i], symtabSection.Link, nil } -func (f *MMapedElfFile) getSymbols32(typ elf.SectionType, opt *SymbolsOptions) ([]SymbolIndex, uint32, error) { +func (f *InMemElfFile) getSymbols32(typ elf.SectionType, opt *SymbolsOptions) ([]SymbolIndex, uint32, error) { symtabSection := f.sectionByType(typ) if symtabSection == nil { return nil, 0, ErrNoSymbols diff --git a/ebpf/symtab/elf/elfinmem.go b/ebpf/symtab/elf/elfinmem.go new file mode 100644 index 0000000000..eff01a3286 --- /dev/null +++ b/ebpf/symtab/elf/elfinmem.go @@ -0,0 +1,114 @@ +package elf + +import ( + "bytes" + "debug/elf" + "io" + "strings" + + "github.com/ianlancetaylor/demangle" +) + +type ElfSymbolReader interface { + getString(start int, demangleOptions []demangle.Option) (string, bool) +} + +type InMemElfFile struct { + elf.FileHeader + Sections []elf.SectionHeader + Progs []elf.ProgHeader + stringCache map[int]string + + reader io.ReaderAt +} + +func NewInMemElfFile(r io.ReaderAt) (*InMemElfFile, error) { + res := &InMemElfFile{ + reader: r, + } + elfFile, err := elf.NewFile(res.reader) + if err != nil { + return nil, err + } + progs := make([]elf.ProgHeader, 0, len(elfFile.Progs)) + sections := make([]elf.SectionHeader, 0, len(elfFile.Sections)) + for i := range elfFile.Progs { + progs = append(progs, elfFile.Progs[i].ProgHeader) + } + for i := range elfFile.Sections { + sections = append(sections, elfFile.Sections[i].SectionHeader) + } + res.FileHeader = elfFile.FileHeader + res.Progs = progs + res.Sections = sections + return res, nil +} + +func (f *InMemElfFile) Clear() { + f.stringCache = nil + f.Sections = nil +} + +func (f *InMemElfFile) resetReader(r io.ReaderAt) { + f.reader = r +} + +func (f *InMemElfFile) Section(name string) *elf.SectionHeader { + for i := range f.Sections { + s := &f.Sections[i] + if s.Name == name { + return s + } + } + return nil +} + +func (f *InMemElfFile) sectionByType(typ elf.SectionType) *elf.SectionHeader { + for i := range f.Sections { + s := &f.Sections[i] + if s.Type == typ { + return s + } + } + return nil +} + +func (f *InMemElfFile) SectionData(s *elf.SectionHeader) ([]byte, error) { + res := make([]byte, s.Size) + if _, err := f.reader.ReadAt(res, int64(s.Offset)); err != nil { + return nil, err + } + return res, nil +} + +// getString extracts a string from an ELF string table. +func (f *InMemElfFile) getString(start int, demangleOptions []demangle.Option) (string, bool) { + if s, ok := f.stringCache[start]; ok { + return s, true + } + const tmpBufSize = 128 + var tmpBuf [tmpBufSize]byte + sb := strings.Builder{} + for i := 0; i < 10; i++ { + _, err := f.reader.ReadAt(tmpBuf[:], int64(start+i*tmpBufSize)) + if err != nil { + return "", false + } + idx := bytes.IndexByte(tmpBuf[:], 0) + if idx >= 0 { + sb.Write(tmpBuf[:idx]) + s := sb.String() + if len(demangleOptions) > 0 { + s = demangle.Filter(s, demangleOptions...) + } + if f.stringCache == nil { + f.stringCache = make(map[int]string) + } + f.stringCache[start] = s + return s, true + } else { + sb.Write(tmpBuf[:]) + } + } + return "", false +} diff --git a/ebpf/symtab/elf/elfmmap.go b/ebpf/symtab/elf/elfmmap.go index 97040b9b40..3bc2350d0a 100644 --- a/ebpf/symtab/elf/elfmmap.go +++ b/ebpf/symtab/elf/elfmmap.go @@ -1,26 +1,19 @@ package elf import ( - "bytes" "debug/elf" "fmt" "os" "runtime" - "strings" "github.com/ianlancetaylor/demangle" ) type MMapedElfFile struct { - elf.FileHeader - Sections []elf.SectionHeader - Progs []elf.ProgHeader - + InMemElfFile fpath string err error fd *os.File - - stringCache map[int]string } func NewMMapedElfFile(fpath string) (*MMapedElfFile, error) { @@ -32,47 +25,15 @@ func NewMMapedElfFile(fpath string) (*MMapedElfFile, error) { res.Close() return nil, err } - elfFile, err := elf.NewFile(res.fd) + f, err := NewInMemElfFile(res.fd) if err != nil { res.Close() return nil, err } - progs := make([]elf.ProgHeader, 0, len(elfFile.Progs)) - sections := make([]elf.SectionHeader, 0, len(elfFile.Sections)) - for i := range elfFile.Progs { - progs = append(progs, elfFile.Progs[i].ProgHeader) - } - for i := range elfFile.Sections { - sections = append(sections, elfFile.Sections[i].SectionHeader) - } - res.FileHeader = elfFile.FileHeader - res.Progs = progs - res.Sections = sections - + res.InMemElfFile = *f runtime.SetFinalizer(res, (*MMapedElfFile).Finalize) return res, nil } - -func (f *MMapedElfFile) Section(name string) *elf.SectionHeader { - for i := range f.Sections { - s := &f.Sections[i] - if s.Name == name { - return s - } - } - return nil -} - -func (f *MMapedElfFile) sectionByType(typ elf.SectionType) *elf.SectionHeader { - for i := range f.Sections { - s := &f.Sections[i] - if s.Type == typ { - return s - } - } - return nil -} - func (f *MMapedElfFile) ensureOpen() error { if f.fd != nil { return nil @@ -91,8 +52,7 @@ func (f *MMapedElfFile) Close() { f.fd.Close() f.fd = nil } - f.stringCache = nil - f.Sections = nil + f.InMemElfFile.Clear() } func (f *MMapedElfFile) open() error { if f.err != nil { @@ -104,6 +64,7 @@ func (f *MMapedElfFile) open() error { return fmt.Errorf("open elf file %s %w", f.fpath, err) } f.fd = fd + f.InMemElfFile.resetReader(f.fd) return nil } @@ -111,11 +72,7 @@ func (f *MMapedElfFile) SectionData(s *elf.SectionHeader) ([]byte, error) { if err := f.ensureOpen(); err != nil { return nil, err } - res := make([]byte, s.Size) - if _, err := f.fd.ReadAt(res, int64(s.Offset)); err != nil { - return nil, err - } - return res, nil + return f.InMemElfFile.SectionData(s) } func (f *MMapedElfFile) FilePath() string { @@ -127,32 +84,5 @@ func (f *MMapedElfFile) getString(start int, demangleOptions []demangle.Option) if err := f.ensureOpen(); err != nil { return "", false } - if s, ok := f.stringCache[start]; ok { - return s, true - } - const tmpBufSize = 128 - var tmpBuf [tmpBufSize]byte - sb := strings.Builder{} - for i := 0; i < 10; i++ { - _, err := f.fd.ReadAt(tmpBuf[:], int64(start+i*tmpBufSize)) - if err != nil { - return "", false - } - idx := bytes.IndexByte(tmpBuf[:], 0) - if idx >= 0 { - sb.Write(tmpBuf[:idx]) - s := sb.String() - if len(demangleOptions) > 0 { - s = demangle.Filter(s, demangleOptions...) - } - if f.stringCache == nil { - f.stringCache = make(map[int]string) - } - f.stringCache[start] = s - return s, true - } else { - sb.Write(tmpBuf[:]) - } - } - return "", false + return f.InMemElfFile.getString(start, demangleOptions) } diff --git a/ebpf/symtab/elf/go_table.go b/ebpf/symtab/elf/go_table.go index b78fcd3a77..abae81f619 100644 --- a/ebpf/symtab/elf/go_table.go +++ b/ebpf/symtab/elf/go_table.go @@ -144,7 +144,7 @@ func (g *GoTable) goSymbolName(idx int) (string, error) { type GoTableWithFallback struct { GoTable *GoTable - SymTable *SymbolTable + SymTable SymbolTableInterface } func (g *GoTableWithFallback) IsDead() bool { @@ -179,3 +179,71 @@ func (g *GoTableWithFallback) Cleanup() { g.GoTable.Cleanup() g.SymTable.Cleanup() // second call is no op now, but call anyway just in case } + +type SymbolTableWithMiniDebugInfo struct { + Primary *SymbolTable + MiniDebug *SymbolTable +} + +func (stm *SymbolTableWithMiniDebugInfo) IsDead() bool { + return (stm.Primary != nil && stm.Primary.IsDead()) || (stm.MiniDebug != nil && stm.MiniDebug.IsDead()) +} + +func (stm *SymbolTableWithMiniDebugInfo) DebugInfo() SymTabDebugInfo { + return SymTabDebugInfo{ + Name: fmt.Sprintf("SymbolTableWithMiniDebugInfo %p", stm), + Size: stm.Size(), + } +} + +func (stm *SymbolTableWithMiniDebugInfo) Size() int { + size := 0 + if stm.Primary != nil { + size += stm.Primary.Size() + } + if stm.MiniDebug != nil { + size += stm.MiniDebug.Size() + } + return size +} + +func (stm *SymbolTableWithMiniDebugInfo) Refresh() { + if stm.Primary != nil { + stm.Primary.Refresh() + } + if stm.MiniDebug != nil { + stm.MiniDebug.Refresh() + } +} + +func (stm *SymbolTableWithMiniDebugInfo) DebugString() string { + primary := "nil" + if stm.Primary != nil { + primary = stm.Primary.DebugString() + } + minidebug := "nil" + if stm.MiniDebug != nil { + minidebug = stm.MiniDebug.DebugString() + } + return fmt.Sprintf("SymbolTableWithMiniDebugInfo{ %s %s }", primary, minidebug) +} + +func (stm *SymbolTableWithMiniDebugInfo) Resolve(addr uint64) string { + name := "" + if stm.Primary != nil { + name = stm.Primary.Resolve(addr) + } + if name == "" && stm.MiniDebug != nil { + name = stm.MiniDebug.Resolve(addr) + } + return name +} + +func (stm *SymbolTableWithMiniDebugInfo) Cleanup() { + if stm.Primary != nil { + stm.Primary.Cleanup() + } + if stm.MiniDebug != nil { + stm.MiniDebug.Cleanup() + } +} diff --git a/ebpf/symtab/elf/symbol_table.go b/ebpf/symtab/elf/symbol_table.go index 0e207c5dd5..826446fd05 100644 --- a/ebpf/symtab/elf/symbol_table.go +++ b/ebpf/symtab/elf/symbol_table.go @@ -1,17 +1,29 @@ package elf import ( + "bytes" "debug/elf" "errors" "fmt" + "io" "sort" "github.com/grafana/pyroscope/ebpf/symtab/gosym" "github.com/ianlancetaylor/demangle" + "github.com/ulikunitz/xz" ) // symbols from .symtab, .dynsym +type SymbolTableInterface interface { + Refresh() + Cleanup() + DebugInfo() SymTabDebugInfo + IsDead() bool + Resolve(addr uint64) string + Size() int +} + type SymbolIndex struct { Name Name Value uint64 @@ -42,8 +54,10 @@ type FlatSymbolIndex struct { Values gosym.PCIndex } type SymbolTable struct { - Index FlatSymbolIndex - File *MMapedElfFile + Index FlatSymbolIndex + File *MMapedElfFile + SymReader ElfSymbolReader + hasSection map[elf.SectionType]bool demangleOptions []demangle.Option } @@ -54,9 +68,10 @@ func (st *SymbolTable) IsDead() bool { func (st *SymbolTable) DebugInfo() SymTabDebugInfo { return SymTabDebugInfo{ - Name: fmt.Sprintf("SymbolTable %p", st), - Size: len(st.Index.Names), - File: st.File.fpath, + Name: fmt.Sprintf("SymbolTable %p", st), + Size: len(st.Index.Names), + File: st.File.fpath, + MiniDebugInfo: st.File != st.SymReader, } } @@ -64,12 +79,21 @@ func (st *SymbolTable) Size() int { return len(st.Index.Names) } +func (st *SymbolTable) HasSection(typ elf.SectionType) bool { + val, exist := st.hasSection[typ] + if exist { + return val + } else { + return false + } +} + func (st *SymbolTable) Refresh() { } func (st *SymbolTable) DebugString() string { - return fmt.Sprintf("SymbolTable{ f = %s , sz = %d }", st.File.FilePath(), st.Index.Values.Length()) + return fmt.Sprintf("SymbolTable{ f = %s , sz = %d, mdi = %t }", st.File.FilePath(), st.Index.Values.Length(), st.File != st.SymReader) } func (st *SymbolTable) Resolve(addr uint64) string { @@ -88,7 +112,7 @@ func (st *SymbolTable) Cleanup() { st.File.Close() } -func (f *MMapedElfFile) NewSymbolTable(opt *SymbolsOptions) (*SymbolTable, error) { +func (f *InMemElfFile) NewSymbolTable(opt *SymbolsOptions, symReader ElfSymbolReader, file *MMapedElfFile) (*SymbolTable, error) { sym, sectionSym, err := f.getSymbols(elf.SHT_SYMTAB, opt) if err != nil && !errors.Is(err, ErrNoSymbols) { return nil, err @@ -121,7 +145,12 @@ func (f *MMapedElfFile) NewSymbolTable(opt *SymbolsOptions) (*SymbolTable, error Names: make([]Name, total), Values: gosym.NewPCIndex(total), }, - File: f, + hasSection: map[elf.SectionType]bool{ + elf.SHT_SYMTAB: len(sym) > 0, + elf.SHT_DYNSYM: len(dynsym) > 0, + }, + File: file, + SymReader: symReader, demangleOptions: opt.DemangleOptions, } for i := range all { @@ -131,11 +160,40 @@ func (f *MMapedElfFile) NewSymbolTable(opt *SymbolsOptions) (*SymbolTable, error return res, nil } +func (f *MMapedElfFile) NewSymbolTable(opt *SymbolsOptions) (*SymbolTable, error) { + return f.InMemElfFile.NewSymbolTable(opt, f, f) +} + +func (f *MMapedElfFile) NewMiniDebugInfoSymbolTable(opt *SymbolsOptions) (*SymbolTable, error) { + miniDebugSection := f.Section(".gnu_debugdata") + if miniDebugSection == nil { + return nil, ErrNoSymbols + } + data, dataErr := f.SectionData(miniDebugSection) + if dataErr != nil { + return nil, dataErr + } + reader, readErr := xz.NewReader(bytes.NewReader(data)) + if readErr != nil { + return nil, readErr + } + var uncompressed bytes.Buffer + _, ioErr := io.Copy(&uncompressed, reader) + if ioErr != nil { + return nil, ioErr + } + miniDebugElf, miniDebugElfErr := NewInMemElfFile(bytes.NewReader(uncompressed.Bytes())) + if miniDebugElfErr != nil { + return nil, miniDebugElfErr + } + return miniDebugElf.NewSymbolTable(opt, miniDebugElf, f) +} + func (st *SymbolTable) symbolName(idx int) (string, error) { linkIndex := st.Index.Names[idx].LinkIndex() SectionHeaderLink := &st.Index.Links[linkIndex] NameIndex := st.Index.Names[idx].NameIndex() - s, b := st.File.getString(int(NameIndex)+int(SectionHeaderLink.Offset), st.demangleOptions) + s, b := st.SymReader.getString(int(NameIndex)+int(SectionHeaderLink.Offset), st.demangleOptions) if !b { return "", fmt.Errorf("elf getString") } @@ -146,5 +204,6 @@ type SymTabDebugInfo struct { Name string `alloy:"name,attr,optional" river:"name,attr,optional"` Size int `alloy:"symbol_count,attr,optional" river:"symbol_count,attr,optional"` File string `alloy:"file,attr,optional" river:"file,attr,optional"` + MiniDebugInfo bool `alloy:"mini_debug_info,attr,optional" river:"mini_debug_info,attr,optional"` LastUsedRound int `alloy:"last_used_round,attr,optional" river:"last_used_round,attr,optional"` } diff --git a/ebpf/symtab/elf/testdata/elfs/elf.minidebuginfo b/ebpf/symtab/elf/testdata/elfs/elf.minidebuginfo new file mode 100755 index 0000000000..40c3cb993b Binary files /dev/null and b/ebpf/symtab/elf/testdata/elfs/elf.minidebuginfo differ diff --git a/ebpf/symtab/elf_test.go b/ebpf/symtab/elf_test.go index b01f39db98..fd9cc8cc11 100644 --- a/ebpf/symtab/elf_test.go +++ b/ebpf/symtab/elf_test.go @@ -166,3 +166,26 @@ func TestFindBaseUnalignedSeparateCode(t *testing.T) { assert.True(t, et.findBase(&ef)) assert.Equal(t, uint64(0x555e3d192000), et.base) } + +func TestMiniDebugInfo(t *testing.T) { + elfCache, _ := NewElfCache(testCacheOptions, testCacheOptions) + logger := util.TestLogger(t) + tab := NewElfTable(logger, &ProcMap{StartAddr: 0x1000, Offset: 0x1000}, ".", "elf/testdata/elfs/elf.minidebuginfo", + ElfTableOptions{ + ElfCache: elfCache, + Metrics: metrics.NewSymtabMetrics(nil), + }) + + syms := []struct { + name string + pc uint64 + }{ + {"", 0x0}, + {"android_res_cancel", 0x1330}, // in .dynsym + {"__on_dlclose", 0x1000}, // in .gnu_debugdata.symtab + } + for _, sym := range syms { + res := tab.Resolve(sym.pc) + require.Equal(t, res, sym.name) + } +}