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 0000000..d4b9c11 Binary files /dev/null and b/testdata/spill.o differ