From 5192e8b49f2c256ba4355dc643a91d53c53c531f Mon Sep 17 00:00:00 2001 From: Dylan Reimerink Date: Tue, 19 May 2026 14:58:10 +0200 Subject: [PATCH] Add instruction based stack slot usage analysis It turns out that clang does not always emit DWARF variable nodes for all stack slot usage. This caused gaps in the list output. This commit adds a second source of stack slot usage information be analyzing the eBPF instructions directly to find pattens missed by the DWARF analysis. Specifically when clang emits instructions like: ``` Mov R2, R10 Add R2, -16 ... Call some_func ``` This is a common pattern when values are passed to helper functions or kfuncs. The downside is that we do not get information about the size of the variable being referenced. Specifically for map lookups there is no size parameter passed, rather size is inferred from the map definition. So for now we just report the slot size as -1 / unknown. During testing it was found that the Mov+Add instructions usually do not have line info themselfs and that the closest line info is often on the Call instruction below it. So when we find the pattern we associate the first line info below the Mov+Add instructions with the stack slot usage. To obtain the callstack we create a instruction -> callstack mapping for every instruction in the program based on DWARF lowpc, highpc and range lists. Signed-off-by: Dylan Reimerink --- cmd/stackwhere/list.go | 365 +++++++++++++++++++++++++++--------- cmd/stackwhere/list_test.go | 77 +++++++- go.mod | 4 +- go.sum | 20 +- internal/dwarf/loclist.go | 1 + internal/dwarf/rangelist.go | 276 +++++++++++++++++++++++++++ internal/dwarf/reexport.go | 14 +- internal/dwarf/tree.go | 67 ++++++- testdata/Makefile | 3 +- testdata/spill.c | 46 +++++ testdata/spill.o | Bin 0 -> 5424 bytes 11 files changed, 773 insertions(+), 100 deletions(-) create mode 100644 internal/dwarf/rangelist.go create mode 100644 testdata/spill.c create mode 100644 testdata/spill.o diff --git a/cmd/stackwhere/list.go b/cmd/stackwhere/list.go index 489810e..dad7998 100644 --- a/cmd/stackwhere/list.go +++ b/cmd/stackwhere/list.go @@ -7,6 +7,9 @@ import ( "slices" "strings" + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/asm" + "github.com/cilium/ebpf/btf" "github.com/cilium/stackwhere/internal/dwarf" "github.com/cilium/stackwhere/internal/dwarf/op" "github.com/spf13/cobra" @@ -52,7 +55,76 @@ func (psl *programStackList) runListProgram(cmd *cobra.Command, args []string) e return fmt.Errorf("failed to parse DWARF data: %w", err) } - usage := getStackSlotUsage(tree, functionName) + coll, err := ebpf.LoadCollectionSpec(collectionPath) + if err != nil { + return fmt.Errorf("failed to load eBPF collection: %w", err) + } + + subProgsDwarf := tree.ByType(dwarf.TagSubprogram) + subProgDwarfIdx := slices.IndexFunc(subProgsDwarf, func(n *dwarf.Node) bool { + return n.Name() == functionName + }) + if subProgDwarfIdx == -1 { + return fmt.Errorf("function %q not found in DWARF data", functionName) + } + + subProgDwarf := subProgsDwarf[subProgDwarfIdx] + subProg := coll.Programs[functionName] + if subProg == nil { + return fmt.Errorf("function %q not found in eBPF collection", functionName) + } + + usage := stackSlotsFromDWARFVars(subProgDwarf) + usage = append(usage, stackSlotsFromInsns(subProg, subProgDwarf)...) + + // Sort outer array + slices.SortFunc(usage, func(a, b []slotUsage) int { + return int(a[0].Offset - b[0].Offset) + }) + // Merge inner arrays with the same offset + for i := range slices.Backward(usage) { + if i == 0 { + break + } + + if usage[i][0].Offset == usage[i-1][0].Offset { + usage[i-1] = append(usage[i-1], usage[i]...) + usage = slices.Delete(usage, i, i+1) + } + } + // Sort inner arrays by size, largest first, and then by name. And deduplicate. + for i := range usage { + slices.SortFunc(usage[i], func(a, b slotUsage) int { + sz := int(b.ByteSize - a.ByteSize) + if sz != 0 { + return sz + } + + name := strings.Compare(a.Name, b.Name) + if name != 0 { + return name + } + + return strings.Compare(a.FileLineCol, b.FileLineCol) + }) + + // Remove duplicates that can occur, for example when a function is inlined multiple times and it ends up reusing the same stack space. + usage[i] = slices.CompactFunc(usage[i], func(a, b slotUsage) bool { + callstackEqual := true + if len(a.Callstack) != len(b.Callstack) { + callstackEqual = false + } else { + for j := range a.Callstack { + if a.Callstack[j] != b.Callstack[j] { + callstackEqual = false + break + } + } + } + return a.Name == b.Name && a.ByteSize == b.ByteSize && a.FileLineCol == b.FileLineCol && callstackEqual + }) + } + if jsonOutput(cmd) { e := json.NewEncoder(cmd.OutOrStdout()) e.SetIndent("", " ") @@ -64,10 +136,20 @@ func (psl *programStackList) runListProgram(cmd *cobra.Command, args []string) e for _, slots := range usage { fmt.Printf("R10-%d:\n", slots[0].Offset) for _, slot := range slots { - fmt.Printf(" %d - %s @ %s\n", slot.ByteSize, slot.Name, slot.FileCol) + size := fmt.Sprintf("%d", slot.ByteSize) + if slot.ByteSize == -1 { + size = "?" + } + + name := slot.Name + if name == "" { + name = "(unknown)" + } + + fmt.Printf(" %s - %s @ %s\n", size, name, slot.FileLineCol) if *psl.flagCallStack { for _, entry := range slot.Callstack { - fmt.Printf(" %s @ %s\n", entry.Name, entry.FileCol) + fmt.Printf(" %s @ %s\n", entry.Name, entry.FileLineCol) } } } @@ -77,108 +159,107 @@ func (psl *programStackList) runListProgram(cmd *cobra.Command, args []string) e return nil } +type slotList [][]slotUsage + +func (s slotList) Add(slot slotUsage) slotList { + i, found := slices.BinarySearchFunc(s, []slotUsage{slot}, func(a, b []slotUsage) int { + return int(a[0].Offset - b[0].Offset) + }) + if found { + s[i] = append(s[i], slot) + } else { + s = slices.Insert(s, i, []slotUsage{slot}) + } + return s +} + type slotUsage struct { - Offset int64 `json:"offset"` - Name string `json:"name"` - ByteSize int64 `json:"byte_size"` - FileCol string `json:"file_col"` - Callstack []callStackEntry `json:"callstack,omitempty"` + Offset int64 `json:"offset"` + Name string `json:"name"` + ByteSize int64 `json:"byte_size"` + FileLineCol string `json:"file_line_col"` + Callstack []callStackEntry `json:"callstack,omitempty"` } type callStackEntry struct { - Name string `json:"name"` - FileCol string `json:"file_col"` + Name string `json:"name"` + FileLineCol string `json:"file_line_col"` } -// getStackSlotUsage returns a list of stack slots used by the given function, sorted by their offset from R10 (largest offset first). -// Each stack slot includes the variables that live at that slot, sorted by byte size (largest first) and then name, -// and optionally the callstack of each variable. -func getStackSlotUsage(tree *dwarf.Tree, functionName string) [][]slotUsage { - result := [][]slotUsage{} - for _, n := range tree.ByType(dwarf.TagSubprogram) { - name := n.Name() - if name == "" || name != functionName { - continue +// stackSlotsFromDWARFVars returns a list of stack slots used by the given function, result is unsorted. +func stackSlotsFromDWARFVars(progDwarf *dwarf.Node) slotList { + result := slotList{} + + stackMap := map[int64][]*dwarf.Node{} + dwarf.VisitPrefixOrder(progDwarf, func(n *dwarf.Node) { + // We are interested in variables and function parameters since those are the things that can be stored on + // the stack. + if n.Entry().Tag != dwarf.TagVariable && n.Entry().Tag != dwarf.TagFormalParameter { + return } - entrypoint := n - stackMap := map[int64][]*dwarf.Node{} - dwarf.VisitPrefixOrder(n, func(n *dwarf.Node) { - // We are interested in variables and function parameters since those are the things that can be stored on - // the stack. - if n.Entry().Tag != dwarf.TagVariable && n.Entry().Tag != dwarf.TagFormalParameter { - return + // If the current variable lives on the stack, add it to the map of stack offsets to variables that live at that offset. + offsets := stackOffsets(n) + if len(offsets) > 0 { + for _, offset := range offsets { + if !slices.Contains(stackMap[offset], n) { + stackMap[offset] = append(stackMap[offset], n) + } } + } + }) - // If the current variable lives on the stack, add it to the map of stack offsets to variables that live at that offset. - offsets := stackOffsets(n) - if len(offsets) > 0 { - for _, offset := range offsets { - if !slices.Contains(stackMap[offset], n) { - stackMap[offset] = append(stackMap[offset], n) - } - } + for offset, nodes := range stackMap { + slices.SortFunc(nodes, func(a, b *dwarf.Node) int { + sz := int(b.ByteSize()) - int(a.ByteSize()) + if sz != 0 { + return sz } + + return strings.Compare(a.Name(), b.Name()) }) - // Print the variables grouped by their stack offset, sorted by largest byte size first and then name. - for _, offset := range slices.SortedFunc(maps.Keys(stackMap), func(a, b int64) int { - return int(b - a) - }) { - nodes := stackMap[offset] - slices.SortFunc(nodes, func(a, b *dwarf.Node) int { - sz := int(b.ByteSize()) - int(a.ByteSize()) - if sz != 0 { - return sz - } + for _, n := range nodes { + callstack := []callStackEntry{} - return strings.Compare(a.Name(), b.Name()) - }) - - // Remove duplicates that can occur, for example when a function is inlined multiple times and it - // ends up reusing the same stack space. - nodes = slices.CompactFunc(nodes, func(a, b *dwarf.Node) bool { - return a.Name() == b.Name() && a.ByteSize() == b.ByteSize() && a.FileCol() == b.FileCol() - }) - for _, n := range nodes { - callstack := []callStackEntry{} - - parents := []*dwarf.Node{} - p := n - for p != nil { - p = p.Parent() - parents = append(parents, p) - if p == entrypoint { - break - } + parents := []*dwarf.Node{} + p := n + for p != nil { + p = p.Parent() + parents = append(parents, p) + if p == progDwarf { + break } - for _, parent := range parents { - if parent.Name() == "" { - continue - } - callstack = append(callstack, callStackEntry{ - Name: parent.Name(), - FileCol: parent.FileCol(), - }) + } + for _, parent := range parents { + if parent.Name() == "" { + continue + } + fileLineCol := parent.CallFileLineCol() + if fileLineCol == "" { + fileLineCol = parent.FileLineCol() } - usage := []slotUsage{{ - Offset: offset, - Name: n.Name(), - ByteSize: n.ByteSize(), - FileCol: n.FileCol(), - Callstack: callstack, - }} - - i, found := slices.BinarySearchFunc(result, usage, func(a, b []slotUsage) int { - return int(a[0].Offset - b[0].Offset) + callstack = append(callstack, callStackEntry{ + Name: parent.Name(), + FileLineCol: fileLineCol, }) - if found { - result[i] = append(result[i], usage...) - } else { - result = slices.Insert(result, i, usage) - } } + + fileLineCol := n.CallFileLineCol() + if fileLineCol == "" { + fileLineCol = n.FileLineCol() + } + + usage := slotUsage{ + Offset: offset, + Name: n.Name(), + ByteSize: n.ByteSize(), + FileLineCol: fileLineCol, + Callstack: callstack, + } + + result = result.Add(usage) } } @@ -335,3 +416,115 @@ func isBPFProgram(n *dwarf.Node) bool { return true } + +// Find stack slot usage by looking at the instructions and enhance with DWARF information. +// We are specifically looking for this pattern of instructions: +// +// Mov Rx, R10 +// Add Rx, -N +// +// Where Rx is any register and N is some constant. +func stackSlotsFromInsns(prog *ebpf.ProgramSpec, progDbg *dwarf.Node) slotList { + i2n := instructionToNodes(progDbg) + + var result slotList + + iter := prog.Instructions.Iterate() + for iter.Next() { + // cilium/ebpf automatically adds functions to the program spec, so stop processing + // once we reach the end of the original program's instructions. + if fn := btf.FuncMetadata(iter.Ins); fn != nil { + if fn.Name != prog.Name { + break + } + } + + if iter.Ins.Src == asm.R10 && iter.Ins.OpCode.ALUOp() == asm.Mov { + // Validate the pattern: Mov Rx, R10 followed by Add Rx, -N on the same register. + nextIdx := iter.Index + 1 + if nextIdx >= len(prog.Instructions) { + continue + } + nextInsn := prog.Instructions[nextIdx] + if nextInsn.OpCode.ALUOp() != asm.Add || nextInsn.Dst != iter.Ins.Dst || nextInsn.Constant >= 0 { + continue + } + + byteOff := uint64(iter.Offset * asm.InstructionSize) + + var line *btf.Line + for j := iter.Index; j < len(prog.Instructions); j++ { + ins := prog.Instructions[j] + if lo, ok := ins.Source().(*btf.Line); ok && lo.LineNumber() != 0 { + line = lo + break + } + } + + fileCol := "" + if line != nil { + fileCol = line.FileName() + ":" + fmt.Sprint(line.LineNumber()) + } + + usage := slotUsage{ + Offset: -nextInsn.Constant, + Name: iter.Ins.Dst.String(), + ByteSize: -1, + FileLineCol: fileCol, + } + for _, n := range i2n[byteOff] { + // Some nodes like Lexical blocks do not have a name or file/line information. + // So not useful in the trace. + if n.Name() == "" && n.FileLineCol() == "" { + continue + } + + fileLineCol := n.CallFileLineCol() + if fileLineCol == "" { + fileLineCol = n.FileLineCol() + } + + usage.Callstack = append(usage.Callstack, callStackEntry{ + Name: n.Name(), + FileLineCol: fileLineCol, + }) + } + + result = result.Add(usage) + } + } + + return result +} + +// Create a mapping of instruction offsets to the DWARF nodes that are valid at that instruction. +func instructionToNodes(prog *dwarf.Node) map[uint64][]*dwarf.Node { + instRange := make(map[uint64][]*dwarf.Node) + + progInsOffset := prog.Entry().Val(dwarf.AttrLowpc).(uint64) + + dwarf.VisitPrefixOrder(prog, func(n *dwarf.Node) { + lowpc := n.Entry().Val(dwarf.AttrLowpc) + highpc := n.Entry().Val(dwarf.AttrHighpc) + if lowpc != nil && highpc != nil { + for i := lowpc.(uint64); i < lowpc.(uint64)+uint64(highpc.(int64)); i += asm.InstructionSize { + instRange[i-progInsOffset] = append(instRange[i-progInsOffset], n) + } + } + + if rng, err := n.Ranges(); err == nil { + for _, r := range rng.Ranges { + for i := r.Start; i < r.End; i += asm.InstructionSize { + instRange[i-progInsOffset] = append(instRange[i-progInsOffset], n) + } + } + } + }) + + // Reverse since we want to report the stack callstack from the innermost function to the outermost + for _, nodes := range instRange { + slices.Reverse(nodes) + } + + return instRange +} diff --git a/cmd/stackwhere/list_test.go b/cmd/stackwhere/list_test.go index dacfd4f..a81ded8 100644 --- a/cmd/stackwhere/list_test.go +++ b/cmd/stackwhere/list_test.go @@ -1,18 +1,28 @@ package main import ( + "slices" "testing" + "github.com/cilium/ebpf" "github.com/cilium/stackwhere/internal/dwarf" ) -func TestGetStackSlotUsage(t *testing.T) { +func TestGetStackSlotUsageFromDWARF(t *testing.T) { tree, err := dwarf.NewDWARFTree("../../testdata/basic.o") if err != nil { t.Fatalf("failed to parse DWARF data: %v", err) } - stackUsage := getStackSlotUsage(tree, "cil_entry") + subProgs := tree.ByType(dwarf.TagSubprogram) + idx := slices.IndexFunc(subProgs, func(n *dwarf.Node) bool { + return n.Name() == "cil_entry" + }) + if idx == -1 { + t.Fatalf("failed to find subprogram node for cil_entry") + } + + stackUsage := stackSlotsFromDWARFVars(subProgs[idx]) if len(stackUsage) != 3 { t.Fatalf("expected 3 stack slots, got %d", len(stackUsage)) } @@ -72,6 +82,69 @@ func TestGetStackSlotUsage(t *testing.T) { } } +func TestGetStackSlotUsageFromInsns(t *testing.T) { + tree, err := dwarf.NewDWARFTree("../../testdata/spill.o") + if err != nil { + t.Fatalf("failed to parse DWARF data: %v", err) + } + + spec, err := ebpf.LoadCollectionSpec("../../testdata/spill.o") + if err != nil { + t.Fatalf("failed to load collection spec: %v", err) + } + + subProgs := tree.ByType(dwarf.TagSubprogram) + idx := slices.IndexFunc(subProgs, func(n *dwarf.Node) bool { + return n.Name() == "cil_entry" + }) + if idx == -1 { + t.Fatalf("failed to find subprogram node for cil_entry") + } + + stackUsage := stackSlotsFromInsns(spec.Programs["cil_entry"], subProgs[idx]) + if len(stackUsage) != 1 { + t.Fatalf("expected 1 stack slot group, got %d", len(stackUsage)) + } + + // Verify each stack slot group contains expected slots + slotGroups := []struct { + index int + expected map[string]int64 + }{ + { + index: 0, + expected: map[string]int64{ + "r2": -1, + }, + }, + } + + for _, group := range slotGroups { + found := make(map[string]int64) + for _, slot := range stackUsage[group.index] { + found[slot.Name] = slot.ByteSize + } + + // Check all expected slots are present with correct size + for name, expectedSize := range group.expected { + actualSize, ok := found[name] + if !ok { + t.Errorf("slot group %d: expected slot %q not found", group.index, name) + continue + } + if actualSize != expectedSize { + t.Errorf("slot group %d: slot %q has size %d, expected %d", group.index, name, actualSize, expectedSize) + } + delete(found, name) + } + + // Check no unexpected slots are present + if len(found) > 0 { + t.Errorf("slot group %d: unexpected slots found: %v", group.index, found) + } + } +} + func TestGetProgramStackUsage(t *testing.T) { tree, err := dwarf.NewDWARFTree("../../testdata/basic.o") if err != nil { diff --git a/go.mod b/go.mod index ccce850..3776b47 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,13 @@ module github.com/cilium/stackwhere go 1.25.0 require ( - github.com/davecgh/go-spew v1.1.1 + github.com/cilium/ebpf v0.21.0 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/spf13/cobra v1.10.2 ) require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.9 // indirect + golang.org/x/sys v0.37.0 // indirect ) diff --git a/go.sum b/go.sum index 577cf0d..e010cc9 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,28 @@ +github.com/cilium/ebpf v0.21.0 h1:4dpx1J/B/1apeTmWBH5BkVLayHTkFrMovVPnHEk+l3k= +github.com/cilium/ebpf v0.21.0/go.mod h1:1kHKv6Kvh5a6TePP5vvvoMa1bclRyzUXELSs272fmIQ= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s= +github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/dwarf/loclist.go b/internal/dwarf/loclist.go index 1ccc4ca..e3a71f9 100644 --- a/internal/dwarf/loclist.go +++ b/internal/dwarf/loclist.go @@ -175,6 +175,7 @@ func NewLoclistTable(f *elf.File) (*LoclistTable, error) { } if f.Class != elf.ELFCLASS64 { + // Code should work, but untested return nil, fmt.Errorf("unexpected 32-bit ELF file") } diff --git a/internal/dwarf/rangelist.go b/internal/dwarf/rangelist.go new file mode 100644 index 0000000..e63df32 --- /dev/null +++ b/internal/dwarf/rangelist.go @@ -0,0 +1,276 @@ +package dwarf + +import ( + "bytes" + "debug/elf" + "encoding/binary" + "errors" + "fmt" + "io" + "unsafe" + + "github.com/cilium/stackwhere/internal/dwarf/leb128" +) + +type rangeListHeader struct { + unitLength uint64 + version uint16 + addrSize uint8 + segmentSelectorSize uint8 + offsetEntryCount uint32 +} + +type RangeListTable struct { + hdr rangeListHeader + offsets []uint64 + entries map[uint64]RangeListEntry +} + +type RangeListEntry struct { + BaseAddressIdx uint64 + Ranges []Range +} + +type Range struct { + Start uint64 + End uint64 +} + +type rangelistDescriptorCode byte + +const ( + DW_RLE_end_of_list rangelistDescriptorCode = 0x00 + DW_RLE_base_addressx rangelistDescriptorCode = 0x01 + // DW_RLE_startx_endx = 0x02 + // DW_RLE_startx_length = 0x03 + DW_RLE_offset_pair rangelistDescriptorCode = 0x04 + // DW_RLE_base_address = 0x05 + // DW_RLE_start_end = 0x06 + // DW_RLE_start_length = 0x07 +) + +func NewRangeListTable(f *elf.File) (*RangeListTable, error) { + sec := f.Section(".debug_rnglists") + if sec == nil { + return nil, nil + } + + if f.Class != elf.ELFCLASS64 { + // Code should work, but untested + return nil, fmt.Errorf("unexpected 32-bit ELF file") + } + + b, err := sec.Data() + if err != nil { + return nil, err + } + + r := bytes.NewReader(b) + + list := RangeListTable{ + entries: make(map[uint64]RangeListEntry), + } + + debugAddrs, err := readDebugAddrEntries(f) + if err != nil { + return nil, fmt.Errorf("failed to read .debug_addr: %w", err) + } + + var ( + ulen uint32 + _32bit bool + ) + + if err := binary.Read(r, f.ByteOrder, &ulen); err != nil { + return nil, err + } + if ulen == 0xffffffff { + var ulen64 uint64 + if err := binary.Read(r, f.ByteOrder, &ulen64); err != nil { + return nil, err + } + list.hdr.unitLength = ulen64 + _32bit = false + } else { + list.hdr.unitLength = uint64(ulen) + _32bit = true + } + + if err := binary.Read(r, f.ByteOrder, &list.hdr.version); err != nil { + return nil, err + } + if err := binary.Read(r, f.ByteOrder, &list.hdr.addrSize); err != nil { + return nil, err + } + if err := binary.Read(r, f.ByteOrder, &list.hdr.segmentSelectorSize); err != nil { + return nil, err + } + if err := binary.Read(r, f.ByteOrder, &list.hdr.offsetEntryCount); err != nil { + return nil, err + } + + off := uint64(12) + if !_32bit { + off += 8 + } + + if _32bit { + offsets := make([]uint32, list.hdr.offsetEntryCount) + if err := binary.Read(r, f.ByteOrder, &offsets); err != nil { + return nil, err + } + off += uint64(uint64(unsafe.Sizeof(uint32(0))) * uint64(list.hdr.offsetEntryCount)) + + list.offsets = make([]uint64, len(offsets)) + for i, off := range offsets { + list.offsets[i] = uint64(off) + } + } else { + offsets := make([]uint64, list.hdr.offsetEntryCount) + if err := binary.Read(r, f.ByteOrder, &offsets); err != nil { + return nil, err + } + off += uint64(uint64(unsafe.Sizeof(uint64(0))) * uint64(list.hdr.offsetEntryCount)) + list.offsets = offsets + } + +loop: + for { + entryOff := off + var entry RangeListEntry + currentBase := uint64(0) + + for { + descriptorByte, err := r.ReadByte() + if err != nil { + if errors.Is(err, io.EOF) { + break loop + } + + return nil, err + } + off++ + + switch rangelistDescriptorCode(descriptorByte) { + case DW_RLE_end_of_list: + list.entries[entryOff] = entry + continue loop + case DW_RLE_base_addressx: + var idx uint64 + var l uint32 + idx, l, err = leb128.DecodeUnsigned(r) + if err != nil { + return nil, fmt.Errorf("error parsing base addressx entry: %w", err) + } + off += uint64(l) + entry.BaseAddressIdx = idx + if idx < uint64(len(debugAddrs)) { + currentBase = debugAddrs[idx] + } + case DW_RLE_offset_pair: + var rng Range + var l uint32 + var startOff, endOff uint64 + startOff, l, err = leb128.DecodeUnsigned(r) + if err != nil { + return nil, fmt.Errorf("error parsing offset pair entry: %w", err) + } + off += uint64(l) + + endOff, l, err = leb128.DecodeUnsigned(r) + if err != nil { + return nil, fmt.Errorf("error parsing offset pair entry: %w", err) + } + off += uint64(l) + + rng.Start = currentBase + startOff + rng.End = currentBase + endOff + entry.Ranges = append(entry.Ranges, rng) + default: + return nil, fmt.Errorf("unsupported rangelist descriptor code: %x", descriptorByte) + } + } + } + + return &list, nil +} + +// readDebugAddrEntries reads the address entries from the .debug_addr section. +// It returns a slice of addresses indexed by their position in the table, +// or nil if the section is absent. +func readDebugAddrEntries(f *elf.File) ([]uint64, error) { + sec := f.Section(".debug_addr") + if sec == nil { + return nil, nil + } + + b, err := sec.Data() + if err != nil { + return nil, err + } + + r := bytes.NewReader(b) + + // Parse DWARF unit_length to determine 32-bit vs 64-bit DWARF format. + var ulen uint32 + if err := binary.Read(r, f.ByteOrder, &ulen); err != nil { + return nil, fmt.Errorf("reading .debug_addr unit_length: %w", err) + } + + hdrSize := 8 // 32-bit DWARF: 4 (unit_length) + 2 (version) + 1 (addr_size) + 1 (segment_selector_size) + if ulen == 0xffffffff { + // 64-bit DWARF format: skip the 8-byte extended length. + var ulen64 uint64 + if err := binary.Read(r, f.ByteOrder, &ulen64); err != nil { + return nil, fmt.Errorf("reading .debug_addr extended unit_length: %w", err) + } + hdrSize = 16 // 4 + 8 + 2 + 1 + 1 + } + + var version uint16 + if err := binary.Read(r, f.ByteOrder, &version); err != nil { + return nil, fmt.Errorf("reading .debug_addr version: %w", err) + } + + var addrSize uint8 + if err := binary.Read(r, f.ByteOrder, &addrSize); err != nil { + return nil, fmt.Errorf("reading .debug_addr addr_size: %w", err) + } + + // Skip segment_selector_size. + if _, err := r.ReadByte(); err != nil { + return nil, fmt.Errorf("reading .debug_addr segment_selector_size: %w", err) + } + + if addrSize == 0 { + return nil, nil + } + + remaining := len(b) - hdrSize + if remaining <= 0 { + return nil, nil + } + + count := remaining / int(addrSize) + addrs := make([]uint64, count) + for i := range addrs { + switch addrSize { + case 8: + var addr uint64 + if err := binary.Read(r, f.ByteOrder, &addr); err != nil { + return nil, fmt.Errorf("reading .debug_addr entry: %w", err) + } + addrs[i] = addr + case 4: + var addr uint32 + if err := binary.Read(r, f.ByteOrder, &addr); err != nil { + return nil, fmt.Errorf("reading .debug_addr entry: %w", err) + } + addrs[i] = uint64(addr) + default: + return nil, fmt.Errorf("unsupported .debug_addr address size: %d", addrSize) + } + } + + return addrs, nil +} diff --git a/internal/dwarf/reexport.go b/internal/dwarf/reexport.go index bc3626a..0d50e58 100644 --- a/internal/dwarf/reexport.go +++ b/internal/dwarf/reexport.go @@ -8,10 +8,16 @@ import ( // the debug/dwarf package in those packages. var ( - AttrLocation = dbgDwarf.AttrLocation - AttrName = dbgDwarf.AttrName - AttrInline = dbgDwarf.AttrInline - AttrType = dbgDwarf.AttrType + AttrLocation = dbgDwarf.AttrLocation + AttrName = dbgDwarf.AttrName + AttrInline = dbgDwarf.AttrInline + AttrType = dbgDwarf.AttrType + AttrLowpc = dbgDwarf.AttrLowpc + AttrHighpc = dbgDwarf.AttrHighpc + AttrRanges = dbgDwarf.AttrRanges + AttrCallFile = dbgDwarf.AttrCallFile + AttrCallLine = dbgDwarf.AttrCallLine + AttrCallColumn = dbgDwarf.AttrCallColumn ) var ( diff --git a/internal/dwarf/tree.go b/internal/dwarf/tree.go index 3cba7d9..f84f937 100644 --- a/internal/dwarf/tree.go +++ b/internal/dwarf/tree.go @@ -40,7 +40,12 @@ func newDWARFTreeReader(fileReader io.ReaderAt) (*Tree, error) { return nil, fmt.Errorf("failed to create loclist table: %w", err) } - tree := newTree(llt) + rlt, err := NewRangeListTable(obj) + if err != nil { + return nil, fmt.Errorf("failed to create range list table: %w", err) + } + + tree := newTree(llt, rlt) var cur *Node r := dbg.Reader() @@ -91,14 +96,16 @@ type Tree struct { files []*dwarf.LineFile llt *LoclistTable + rlt *RangeListTable } -func newTree(llt *LoclistTable) *Tree { +func newTree(llt *LoclistTable, rlt *RangeListTable) *Tree { return &Tree{ index: make(map[dwarf.Offset]*Node), byType: make(map[dwarf.Tag][]*Node), files: nil, llt: llt, + rlt: rlt, } } @@ -336,13 +343,40 @@ func (n *Node) Type() *Node { return nil } +func (n *Node) Ranges() (RangeListEntry, error) { + ranges := n.Entry().Val(dwarf.AttrRanges) + if ranges == nil { + // Fall back to the abstract origin when the concrete node has no ranges. + if abstractOrigin := n.AbstractOrigin(); abstractOrigin != nil { + return abstractOrigin.Ranges() + } + return RangeListEntry{}, nil + } + + if n.tree.rlt == nil { + return RangeListEntry{}, fmt.Errorf("DW_AT_ranges present but no .debug_rnglists section") + } + + rangesOffset, ok := ranges.(uint64) + if !ok { + return RangeListEntry{}, fmt.Errorf("unexpected type for DW_AT_ranges value: %T", ranges) + } + + rangesEntry, ok := n.tree.rlt.entries[rangesOffset] + if !ok { + return RangeListEntry{}, fmt.Errorf("invalid ranges offset: %#x", rangesOffset) + } + + return rangesEntry, nil +} + // Returns the file and line number of this entry, or an empty string if there is no file and line number. -func (n *Node) FileCol() string { +func (n *Node) FileLineCol() string { fileIndex := n.Entry().Val(dwarf.AttrDeclFile) if fileIndex == nil { abstractOrigin := n.AbstractOrigin() if abstractOrigin != nil { - return abstractOrigin.FileCol() + return abstractOrigin.FileLineCol() } return "" @@ -362,6 +396,31 @@ func (n *Node) FileCol() string { return file.Name } +func (n *Node) CallFileLineCol() string { + callFileIndex := n.Entry().Val(dwarf.AttrCallFile) + if callFileIndex == nil { + abstractOrigin := n.AbstractOrigin() + if abstractOrigin != nil { + return abstractOrigin.CallFileLineCol() + } + + return "" + } + file := n.tree.files[callFileIndex.(int64)] + + callLine := n.Entry().Val(dwarf.AttrCallLine) + if callLine != nil { + callCol := n.Entry().Val(dwarf.AttrCallColumn) + if callCol != nil { + return fmt.Sprintf("%s:%d:%d", file.Name, callLine.(int64), callCol.(int64)) + } + + return fmt.Sprintf("%s:%d", file.Name, callLine.(int64)) + } + + return file.Name +} + func VisitPrefixOrder(n *Node, f func(*Node)) { f(n) for _, c := range n.children { diff --git a/testdata/Makefile b/testdata/Makefile index 46dc713..20899d1 100644 --- a/testdata/Makefile +++ b/testdata/Makefile @@ -18,7 +18,8 @@ docker: $(CONTAINER_ENGINE) run --rm -v $(shell pwd):/src -w /src $(IMAGE):$(VERSION) make all TARGETS := \ - basic + basic \ + spill .PHONY: all all: $(addsuffix .o,$(TARGETS)) diff --git a/testdata/spill.c b/testdata/spill.c new file mode 100644 index 0000000..30908bc --- /dev/null +++ b/testdata/spill.c @@ -0,0 +1,46 @@ +#define __section(X) __attribute__((section(X), used)) +#define __always_inline inline __attribute__((always_inline)) + +#define __uint(name, val) int (*name)[val] +#define __type(name, val) typeof(val) *name + +#define BPF_MAP_TYPE_ARRAY 1 + +struct +{ + __uint(type, BPF_MAP_TYPE_ARRAY); + __type(key, int); + __type(value, int); + __uint(max_entries, 16); +} example_map __section(".maps"); + +static void *(*const bpf_map_lookup_elem)(void *map, const void *key) = (void *)1; +static int (*const bpf_get_prandom_u32)(void) = (void *)7; + +extern void use_int(int); + +__always_inline int lookup_example_map(int key) +{ + int *value = bpf_map_lookup_elem(&example_map, &key); + if (value) + return *value; + return -1; +} + +__section("tc") int cil_entry(void *ctx) +{ + // Get a value, unknown at compile time, so cannot be optimized away. + int a = bpf_get_prandom_u32(); + // Use in external function, this prevents the compiler from reordering, side effects unknown. + use_int(a); + // Lookup map value, which forces `key` to be stored on the stack + int b = lookup_example_map(0); + // Use the value to prevent reordering. + use_int(b); + // Clobber all registers, forcing spilling around this point. + asm volatile("" ::: "r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9"); + // Use A separately, otherwise "a+b" would be calculated before the clobber and its result be stored in the stack. + use_int(a); + // Now use both + return a + b; +} diff --git a/testdata/spill.o b/testdata/spill.o new file mode 100644 index 0000000000000000000000000000000000000000..d4b9c11777149a0f1f8da3eea9e91dd67a9d9b64 GIT binary patch literal 5424 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