From 9682c55d0591631af6259db1454ae53e6e18702f Mon Sep 17 00:00:00 2001 From: Nikolay Sivko Date: Wed, 16 Apr 2025 17:20:18 +0300 Subject: [PATCH] feat: add support for reading symbols from perf map files --- ebpf/symtab/kallsyms.go | 2 +- ebpf/symtab/perfmap.go | 151 ++++++++++++++++++++++++++++++++++++++++ ebpf/symtab/proc.go | 12 ++++ ebpf/symtab/table.go | 3 + 4 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 ebpf/symtab/perfmap.go diff --git a/ebpf/symtab/kallsyms.go b/ebpf/symtab/kallsyms.go index 722c1acc28..fa0499f3e2 100644 --- a/ebpf/symtab/kallsyms.go +++ b/ebpf/symtab/kallsyms.go @@ -90,7 +90,7 @@ func NewKallsymsFromData(kallsyms []byte) (*SymbolTab, error) { if istart != 0 { allZeros = false } - syms = append(syms, Symbol{istart, string(name), string(mod)}) + syms = append(syms, Symbol{Start: istart, Name: string(name), Module: string(mod)}) } if allZeros { return NewSymbolTab(nil), nil diff --git a/ebpf/symtab/perfmap.go b/ebpf/symtab/perfmap.go new file mode 100644 index 0000000000..bff34cdfba --- /dev/null +++ b/ebpf/symtab/perfmap.go @@ -0,0 +1,151 @@ +package symtab + +import ( + "bufio" + "cmp" + "errors" + "fmt" + "io" + "os" + "slices" + "strconv" + "strings" +) + +type PerfMap struct { + symbols []Symbol +} + +func NewPerfMap(pid int) (*PerfMap, error) { + nsPid := getNsPid(pid) + + perfMapPath := fmt.Sprintf("/proc/%d/root/tmp/perf-%d.map", pid, nsPid) + f, err := os.Open(perfMapPath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, nil + } + return nil, err + } + defer f.Close() + pm := &PerfMap{} + if pm.symbols, err = parsePerfMap(f); err != nil { + return nil, err + } + return pm, nil +} + +func (pm *PerfMap) Resolve(pc uint64) Symbol { + i, ok := slices.BinarySearchFunc(pm.symbols, -1, func(s Symbol, _ int) int { + if s.Start <= pc { + if pc < s.Start+s.size { + return 0 + } + return -1 + } + return 1 + }) + if ok { + return pm.symbols[i] + } + return Symbol{} +} + +func parsePerfMap(reader io.Reader) ([]Symbol, error) { + var ( + syms []Symbol + err error + ) + scanner := bufio.NewScanner(reader) + for i := 0; scanner.Scan(); i++ { + line := scanner.Text() + items := strings.SplitN(line, " ", 3) + if len(items) != 3 { + return nil, fmt.Errorf("invalid line (does not contain 3 parts): %s", line) + } + s := Symbol{ + Name: items[2], + generation: i, + } + s.Start, err = strconv.ParseUint(strings.TrimPrefix(items[0], "0x"), 16, 64) + if err != nil { + return nil, err + } + s.size, err = strconv.ParseUint(strings.TrimPrefix(items[1], "0x"), 16, 64) + if err != nil { + return nil, err + } + syms = append(syms, s) + } + if err := scanner.Err(); err != nil { + return nil, err + } + slices.SortFunc(syms, func(a, b Symbol) int { + return cmp.Compare(a.Start, b.Start) + }) + syms = removeOverlaps(syms) + return syms, nil +} + +// Node.js appends new symbols to the perf map file, and their addresses can overlap earlier ones. +// This function removes overlapping symbols, keeping those that appeared latest. +func removeOverlaps(syms []Symbol) []Symbol { + if len(syms) == 0 { + return nil + } + + var ( + lastValid = 0 + toRemove = make(map[int]struct{}) + ) + + for i := 1; i < len(syms); i++ { + prev := syms[lastValid] + curr := syms[i] + + if prev.Start+prev.size > curr.Start { + // Overlap detected + if prev.generation > curr.generation { + toRemove[i] = struct{}{} + } else { + toRemove[lastValid] = struct{}{} + lastValid = i + } + } else { + lastValid = i + } + } + + // Compact the slice in-place + write := 0 + for i := range syms { + if _, skip := toRemove[i]; !skip { + syms[write] = syms[i] + write++ + } + } + + return syms[:write] +} + +func getNsPid(pid int) int { + data, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid)) + if err != nil { + return pid + } + for _, line := range strings.Split(string(data), "\n") { + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + if fields[0] == "NSpid:" { + if len(fields) == 3 { + if nsPid, err := strconv.ParseUint(fields[2], 10, 32); err == nil { + return int(nsPid) + } + } + return pid + } + } + return pid +} diff --git a/ebpf/symtab/proc.go b/ebpf/symtab/proc.go index 2bcf3d6938..5c7c514f46 100644 --- a/ebpf/symtab/proc.go +++ b/ebpf/symtab/proc.go @@ -22,6 +22,7 @@ type ProcTable struct { options ProcTableOptions rootFS string err error + perfMap *PerfMap } type ProcTableDebugInfo struct { @@ -83,6 +84,12 @@ func (p *ProcTable) Refresh() { if p.err != nil { _ = level.Error(p.logger).Log("err", p.err) } + + p.perfMap, err = NewPerfMap(p.options.Pid) + if err != nil { + level.Error(p.logger).Log("msg", "failed to read perf map file", "err", err) + p.err = err + } } func (p *ProcTable) Error() error { @@ -141,6 +148,11 @@ func (p *ProcTable) Resolve(pc uint64) Symbol { if pc == 0xcccccccccccccccc || pc == 0x9090909090909090 { return Symbol{Start: 0, Name: "end_of_stack", Module: "[unknown]"} } + if p.perfMap != nil { + if s := p.perfMap.Resolve(pc); s.Name != "" { + return s + } + } i, found := slices.BinarySearchFunc(p.ranges, pc, binarySearchElfRange) if !found { return Symbol{} diff --git a/ebpf/symtab/table.go b/ebpf/symtab/table.go index 1f18815bf9..063d1376ea 100644 --- a/ebpf/symtab/table.go +++ b/ebpf/symtab/table.go @@ -17,6 +17,9 @@ type Symbol struct { Start uint64 Name string Module string + + generation int + size uint64 } func NewSymbolTab(symbols []Symbol) *SymbolTab {