From 1bdc29e27220001411d94c7a70b3f45b511246ff Mon Sep 17 00:00:00 2001 From: Dale Hamel Date: Fri, 15 Aug 2025 16:31:10 -0400 Subject: [PATCH 01/14] WIP to load struct info from DWARF with minimal memory overhead --- libpf/pfelf/dwarf.go | 188 +++++++++++++++++++++++++++++++++++++++++++ libpf/pfelf/file.go | 7 ++ test.go | 39 +++++++++ 3 files changed, 234 insertions(+) create mode 100644 libpf/pfelf/dwarf.go create mode 100644 test.go diff --git a/libpf/pfelf/dwarf.go b/libpf/pfelf/dwarf.go new file mode 100644 index 000000000..a1e17cf64 --- /dev/null +++ b/libpf/pfelf/dwarf.go @@ -0,0 +1,188 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// package pfelf implements functions for processing of ELF files and extracting data from +// them. This file implements an interface to read struct sizes and field offsets +// from DWARF if it is present. + +package pfelf // import "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" + +import ( + "debug/dwarf" + //"debug/elf" + "fmt" + "os" + //"io" + "strings" + "slices" +) + +type structData struct { + name string + structTypeInfo *dwarf.StructType +} + +func (data structData) String() string { + var str string = "" + + str += fmt.Sprintf("\nstruct %s {\n", data.name) + + // Calculate size based on fields + calculatedSize := int64(0) + hasFlexibleArray := false + var flexArrayOffset int64 = 0 + + // Print field information + for i, field := range data.structTypeInfo.Field { + fieldType := field.Type.String() + // Clean up the type string a bit + fieldType = strings.TrimPrefix(fieldType, "struct ") + + str += fmt.Sprintf(" %s %s; // offset: %d, size: %d", + fieldType, field.Name, field.ByteOffset, field.Type.Size()) + + // Check if this might be a flexible array member + isLastField := i == len(data.structTypeInfo.Field)-1 + isArrayType := strings.HasSuffix(fieldType, "[]") || + strings.Contains(fieldType, "[0]") || + strings.Contains(fieldType, "FLEX_ARY") + hasArrayName := strings.HasSuffix(field.Name, "_array") || + strings.HasSuffix(field.Name, "_part") + + if isLastField && (isArrayType || hasArrayName) { + str += fmt.Sprintf(" (flexible array member)") + hasFlexibleArray = true + flexArrayOffset = field.ByteOffset + } + str += "\n" + + // Update calculated size + fieldEnd := field.ByteOffset + field.Type.Size() + if fieldEnd > calculatedSize { + calculatedSize = fieldEnd + } + } + + // Get the struct size from DWARF + dwarfSize := data.structTypeInfo.ByteSize + + // Use calculated size if DWARF size is 0 or negative + reportedSize := dwarfSize + if dwarfSize <= 0 || (hasFlexibleArray && dwarfSize < flexArrayOffset) { + reportedSize = calculatedSize + } + + str += fmt.Sprintf("} // total size: %d bytes", reportedSize) + if hasFlexibleArray { + str += fmt.Sprintf(" (base size without flexible array)") + } + if dwarfSize != reportedSize { + str += fmt.Sprintf(" (DWARF: %d, calculated: %d)", dwarfSize, calculatedSize) + } + str += "\n" + + return str +} + +func loadStructData(debugInfo, debugAbbrev, debugStr, debugLineStr *Section, names []string) ([]structData, error) { + results := []structData{} + + // To reduce memory usage, we will use the Section's Data() accessor to + // get a memory mapped "subslice" and avoid allocations + // This prevents a substantial amount of memory bloat that elf.File's DWARF() accessor + // otherwise incurs + abbrevData, err := debugAbbrev.Data(maxBytesLargeSection) + if err != nil { + return nil, err + } + + debugData, err := debugInfo.Data(maxBytesLargeSection) + if err != nil { + return nil, err + } + + strData, err := debugStr.Data(maxBytesLargeSection) + if err != nil { + return nil, err + } + + lineStrData, err := debugLineStr.Data(maxBytesLargeSection) + if err != nil { + return nil, err + } + + // Directly construct the DWARF file from the memory mapped slices above + dwarfData, err := dwarf.New(abbrevData, nil, nil, debugData, nil, nil, nil, strData) + if err != nil { + return nil, err + } + + // This section is required to be able to decode + if err := dwarfData.AddSection(".debug_line_str", lineStrData); err != nil { + return nil, err + } + + processedStructs := make(map[string]struct{}) + + reader := dwarfData.Reader() + for { + // return early if we have all of the structs that were asked for + if len(processedStructs) == len(names) { + return results, nil + } + + entry, err := reader.Next() + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading DWARF entry: %v\n", err) + break + } + if entry == nil { + break + } + + // Look for struct type entries + if entry.Tag == dwarf.TagStructType { + nameVal, ok := entry.Val(dwarf.AttrName).(string) + if !ok { + continue // Skip unnamed structs + } + name := nameVal + + if _, ok := processedStructs[name]; ok { + continue + } + + // Ignore structs that weren't asked for + if !slices.Contains(names, name) { + continue + } + + // Get the type information + structType, err := dwarfData.Type(entry.Offset) + if err != nil { + fmt.Printf("Warning: Error getting type info for %s: %v\n", name, err) + continue + } + + // Type assertion to get the struct type + structTypeInfo, ok := structType.(*dwarf.StructType) + if !ok { + continue + } + + // Skip incomplete structs, try another compilation unit + if structTypeInfo.Incomplete { + continue + } + + results = append(results, structData{ + name: name, + structTypeInfo: structTypeInfo, + }) + + processedStructs[name] = struct{}{} + } + } + + return results, nil +} diff --git a/libpf/pfelf/file.go b/libpf/pfelf/file.go index b75c60429..e9f8cc8f1 100644 --- a/libpf/pfelf/file.go +++ b/libpf/pfelf/file.go @@ -504,6 +504,13 @@ func (f *File) SymbolData(name libpf.SymbolName, maxCopy int) (*libpf.Symbol, [] return sym, data, err } + +// Gets StructData for specific structs from DWARF, without loading and parsing +// the whole thing +func (f *File) StructData (names []string) ([]structData, error) { + return loadStructData(f.Section(".debug_info"), f.Section(".debug_abbrev"), f.Section(".debug_str"), f.Section(".debug_line_str"), names) +} + // ReadVirtualMemory reads bytes from given virtual address func (f *File) ReadVirtualMemory(p []byte, addr int64) (int, error) { if len(p) == 0 { diff --git a/test.go b/test.go new file mode 100644 index 000000000..12d862b05 --- /dev/null +++ b/test.go @@ -0,0 +1,39 @@ +package main + + +import ( + "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" + "os" + "fmt" +) + + +func main() { + if len(os.Args) < 2 { + fmt.Fprintf(os.Stderr, "Usage: %s [struct-name]\n", os.Args[0]) + os.Exit(1) + } + + elfFile := os.Args[1] + specificStructs := []string{} + if len(os.Args) > 2 { + specificStructs = os.Args[2:] + } + + pf, err := pfelf.Open(elfFile) + if err != nil { + fmt.Fprintf(os.Stderr, "Error opening ELF file for %s: %v\n", specificStructs, err) + os.Exit(1) + } + defer pf.Close() + + data, err := pf.StructData(specificStructs) + if err != nil { + fmt.Fprintf(os.Stderr, "Error opening ELF file: %v\n", err) + os.Exit(1) + } + + for _, s := range data { + fmt.Printf("%s\n", s) + } +} From 91e9f80abe88e18a7597300205204f93fb4f44de Mon Sep 17 00:00:00 2001 From: Dale Hamel Date: Sun, 17 Aug 2025 19:17:10 -0400 Subject: [PATCH 02/14] Refactor into actual module with a basic test --- interpreter/ruby/ruby.go | 216 +++++++++++++++++++++++--------------- libpf/pfelf/dwarf.go | 112 ++++++++++++++------ libpf/pfelf/dwarf_test.go | 97 +++++++++++++++++ test.go | 1 + 4 files changed, 308 insertions(+), 118 deletions(-) create mode 100644 libpf/pfelf/dwarf_test.go diff --git a/interpreter/ruby/ruby.go b/interpreter/ruby/ruby.go index e3464353c..215acb40e 100644 --- a/interpreter/ruby/ruby.go +++ b/interpreter/ruby/ruby.go @@ -233,6 +233,49 @@ func (r *rubyData) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, bias libp func (r *rubyData) Unload(_ interpreter.EbpfHandler) { } +func (r *rubyData) calculateStructsFromDWARF(ef *pfelf.File) error { + + + //Vm_stack: r.vmStructs.execution_context_struct.vm_stack, + //Vm_stack_size: r.vmStructs.execution_context_struct.vm_stack_size, + //Cfp: r.vmStructs.execution_context_struct.cfp, + + //Pc: r.vmStructs.control_frame_struct.pc, + //Iseq: r.vmStructs.control_frame_struct.iseq, + //Ep: r.vmStructs.control_frame_struct.ep, + //Size_of_control_frame_struct: r.vmStructs.control_frame_struct.size_of_control_frame_struct, + + //Body: r.vmStructs.iseq_struct.body, + + //Iseq_size: r.vmStructs.iseq_constant_body.size, + //Iseq_encoded: r.vmStructs.iseq_constant_body.encoded, + + //Size_of_value: r.vmStructs.size_of_value, + + //Running_ec: r.vmStructs.rb_ractor_struct.running_ec, + + referenced_ruby_structs := []string{ + "rb_execution_context_struct", + "rb_control_frame_struct", + "rb_iseq_struct", + "rb_iseq_constant_body", + "rb_iseq_location_struct", + "iseq_insn_info_entry", + "RString", + "RArray", + "succ_index_table", + "succ_dict_block", + } + struct_info, err := ef.StructData() + if err != nil { + return err + } + + // TODO copy the values to populate vmstructs + + return nil +} + // rubyIseqBodyPC holds a reported address to a iseq_constant_body and Ruby VM program counter // combination and is used as key in the cache. type rubyIseqBodyPC struct { @@ -766,6 +809,7 @@ func determineRubyVersion(ef *pfelf.File) (uint32, error) { return rubyVersion(uint32(major), uint32(minor), uint32(release)), nil } + func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) { if !rubyRegex.MatchString(info.FileName()) { return nil, nil @@ -830,94 +874,96 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr vms := &rid.vmStructs - // Ruby does not provide introspection data, hard code the struct field offsets. Some - // values can be fairly easily calculated from the struct definitions, but some are - // looked up by using gdb and getting the field offset directly from debug data. - vms.execution_context_struct.vm_stack = 0 - vms.execution_context_struct.vm_stack_size = 8 - vms.execution_context_struct.cfp = 16 - - vms.control_frame_struct.pc = 0 - vms.control_frame_struct.iseq = 16 - vms.control_frame_struct.ep = 32 - switch { - case version < rubyVersion(2, 6, 0): - vms.control_frame_struct.size_of_control_frame_struct = 48 - case version < rubyVersion(3, 1, 0): - // With Ruby 2.6 the field bp was added to rb_control_frame_t - // https://github.com/ruby/ruby/commit/ed935aa5be0e5e6b8d53c3e7d76a9ce395dfa18b - vms.control_frame_struct.size_of_control_frame_struct = 56 - default: - // 3.1 adds new jit_return field at the end. - // https://github.com/ruby/ruby/commit/9d8cc01b758f9385bd4c806f3daff9719e07faa0 - vms.control_frame_struct.size_of_control_frame_struct = 64 - } - vms.iseq_struct.body = 16 - - vms.iseq_constant_body.iseq_type = 0 - vms.iseq_constant_body.size = 4 - vms.iseq_constant_body.encoded = 8 - vms.iseq_constant_body.location = 64 - switch { - case version < rubyVersion(2, 6, 0): - vms.iseq_constant_body.insn_info_body = 112 - vms.iseq_constant_body.insn_info_size = 200 - vms.iseq_constant_body.succ_index_table = 144 - vms.iseq_constant_body.size_of_iseq_constant_body = 288 - case version < rubyVersion(3, 2, 0): - vms.iseq_constant_body.insn_info_body = 120 - vms.iseq_constant_body.insn_info_size = 136 - vms.iseq_constant_body.succ_index_table = 144 - vms.iseq_constant_body.size_of_iseq_constant_body = 312 - default: - vms.iseq_constant_body.insn_info_body = 112 - vms.iseq_constant_body.insn_info_size = 128 - vms.iseq_constant_body.succ_index_table = 136 - vms.iseq_constant_body.size_of_iseq_constant_body = 320 - } - vms.iseq_location_struct.pathobj = 0 - vms.iseq_location_struct.base_label = 8 - - switch { - case version < rubyVersion(2, 6, 0): - vms.iseq_insn_info_entry.position = 0 - vms.iseq_insn_info_entry.size_of_position = 4 - vms.iseq_insn_info_entry.line_no = 4 - vms.iseq_insn_info_entry.size_of_line_no = 4 - vms.iseq_insn_info_entry.size_of_iseq_insn_info_entry = 12 - case version < rubyVersion(3, 1, 0): - // The position field was removed from this struct with - // https://github.com/ruby/ruby/commit/295838e6eb1d063c64f7cde5bbbd13c7768908fd - vms.iseq_insn_info_entry.position = 0 - vms.iseq_insn_info_entry.size_of_position = 0 - vms.iseq_insn_info_entry.line_no = 0 - vms.iseq_insn_info_entry.size_of_line_no = 4 - vms.iseq_insn_info_entry.size_of_iseq_insn_info_entry = 8 - default: - // https://github.com/ruby/ruby/commit/0a36cab1b53646062026c3181117fad73802baf4 - vms.iseq_insn_info_entry.position = 0 - vms.iseq_insn_info_entry.size_of_position = 0 - vms.iseq_insn_info_entry.line_no = 0 - vms.iseq_insn_info_entry.size_of_line_no = 4 - vms.iseq_insn_info_entry.size_of_iseq_insn_info_entry = 12 - } - if version < rubyVersion(3, 2, 0) { - vms.rstring_struct.as_ary = 16 - } else { - vms.rstring_struct.as_ary = 24 - } - vms.rstring_struct.as_heap_ptr = 24 + if err := calculateStructsFromDWARF(); err != nil { + // Ruby does not provide introspection data, hard code the struct field offsets. Some + // values can be fairly easily calculated from the struct definitions, but some are + // looked up by using gdb and getting the field offset directly from debug data. + vms.execution_context_struct.vm_stack = 0 + vms.execution_context_struct.vm_stack_size = 8 + vms.execution_context_struct.cfp = 16 + + vms.control_frame_struct.pc = 0 + vms.control_frame_struct.iseq = 16 + vms.control_frame_struct.ep = 32 + switch { + case version < rubyVersion(2, 6, 0): + vms.control_frame_struct.size_of_control_frame_struct = 48 + case version < rubyVersion(3, 1, 0): + // With Ruby 2.6 the field bp was added to rb_control_frame_t + // https://github.com/ruby/ruby/commit/ed935aa5be0e5e6b8d53c3e7d76a9ce395dfa18b + vms.control_frame_struct.size_of_control_frame_struct = 56 + default: + // 3.1 adds new jit_return field at the end. + // https://github.com/ruby/ruby/commit/9d8cc01b758f9385bd4c806f3daff9719e07faa0 + vms.control_frame_struct.size_of_control_frame_struct = 64 + } + vms.iseq_struct.body = 16 + + vms.iseq_constant_body.iseq_type = 0 + vms.iseq_constant_body.size = 4 + vms.iseq_constant_body.encoded = 8 + vms.iseq_constant_body.location = 64 + switch { + case version < rubyVersion(2, 6, 0): + vms.iseq_constant_body.insn_info_body = 112 + vms.iseq_constant_body.insn_info_size = 200 + vms.iseq_constant_body.succ_index_table = 144 + vms.iseq_constant_body.size_of_iseq_constant_body = 288 + case version < rubyVersion(3, 2, 0): + vms.iseq_constant_body.insn_info_body = 120 + vms.iseq_constant_body.insn_info_size = 136 + vms.iseq_constant_body.succ_index_table = 144 + vms.iseq_constant_body.size_of_iseq_constant_body = 312 + default: + vms.iseq_constant_body.insn_info_body = 112 + vms.iseq_constant_body.insn_info_size = 128 + vms.iseq_constant_body.succ_index_table = 136 + vms.iseq_constant_body.size_of_iseq_constant_body = 320 + } + vms.iseq_location_struct.pathobj = 0 + vms.iseq_location_struct.base_label = 8 + + switch { + case version < rubyVersion(2, 6, 0): + vms.iseq_insn_info_entry.position = 0 + vms.iseq_insn_info_entry.size_of_position = 4 + vms.iseq_insn_info_entry.line_no = 4 + vms.iseq_insn_info_entry.size_of_line_no = 4 + vms.iseq_insn_info_entry.size_of_iseq_insn_info_entry = 12 + case version < rubyVersion(3, 1, 0): + // The position field was removed from this struct with + // https://github.com/ruby/ruby/commit/295838e6eb1d063c64f7cde5bbbd13c7768908fd + vms.iseq_insn_info_entry.position = 0 + vms.iseq_insn_info_entry.size_of_position = 0 + vms.iseq_insn_info_entry.line_no = 0 + vms.iseq_insn_info_entry.size_of_line_no = 4 + vms.iseq_insn_info_entry.size_of_iseq_insn_info_entry = 8 + default: + // https://github.com/ruby/ruby/commit/0a36cab1b53646062026c3181117fad73802baf4 + vms.iseq_insn_info_entry.position = 0 + vms.iseq_insn_info_entry.size_of_position = 0 + vms.iseq_insn_info_entry.line_no = 0 + vms.iseq_insn_info_entry.size_of_line_no = 4 + vms.iseq_insn_info_entry.size_of_iseq_insn_info_entry = 12 + } + if version < rubyVersion(3, 2, 0) { + vms.rstring_struct.as_ary = 16 + } else { + vms.rstring_struct.as_ary = 24 + } + vms.rstring_struct.as_heap_ptr = 24 - vms.rarray_struct.as_ary = 16 - vms.rarray_struct.as_heap_ptr = 32 + vms.rarray_struct.as_ary = 16 + vms.rarray_struct.as_heap_ptr = 32 - vms.succ_index_table_struct.small_block_ranks = 8 - vms.succ_index_table_struct.block_bits = 16 - vms.succ_index_table_struct.succ_part = 48 - vms.succ_index_table_struct.size_of_succ_dict_block = 80 - vms.size_of_immediate_table = 54 + vms.succ_index_table_struct.small_block_ranks = 8 + vms.succ_index_table_struct.block_bits = 16 + vms.succ_index_table_struct.succ_part = 48 + vms.succ_index_table_struct.size_of_succ_dict_block = 80 + vms.size_of_immediate_table = 54 - vms.size_of_value = 8 + vms.size_of_value = 8 + } if version >= rubyVersion(3, 0, 0) { if runtime.GOARCH == "amd64" { diff --git a/libpf/pfelf/dwarf.go b/libpf/pfelf/dwarf.go index a1e17cf64..fdfa62682 100644 --- a/libpf/pfelf/dwarf.go +++ b/libpf/pfelf/dwarf.go @@ -9,10 +9,8 @@ package pfelf // import "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" import ( "debug/dwarf" - //"debug/elf" "fmt" "os" - //"io" "strings" "slices" ) @@ -27,13 +25,8 @@ func (data structData) String() string { str += fmt.Sprintf("\nstruct %s {\n", data.name) - // Calculate size based on fields - calculatedSize := int64(0) - hasFlexibleArray := false - var flexArrayOffset int64 = 0 - // Print field information - for i, field := range data.structTypeInfo.Field { + for _, field := range data.structTypeInfo.Field { fieldType := field.Type.String() // Clean up the type string a bit fieldType = strings.TrimPrefix(fieldType, "struct ") @@ -41,49 +34,102 @@ func (data structData) String() string { str += fmt.Sprintf(" %s %s; // offset: %d, size: %d", fieldType, field.Name, field.ByteOffset, field.Type.Size()) - // Check if this might be a flexible array member - isLastField := i == len(data.structTypeInfo.Field)-1 - isArrayType := strings.HasSuffix(fieldType, "[]") || - strings.Contains(fieldType, "[0]") || - strings.Contains(fieldType, "FLEX_ARY") - hasArrayName := strings.HasSuffix(field.Name, "_array") || - strings.HasSuffix(field.Name, "_part") - - if isLastField && (isArrayType || hasArrayName) { - str += fmt.Sprintf(" (flexible array member)") - hasFlexibleArray = true - flexArrayOffset = field.ByteOffset - } + //// Check if this might be a flexible array member + //isLastField := i == len(data.structTypeInfo.Field)-1 + //isArrayType := strings.HasSuffix(fieldType, "[]") || + // strings.Contains(fieldType, "[0]") || + // strings.Contains(fieldType, "FLEX_ARY") + //hasArrayName := strings.HasSuffix(field.Name, "_array") || + // strings.HasSuffix(field.Name, "_part") + // + //if isLastField && (isArrayType || hasArrayName) { + // str += fmt.Sprintf(" (flexible array member)") + // hasFlexibleArray = true + // flexArrayOffset = field.ByteOffset + //} str += "\n" + } + + + str += fmt.Sprintf("} // total size: %d bytes", (&data).Size()) + //if hasFlexibleArray { + // str += fmt.Sprintf(" (base size without flexible array)") + //} + //if dwarfSize != reportedSize { + // str += fmt.Sprintf(" (DWARF: %d, calculated: %d)", dwarfSize, calculatedSize) + //} + str += "\n" + + return str +} + + +// TODO refactor the printing function code to actually return these, then use +// these functions in the String calc + +func (data *structData) FieldOffset(name string) (int64, error) { + field, err := data.field(name) + if err != nil { + return -1, err + } + return field.ByteOffset, nil +} + +func (data *structData) FieldSize(name string) (int64, error) { + field, err := data.field(name) + if err != nil { + return -1, err + } + return field.Type.Size(), nil +} + +func (data *structData) field(name string) (*dwarf.StructField, error) { + var found *dwarf.StructField = nil + + for _, field := range data.structTypeInfo.Field { + if field.Name == name { + found = field + break + } + } + + if found == nil { + return nil, fmt.Errorf("unable to locate struct field %s", name) + } + + return found, nil +} + +func (data *structData) Size() int64 { + // Calculate size based on fields + calculatedSize := int64(0) + //hasFlexibleArray := false + //var flexArrayOffset int64 = 0 + + // Print field information + for _, field := range data.structTypeInfo.Field { // Update calculated size fieldEnd := field.ByteOffset + field.Type.Size() if fieldEnd > calculatedSize { calculatedSize = fieldEnd } } - // Get the struct size from DWARF dwarfSize := data.structTypeInfo.ByteSize // Use calculated size if DWARF size is 0 or negative reportedSize := dwarfSize - if dwarfSize <= 0 || (hasFlexibleArray && dwarfSize < flexArrayOffset) { + if dwarfSize <= 0 { //|| (hasFlexibleArray && dwarfSize < flexArrayOffset) { // TODO verify if flex array offset logic actually needed reportedSize = calculatedSize } - - str += fmt.Sprintf("} // total size: %d bytes", reportedSize) - if hasFlexibleArray { - str += fmt.Sprintf(" (base size without flexible array)") - } - if dwarfSize != reportedSize { - str += fmt.Sprintf(" (DWARF: %d, calculated: %d)", dwarfSize, calculatedSize) - } - str += "\n" - return str + return reportedSize } + +// This accepts a list of names to look up, as we want to try and get "everything in one go", +// since DWARF is inherently O(n) to look up these symbols func loadStructData(debugInfo, debugAbbrev, debugStr, debugLineStr *Section, names []string) ([]structData, error) { results := []structData{} diff --git a/libpf/pfelf/dwarf_test.go b/libpf/pfelf/dwarf_test.go new file mode 100644 index 000000000..612f8f514 --- /dev/null +++ b/libpf/pfelf/dwarf_test.go @@ -0,0 +1,97 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package pfelf + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + //"go.opentelemetry.io/ebpf-profiler/testsupport" + //"go.opentelemetry.io/ebpf-profiler/libpf" +) + +// Probably don't need to redefine this since it is in the same package? +//func getPFELF(path string, t *testing.T) *File { +// file, err := Open(path) +// assert.NoError(t, err) +// return file +//} + +// TODO make a basic test suite that asserts struct sizes and offsets from a +// test file with DWARF info in the test data, add to existing makefile + +// TODO to start with, just point at the ruby test file and assert on some +// structs from there? +// Then for the next step, maybe copy the struct definitions from ruby to have +// a bunch of complex and realistic structs + + +func TestDWARFParseRubyStructs(t *testing.T) { + elfFile, err := Open("/home/dalehamel.linux/.rubies/ruby-3.4.4/bin/ruby") + require.NoError(t, err) + defer elfFile.Close() + + check_structs := []string{ + "rb_execution_context_struct", + "rb_control_frame_struct", + "rb_iseq_struct", + "rb_iseq_constant_body", + "rb_iseq_location_struct", + "iseq_insn_info_entry", + "RString", + "RArray", + "succ_index_table", + "succ_dict_block", + } + + offset_checks := map[string]map[string]int64{ + "rb_execution_context_struct": map[string]int64{ + "vm_stack": int64(0), + "cfp": int64(16), + }, + } + + size_checks := map[string]map[string]int64{ + "rb_execution_context_struct": map[string]int64{ + "vm_stack": int64(8), + }, + } + + + struct_data, err := elfFile.StructData(check_structs) + require.NoError(t, err) + + structs_by_name := map[string]structData{} + + for _, struct_info := range struct_data { + structs_by_name[struct_info.name] = struct_info + } + + assert.Equal(t, len(check_structs), len(struct_data)) + + for name, fields := range offset_checks { + struct_info, ok := structs_by_name[name] + require.True(t, ok) + + for field, expected_offset := range fields { + actual_offset, err := struct_info.FieldOffset(field) + assert.NoError(t, err) + + assert.Equal(t, expected_offset, actual_offset) + } + } + + for name, fields := range size_checks { + struct_info, ok := structs_by_name[name] + require.True(t, ok) + + for field, expected_size := range fields { + actual_size, err := struct_info.FieldSize(field) + assert.NoError(t, err) + + assert.Equal(t, expected_size, actual_size) + } + } +} diff --git a/test.go b/test.go index 12d862b05..c2374e9d0 100644 --- a/test.go +++ b/test.go @@ -7,6 +7,7 @@ import ( "fmt" ) +// /usr/bin/time -v go run test.go /home/dalehamel.linux/.rubies/ruby-3.4.4/bin/ruby rb_execution_context_struct rb_control_frame_struct rb_iseq_struct rb_iseq_constant_body rb_iseq_location_struct iseq_insn_info_entry RString RArray succ_dict_block succ_index_table func main() { if len(os.Args) < 2 { From 445bd3c962a2db4e2ee1c667c34e55f069d67864 Mon Sep 17 00:00:00 2001 From: Dale Hamel Date: Sun, 17 Aug 2025 21:00:16 -0400 Subject: [PATCH 03/14] Dwarf struct parsing tests --- libpf/pfelf/dwarf_test.go | 224 +++++++++++++++------- libpf/pfelf/testdata/.gitignore | 2 + libpf/pfelf/testdata/Makefile | 10 +- libpf/pfelf/testdata/dwarf_structs.c | 11 ++ libpf/pfelf/testdata/ruby_dwarf_structs.c | 18 ++ 5 files changed, 195 insertions(+), 70 deletions(-) create mode 100644 libpf/pfelf/testdata/dwarf_structs.c create mode 100644 libpf/pfelf/testdata/ruby_dwarf_structs.c diff --git a/libpf/pfelf/dwarf_test.go b/libpf/pfelf/dwarf_test.go index 612f8f514..b52e5d3a1 100644 --- a/libpf/pfelf/dwarf_test.go +++ b/libpf/pfelf/dwarf_test.go @@ -8,90 +8,176 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - //"go.opentelemetry.io/ebpf-profiler/testsupport" - //"go.opentelemetry.io/ebpf-profiler/libpf" ) -// Probably don't need to redefine this since it is in the same package? -//func getPFELF(path string, t *testing.T) *File { -// file, err := Open(path) -// assert.NoError(t, err) -// return file -//} - -// TODO make a basic test suite that asserts struct sizes and offsets from a -// test file with DWARF info in the test data, add to existing makefile - -// TODO to start with, just point at the ruby test file and assert on some -// structs from there? -// Then for the next step, maybe copy the struct definitions from ruby to have -// a bunch of complex and realistic structs - - -func TestDWARFParseRubyStructs(t *testing.T) { - elfFile, err := Open("/home/dalehamel.linux/.rubies/ruby-3.4.4/bin/ruby") - require.NoError(t, err) - defer elfFile.Close() - - check_structs := []string{ - "rb_execution_context_struct", - "rb_control_frame_struct", - "rb_iseq_struct", - "rb_iseq_constant_body", - "rb_iseq_location_struct", - "iseq_insn_info_entry", - "RString", - "RArray", - "succ_index_table", - "succ_dict_block", - } - - offset_checks := map[string]map[string]int64{ - "rb_execution_context_struct": map[string]int64{ - "vm_stack": int64(0), - "cfp": int64(16), +func TestDWARFParseStructs(t *testing.T) { + tests := []struct { + desc string + test_file string + expectedStructs []string + expectedStructSizes map[string]int64 + expectedFieldOffsets map[string]map[string]int64 + expectedFieldSizes map[string]map[string]int64 + }{ + { + desc: "it should be able to parse arbitrary structs", + test_file: "testdata/dwarf_structs", + expectedStructs : []string { + "some_struct", + }, + expectedStructSizes : map[string]int64{ + "some_struct" : 72, + }, + expectedFieldOffsets : map[string]map[string]int64{ + "some_struct": map[string]int64{ + "some_array": int64(0), + "some_int": int64(64), + }, + }, + expectedFieldSizes : map[string]map[string]int64{ + "some_struct": map[string]int64{ + "some_array": int64(64), + "some_int": int64(8), + }, + }, }, - } - - size_checks := map[string]map[string]int64{ - "rb_execution_context_struct": map[string]int64{ - "vm_stack": int64(8), + { +/* + 'execution_context_struct.vm_stack': offset_of('rb_execution_context_struct', 'vm_stack'), + 'execution_context_struct.vm_stack_size': offset_of('rb_execution_context_struct', 'vm_stack_size'), + 'execution_context_struct.cfp': offset_of('rb_execution_context_struct', 'cfp'), + + 'control_frame_struct.pc': offset_of('rb_control_frame_struct', 'pc'), + 'control_frame_struct.iseq': offset_of('rb_control_frame_struct', 'iseq'), + 'control_frame_struct.ep': offset_of('rb_control_frame_struct', 'ep'), + 'control_frame_struct.size_of_control_frame_struct': size_of('rb_control_frame_struct'), + + 'iseq_struct.body': offset_of('rb_iseq_struct', 'body'), + + 'iseq_constant_body.iseq_type': offset_of('rb_iseq_constant_body', 'type'), + 'iseq_constant_body.size': offset_of('rb_iseq_constant_body', 'iseq_size'), + 'iseq_constant_body.encoded': offset_of('rb_iseq_constant_body', 'iseq_encoded'), + 'iseq_constant_body.location': offset_of('rb_iseq_constant_body', 'location'), + 'iseq_constant_body.insn_info_body': offset_of('rb_iseq_constant_body', 'insns_info.body'), + 'iseq_constant_body.insn_info_size': offset_of('rb_iseq_constant_body', 'insns_info.size'), + 'iseq_constant_body.succ_index_table': offset_of('rb_iseq_constant_body', 'insns_info.succ_index_table'), + 'iseq_constant_body.size_of_iseq_constant_body': size_of('rb_iseq_constant_body'), + + 'iseq_location_struct.pathobj': offset_of('rb_iseq_location_struct', 'pathobj'), + 'iseq_location_struct.base_label': offset_of('rb_iseq_location_struct', 'base_label'), + + 'iseq_insn_info_entry.position': offset_of('iseq_insn_info_entry', 'position'), + 'iseq_insn_info_entry.size_of_position': size_of_field('iseq_insn_info_entry', 'position'), + 'iseq_insn_info_entry.line_no': offset_of('iseq_insn_info_entry', 'line_no'), + 'iseq_insn_info_entry.size_of_line_no': size_of_field('iseq_insn_info_entry', 'line_no'), + 'iseq_insn_info_entry.size_of_iseq_insn_info_entry': size_of('iseq_insn_info_entry'), + + 'rstring_struct.as_ary': offset_of('RString', 'as.embed.ary'), + 'rstring_struct.as_heap_ptr': offset_of('RString', 'as.heap.ptr'), + + 'rarray_struct.as_ary': offset_of('RArray', 'as.ary'), + 'rarray_struct.as_heap_ptr': offset_of('RArray', 'as.heap.ptr'), + + 'size_of_value': size_of('VALUE', ns=''), + + 'rb_ractor_struct.running_ec': offset_of('rb_ractor_struct', 'threads.running_ec'), +*/ + desc: "it should be able to parse complicated ruby structs", + test_file: "testdata/ruby_dwarf_structs", + expectedStructs : []string { + //"rb_execution_context_struct", + //"rb_control_frame_struct", + //"rb_iseq_struct", + //"rb_iseq_constant_body", + //"rb_iseq_location_struct", + //"iseq_insn_info_entry", + //"RString", + //"RArray", + "succ_index_table", + "succ_dict_block", + }, + expectedStructSizes : map[string]int64{ + "succ_dict_block" : 80, + }, + expectedFieldOffsets : map[string]map[string]int64{ + //"rb_execution_context_struct": map[string]int64{ + // "vm_stack": int64(0), + // "cfp": int64(16), + //}, + "succ_index_table": map[string]int64{ + "succ_part": int64(48), + }, + "succ_dict_block": map[string]int64{ + "small_block_ranks": int64(8), + "bits": int64(16), + }, + }, + expectedFieldSizes : map[string]map[string]int64{ + //"rb_execution_context_struct": map[string]int64{ + // "vm_stack": int64(8), + //}, + "succ_index_table": map[string]int64{ + "imm_part": int64(48), // note that in python they multiple this by 9, then divide by 8 https://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/078ae4d6ded761b513038440bc8525014fa6c016/tools/coredump/testsources/ruby/gdb-dump-offsets.py#L69 because of https://github.com/Shopify/ruby/blob/70b4b6fea0eeb66647539bcb3b9a50d027d92e51/iseq.c#L4265 + "succ_part": int64(0), + }, + }, }, } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + elfFile, err := Open(tt.test_file) + require.NoError(err) + defer elfFile.Close() - struct_data, err := elfFile.StructData(check_structs) - require.NoError(t, err) + struct_data, err := elfFile.StructData(tt.expectedStructs) + require.NoError(err) - structs_by_name := map[string]structData{} + assert.Equal(len(tt.expectedStructs), len(struct_data)) - for _, struct_info := range struct_data { - structs_by_name[struct_info.name] = struct_info - } + structs_by_name := map[string]structData{} - assert.Equal(t, len(check_structs), len(struct_data)) + for _, struct_info := range struct_data { + structs_by_name[struct_info.name] = struct_info + } - for name, fields := range offset_checks { - struct_info, ok := structs_by_name[name] - require.True(t, ok) + for _, name := range tt.expectedStructs { + _, ok := structs_by_name[name] + assert.True(ok) + } - for field, expected_offset := range fields { - actual_offset, err := struct_info.FieldOffset(field) - assert.NoError(t, err) + for name, expected_size := range tt.expectedStructSizes { + struct_info, ok := structs_by_name[name] + require.True(ok) - assert.Equal(t, expected_offset, actual_offset) - } - } + assert.Equal(expected_size, struct_info.Size()) + } + + for name, fields := range tt.expectedFieldOffsets { + struct_info, ok := structs_by_name[name] + require.True(ok) + + for field, expected_offset := range fields { + actual_offset, err := struct_info.FieldOffset(field) + assert.NoError(err) + + assert.Equal(expected_offset, actual_offset) + } + } - for name, fields := range size_checks { - struct_info, ok := structs_by_name[name] - require.True(t, ok) + for name, fields := range tt.expectedFieldSizes { + struct_info, ok := structs_by_name[name] + require.True(ok) - for field, expected_size := range fields { - actual_size, err := struct_info.FieldSize(field) - assert.NoError(t, err) + for field, expected_size := range fields { + actual_size, err := struct_info.FieldSize(field) + assert.NoError(err) - assert.Equal(t, expected_size, actual_size) - } + assert.Equal(expected_size, actual_size) + } + } + }) } } diff --git a/libpf/pfelf/testdata/.gitignore b/libpf/pfelf/testdata/.gitignore index 5025847f1..a4d6607c1 100644 --- a/libpf/pfelf/testdata/.gitignore +++ b/libpf/pfelf/testdata/.gitignore @@ -5,3 +5,5 @@ kernel-image ubuntu-kernel-image go-binary separate-debug-file +dwarf_structs +ruby_dwarf_structs diff --git a/libpf/pfelf/testdata/Makefile b/libpf/pfelf/testdata/Makefile index 757c2801a..63308db98 100644 --- a/libpf/pfelf/testdata/Makefile +++ b/libpf/pfelf/testdata/Makefile @@ -10,13 +10,21 @@ BINARIES=fixed-address \ the_notorious_build_id \ ubuntu-kernel-image \ with-debug-syms \ - without-debug-syms + without-debug-syms \ + dwarf_structs \ + ruby_dwarf_structs all: $(BINARIES) clean: rm -f $(BINARIES) +dwarf_structs: dwarf_structs.c + $(CC) $< -g -o $@ + +ruby_dwarf_structs: ruby_dwarf_structs.c + $(CC) $< -g -o $@ + with-debug-syms: test.c $(CC) $< -g -o $@ diff --git a/libpf/pfelf/testdata/dwarf_structs.c b/libpf/pfelf/testdata/dwarf_structs.c new file mode 100644 index 000000000..f706eaf72 --- /dev/null +++ b/libpf/pfelf/testdata/dwarf_structs.c @@ -0,0 +1,11 @@ +#include + +struct some_struct { + uint64_t some_array[8]; + uint64_t some_int; +}; + +int main(int argc, char *argv[]) { + struct some_struct _my_struct; + return 0; +} diff --git a/libpf/pfelf/testdata/ruby_dwarf_structs.c b/libpf/pfelf/testdata/ruby_dwarf_structs.c new file mode 100644 index 000000000..e53370fe7 --- /dev/null +++ b/libpf/pfelf/testdata/ruby_dwarf_structs.c @@ -0,0 +1,18 @@ +#include +// Snippets of more complicated structs from ruby.h + +#define IMMEDIATE_TABLE_SIZE 54 /* a multiple of 9, and < 128 */ + +struct succ_index_table { + uint64_t imm_part[IMMEDIATE_TABLE_SIZE / 9]; + struct succ_dict_block { + unsigned int rank; + uint64_t small_block_ranks; /* 9 bits * 7 = 63 bits */ + uint64_t bits[512/64]; + } succ_part[0]; +}; + +int main(int argc, char *argv[]) { + struct succ_index_table _idx_table; + return 0; +} From ed9e7cf9321aad393d551f736f84999098362d11 Mon Sep 17 00:00:00 2001 From: Dale Hamel Date: Mon, 18 Aug 2025 11:48:03 -0400 Subject: [PATCH 04/14] More complete test examples --- libpf/pfelf/dwarf.go | 155 ++++---- libpf/pfelf/dwarf_test.go | 189 +++++---- libpf/pfelf/file.go | 5 +- libpf/pfelf/testdata/dwarf_structs.c | 11 +- libpf/pfelf/testdata/ruby_dwarf_structs.c | 461 +++++++++++++++++++++- 5 files changed, 650 insertions(+), 171 deletions(-) diff --git a/libpf/pfelf/dwarf.go b/libpf/pfelf/dwarf.go index fdfa62682..e876992c7 100644 --- a/libpf/pfelf/dwarf.go +++ b/libpf/pfelf/dwarf.go @@ -11,63 +11,44 @@ import ( "debug/dwarf" "fmt" "os" - "strings" "slices" + "strings" ) -type structData struct { - name string +type typeData struct { + name string + size int64 structTypeInfo *dwarf.StructType } -func (data structData) String() string { +func (data typeData) String() string { var str string = "" - - str += fmt.Sprintf("\nstruct %s {\n", data.name) - - // Print field information - for _, field := range data.structTypeInfo.Field { - fieldType := field.Type.String() - // Clean up the type string a bit - fieldType = strings.TrimPrefix(fieldType, "struct ") - - str += fmt.Sprintf(" %s %s; // offset: %d, size: %d", - fieldType, field.Name, field.ByteOffset, field.Type.Size()) - - //// Check if this might be a flexible array member - //isLastField := i == len(data.structTypeInfo.Field)-1 - //isArrayType := strings.HasSuffix(fieldType, "[]") || - // strings.Contains(fieldType, "[0]") || - // strings.Contains(fieldType, "FLEX_ARY") - //hasArrayName := strings.HasSuffix(field.Name, "_array") || - // strings.HasSuffix(field.Name, "_part") - // - //if isLastField && (isArrayType || hasArrayName) { - // str += fmt.Sprintf(" (flexible array member)") - // hasFlexibleArray = true - // flexArrayOffset = field.ByteOffset - //} + + if data.structTypeInfo != nil { + str += fmt.Sprintf("\nstruct %s {\n", data.name) + + // Print field information + for _, field := range data.structTypeInfo.Field { + fieldType := field.Type.String() + // Clean up the type string a bit + fieldType = strings.TrimPrefix(fieldType, "struct ") + + str += fmt.Sprintf(" %s %s; // offset: %d, size: %d", + fieldType, field.Name, field.ByteOffset, field.Type.Size()) + str += "\n" + } + + str += fmt.Sprintf("} // total size: %d bytes", (&data).Size()) str += "\n" + } else { + str += fmt.Sprintf("\n%s ", data.name) + str += fmt.Sprintf(" // total size: %d bytes", data.size) } - - - str += fmt.Sprintf("} // total size: %d bytes", (&data).Size()) - //if hasFlexibleArray { - // str += fmt.Sprintf(" (base size without flexible array)") - //} - //if dwarfSize != reportedSize { - // str += fmt.Sprintf(" (DWARF: %d, calculated: %d)", dwarfSize, calculatedSize) - //} - str += "\n" return str } - -// TODO refactor the printing function code to actually return these, then use -// these functions in the String calc - -func (data *structData) FieldOffset(name string) (int64, error) { +func (data *typeData) FieldOffset(name string) (int64, error) { field, err := data.field(name) if err != nil { return -1, err @@ -75,7 +56,7 @@ func (data *structData) FieldOffset(name string) (int64, error) { return field.ByteOffset, nil } -func (data *structData) FieldSize(name string) (int64, error) { +func (data *typeData) FieldSize(name string) (int64, error) { field, err := data.field(name) if err != nil { return -1, err @@ -83,13 +64,35 @@ func (data *structData) FieldSize(name string) (int64, error) { return field.Type.Size(), nil } -func (data *structData) field(name string) (*dwarf.StructField, error) { +func (data *typeData) field(name string) (*dwarf.StructField, error) { var found *dwarf.StructField = nil - for _, field := range data.structTypeInfo.Field { - if field.Name == name { - found = field - break + parts := strings.Split(name, ".") + + if len(parts) > 1 { + var parent *dwarf.StructType = data.structTypeInfo + for i, part := range parts { + for _, field := range parent.Field { + if field.Name == part { + if i == len(parts)-1 { + found = field + break + } + struct_type, ok := field.Type.(*dwarf.StructType) + if ok { + parent = struct_type + } + break + } + } + } + + } else { + for _, field := range data.structTypeInfo.Field { + if field.Name == name { + found = field + break + } } } @@ -100,12 +103,12 @@ func (data *structData) field(name string) (*dwarf.StructField, error) { return found, nil } -func (data *structData) Size() int64 { +func (data *typeData) Size() int64 { + if data.structTypeInfo == nil { + return data.size + } // Calculate size based on fields calculatedSize := int64(0) - - //hasFlexibleArray := false - //var flexArrayOffset int64 = 0 // Print field information for _, field := range data.structTypeInfo.Field { @@ -117,7 +120,7 @@ func (data *structData) Size() int64 { } // Get the struct size from DWARF dwarfSize := data.structTypeInfo.ByteSize - + // Use calculated size if DWARF size is 0 or negative reportedSize := dwarfSize if dwarfSize <= 0 { //|| (hasFlexibleArray && dwarfSize < flexArrayOffset) { // TODO verify if flex array offset logic actually needed @@ -127,11 +130,10 @@ func (data *structData) Size() int64 { return reportedSize } - // This accepts a list of names to look up, as we want to try and get "everything in one go", // since DWARF is inherently O(n) to look up these symbols -func loadStructData(debugInfo, debugAbbrev, debugStr, debugLineStr *Section, names []string) ([]structData, error) { - results := []structData{} +func loadStructData(debugInfo, debugAbbrev, debugStr, debugLineStr *Section, names []string) ([]typeData, error) { + results := []typeData{} // To reduce memory usage, we will use the Section's Data() accessor to // get a memory mapped "subslice" and avoid allocations @@ -167,13 +169,13 @@ func loadStructData(debugInfo, debugAbbrev, debugStr, debugLineStr *Section, nam if err := dwarfData.AddSection(".debug_line_str", lineStrData); err != nil { return nil, err } - - processedStructs := make(map[string]struct{}) + + processedTypes := make(map[string]struct{}) reader := dwarfData.Reader() for { // return early if we have all of the structs that were asked for - if len(processedStructs) == len(names) { + if len(processedTypes) == len(names) { return results, nil } @@ -185,7 +187,6 @@ func loadStructData(debugInfo, debugAbbrev, debugStr, debugLineStr *Section, nam if entry == nil { break } - // Look for struct type entries if entry.Tag == dwarf.TagStructType { nameVal, ok := entry.Val(dwarf.AttrName).(string) @@ -194,13 +195,13 @@ func loadStructData(debugInfo, debugAbbrev, debugStr, debugLineStr *Section, nam } name := nameVal - if _, ok := processedStructs[name]; ok { + if _, ok := processedTypes[name]; ok { continue } // Ignore structs that weren't asked for if !slices.Contains(names, name) { - continue + continue } // Get the type information @@ -209,26 +210,38 @@ func loadStructData(debugInfo, debugAbbrev, debugStr, debugLineStr *Section, nam fmt.Printf("Warning: Error getting type info for %s: %v\n", name, err) continue } - + // Type assertion to get the struct type structTypeInfo, ok := structType.(*dwarf.StructType) if !ok { continue } - + // Skip incomplete structs, try another compilation unit if structTypeInfo.Incomplete { continue } - - results = append(results, structData{ - name: name, + + results = append(results, typeData{ + name: name, structTypeInfo: structTypeInfo, }) - processedStructs[name] = struct{}{} + processedTypes[name] = struct{}{} + } else if entry_name, ok := entry.Val(dwarf.AttrName).(string); ok && slices.Contains(names, entry_name) { + // look for anything with the name + t, err := dwarfData.Type(entry.Offset) + if err != nil { + return nil, err + } + if t.Size() > 0 { + results = append(results, typeData{ + name: entry_name, + size: t.Size(), + }) + } } } - + return results, nil } diff --git a/libpf/pfelf/dwarf_test.go b/libpf/pfelf/dwarf_test.go index b52e5d3a1..57c6a0cbb 100644 --- a/libpf/pfelf/dwarf_test.go +++ b/libpf/pfelf/dwarf_test.go @@ -14,110 +14,129 @@ func TestDWARFParseStructs(t *testing.T) { tests := []struct { desc string test_file string - expectedStructs []string - expectedStructSizes map[string]int64 + expectedTypes []string + expectedTypeSizes map[string]int64 expectedFieldOffsets map[string]map[string]int64 expectedFieldSizes map[string]map[string]int64 }{ { - desc: "it should be able to parse arbitrary structs", - test_file: "testdata/dwarf_structs", - expectedStructs : []string { + desc: "it should be able to parse arbitrary structs", + test_file: "testdata/dwarf_structs", + expectedTypes: []string{ "some_struct", + "some_typedef", }, - expectedStructSizes : map[string]int64{ - "some_struct" : 72, + expectedTypeSizes: map[string]int64{ + "some_struct": 72, + "some_typedef": 8, }, - expectedFieldOffsets : map[string]map[string]int64{ + expectedFieldOffsets: map[string]map[string]int64{ "some_struct": map[string]int64{ "some_array": int64(0), "some_int": int64(64), }, }, - expectedFieldSizes : map[string]map[string]int64{ + expectedFieldSizes: map[string]map[string]int64{ "some_struct": map[string]int64{ "some_array": int64(64), - "some_int": int64(8), + "some_int": int64(8), }, }, }, { -/* - 'execution_context_struct.vm_stack': offset_of('rb_execution_context_struct', 'vm_stack'), - 'execution_context_struct.vm_stack_size': offset_of('rb_execution_context_struct', 'vm_stack_size'), - 'execution_context_struct.cfp': offset_of('rb_execution_context_struct', 'cfp'), - - 'control_frame_struct.pc': offset_of('rb_control_frame_struct', 'pc'), - 'control_frame_struct.iseq': offset_of('rb_control_frame_struct', 'iseq'), - 'control_frame_struct.ep': offset_of('rb_control_frame_struct', 'ep'), - 'control_frame_struct.size_of_control_frame_struct': size_of('rb_control_frame_struct'), - - 'iseq_struct.body': offset_of('rb_iseq_struct', 'body'), - - 'iseq_constant_body.iseq_type': offset_of('rb_iseq_constant_body', 'type'), - 'iseq_constant_body.size': offset_of('rb_iseq_constant_body', 'iseq_size'), - 'iseq_constant_body.encoded': offset_of('rb_iseq_constant_body', 'iseq_encoded'), - 'iseq_constant_body.location': offset_of('rb_iseq_constant_body', 'location'), - 'iseq_constant_body.insn_info_body': offset_of('rb_iseq_constant_body', 'insns_info.body'), - 'iseq_constant_body.insn_info_size': offset_of('rb_iseq_constant_body', 'insns_info.size'), - 'iseq_constant_body.succ_index_table': offset_of('rb_iseq_constant_body', 'insns_info.succ_index_table'), - 'iseq_constant_body.size_of_iseq_constant_body': size_of('rb_iseq_constant_body'), - - 'iseq_location_struct.pathobj': offset_of('rb_iseq_location_struct', 'pathobj'), - 'iseq_location_struct.base_label': offset_of('rb_iseq_location_struct', 'base_label'), - - 'iseq_insn_info_entry.position': offset_of('iseq_insn_info_entry', 'position'), - 'iseq_insn_info_entry.size_of_position': size_of_field('iseq_insn_info_entry', 'position'), - 'iseq_insn_info_entry.line_no': offset_of('iseq_insn_info_entry', 'line_no'), - 'iseq_insn_info_entry.size_of_line_no': size_of_field('iseq_insn_info_entry', 'line_no'), - 'iseq_insn_info_entry.size_of_iseq_insn_info_entry': size_of('iseq_insn_info_entry'), - - 'rstring_struct.as_ary': offset_of('RString', 'as.embed.ary'), - 'rstring_struct.as_heap_ptr': offset_of('RString', 'as.heap.ptr'), - - 'rarray_struct.as_ary': offset_of('RArray', 'as.ary'), - 'rarray_struct.as_heap_ptr': offset_of('RArray', 'as.heap.ptr'), - - 'size_of_value': size_of('VALUE', ns=''), - - 'rb_ractor_struct.running_ec': offset_of('rb_ractor_struct', 'threads.running_ec'), -*/ - desc: "it should be able to parse complicated ruby structs", - test_file: "testdata/ruby_dwarf_structs", - expectedStructs : []string { - //"rb_execution_context_struct", - //"rb_control_frame_struct", - //"rb_iseq_struct", - //"rb_iseq_constant_body", - //"rb_iseq_location_struct", - //"iseq_insn_info_entry", - //"RString", - //"RArray", + desc: "it should be able to parse complicated ruby structs", + test_file: "testdata/ruby_dwarf_structs", + expectedTypes: []string{ + "rb_execution_context_struct", + "rb_control_frame_struct", + "rb_iseq_struct", + "rb_iseq_constant_body", + "iseq_insn_info", // sub-struct of rb_iseq_constant_body + "rb_iseq_location_struct", + "iseq_insn_info_entry", + "RString", + "RArray", "succ_index_table", "succ_dict_block", + "rb_ractor_struct", + "VALUE", }, - expectedStructSizes : map[string]int64{ - "succ_dict_block" : 80, + expectedTypeSizes: map[string]int64{ + "rb_control_frame_struct": 56, + "rb_iseq_constant_body": 320, + "iseq_insn_info_entry": 8, + "succ_dict_block": 80, + "VALUE": 8, }, - expectedFieldOffsets : map[string]map[string]int64{ - //"rb_execution_context_struct": map[string]int64{ - // "vm_stack": int64(0), - // "cfp": int64(16), - //}, + expectedFieldOffsets: map[string]map[string]int64{ + "rb_execution_context_struct": map[string]int64{ + "vm_stack": int64(0), + "vm_stack_size": int64(8), + "cfp": int64(16), + }, + "rb_control_frame_struct": map[string]int64{ + "pc": int64(0), + "iseq": int64(16), + "ep": int64(32), + }, + "rb_iseq_struct": map[string]int64{ + "body": int64(16), + }, + "rb_iseq_constant_body": map[string]int64{ + "type": int64(0), + "iseq_size": int64(4), + "iseq_encoded": int64(8), + "location": int64(64), + "insns_info": int64(112), + }, + "iseq_insn_info": map[string]int64{ // substruct of rb_iseq_constant_body, these offsets would be added to the insns_info offset + "body": int64(0), + "size": int64(16), + "succ_index_table": int64(24), + }, + "rb_iseq_location_struct": map[string]int64{ + "pathobj": int64(0), + "base_label": int64(8), + }, + "iseq_insn_info_entry": map[string]int64{ + // position was removed in 3.1 + "line_no": int64(0), + }, + "RString": map[string]int64{ + "as": int64(24), + "as.embed": int64(0), + "as.embed.ary": int64(0), + "as.heap": int64(0), + "as.heap.ptr": int64(0), + }, + "RArray": map[string]int64{ + "as": int64(16), + "as.ary": int64(0), + "as.heap": int64(0), + "as.heap.ptr": int64(16), + }, "succ_index_table": map[string]int64{ "succ_part": int64(48), }, "succ_dict_block": map[string]int64{ "small_block_ranks": int64(8), - "bits": int64(16), + "bits": int64(16), + }, + "rb_ractor_struct": map[string]int64{ + "threads": int64(264), + "threads.running_ec": int64(136), }, }, - expectedFieldSizes : map[string]map[string]int64{ - //"rb_execution_context_struct": map[string]int64{ - // "vm_stack": int64(8), - //}, + expectedFieldSizes: map[string]map[string]int64{ + "rb_execution_context_struct": map[string]int64{ + "vm_stack": int64(8), + }, + "iseq_insn_info_entry": map[string]int64{ + // position was removed in 3.1 + "line_no": int64(4), + }, "succ_index_table": map[string]int64{ - "imm_part": int64(48), // note that in python they multiple this by 9, then divide by 8 https://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/078ae4d6ded761b513038440bc8525014fa6c016/tools/coredump/testsources/ruby/gdb-dump-offsets.py#L69 because of https://github.com/Shopify/ruby/blob/70b4b6fea0eeb66647539bcb3b9a50d027d92e51/iseq.c#L4265 + "imm_part": int64(48), // note that in python they multiple this by 9, then divide by 8 https://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/078ae4d6ded761b513038440bc8525014fa6c016/tools/coredump/testsources/ruby/gdb-dump-offsets.py#L69 because of https://github.com/Shopify/ruby/blob/70b4b6fea0eeb66647539bcb3b9a50d027d92e51/iseq.c#L4265 "succ_part": int64(0), }, }, @@ -132,31 +151,31 @@ func TestDWARFParseStructs(t *testing.T) { require.NoError(err) defer elfFile.Close() - struct_data, err := elfFile.StructData(tt.expectedStructs) + type_data, err := elfFile.TypeData(tt.expectedTypes) require.NoError(err) - assert.Equal(len(tt.expectedStructs), len(struct_data)) + assert.Equal(len(tt.expectedTypes), len(type_data)) - structs_by_name := map[string]structData{} + types_by_name := map[string]typeData{} - for _, struct_info := range struct_data { - structs_by_name[struct_info.name] = struct_info + for _, struct_info := range type_data { + types_by_name[struct_info.name] = struct_info } - for _, name := range tt.expectedStructs { - _, ok := structs_by_name[name] + for _, name := range tt.expectedTypes { + _, ok := types_by_name[name] assert.True(ok) } - for name, expected_size := range tt.expectedStructSizes { - struct_info, ok := structs_by_name[name] + for name, expected_size := range tt.expectedTypeSizes { + struct_info, ok := types_by_name[name] require.True(ok) assert.Equal(expected_size, struct_info.Size()) } for name, fields := range tt.expectedFieldOffsets { - struct_info, ok := structs_by_name[name] + struct_info, ok := types_by_name[name] require.True(ok) for field, expected_offset := range fields { @@ -168,7 +187,7 @@ func TestDWARFParseStructs(t *testing.T) { } for name, fields := range tt.expectedFieldSizes { - struct_info, ok := structs_by_name[name] + struct_info, ok := types_by_name[name] require.True(ok) for field, expected_size := range fields { diff --git a/libpf/pfelf/file.go b/libpf/pfelf/file.go index e9f8cc8f1..cf8cdf1de 100644 --- a/libpf/pfelf/file.go +++ b/libpf/pfelf/file.go @@ -504,10 +504,9 @@ func (f *File) SymbolData(name libpf.SymbolName, maxCopy int) (*libpf.Symbol, [] return sym, data, err } - -// Gets StructData for specific structs from DWARF, without loading and parsing +// Gets TypeData for specific types from DWARF, without loading and parsing // the whole thing -func (f *File) StructData (names []string) ([]structData, error) { +func (f *File) TypeData(names []string) ([]typeData, error) { return loadStructData(f.Section(".debug_info"), f.Section(".debug_abbrev"), f.Section(".debug_str"), f.Section(".debug_line_str"), names) } diff --git a/libpf/pfelf/testdata/dwarf_structs.c b/libpf/pfelf/testdata/dwarf_structs.c index f706eaf72..de0b6298d 100644 --- a/libpf/pfelf/testdata/dwarf_structs.c +++ b/libpf/pfelf/testdata/dwarf_structs.c @@ -1,11 +1,14 @@ #include +typedef uintptr_t some_typedef; + struct some_struct { - uint64_t some_array[8]; - uint64_t some_int; + uint64_t some_array[8]; + uint64_t some_int; }; int main(int argc, char *argv[]) { - struct some_struct _my_struct; - return 0; + struct some_struct _my_struct; + some_typedef _my_typedef; + return 0; } diff --git a/libpf/pfelf/testdata/ruby_dwarf_structs.c b/libpf/pfelf/testdata/ruby_dwarf_structs.c index e53370fe7..5e954a131 100644 --- a/libpf/pfelf/testdata/ruby_dwarf_structs.c +++ b/libpf/pfelf/testdata/ruby_dwarf_structs.c @@ -1,18 +1,463 @@ +#include +#include #include + // Snippets of more complicated structs from ruby.h #define IMMEDIATE_TABLE_SIZE 54 /* a multiple of 9, and < 128 */ +#define USE_RJIT 1 + +typedef uintptr_t VALUE; +typedef uintptr_t ID; +typedef uint32_t id_key_t; +typedef uintptr_t iseq_bits_t; +typedef uintptr_t + rb_jit_func_t; // not sure where it is actually defined, pahole says it is 8 +typedef uintptr_t + rb_event_hook_func_t; // size of a function pointer, pahole says 8 +typedef uint32_t rb_event_flag_t; +typedef signed long rb_snum_t; + +#include +typedef pthread_mutex_t rb_nativethread_lock_t; +typedef pthread_cond_t rb_nativethread_cond_t; + +struct RBasic { + VALUE flags; + const VALUE klass; + +#if RBASIC_SHAPE_ID_FIELD + VALUE shape_id; +#endif +}; + +struct RString { + struct RBasic basic; + long len; + union { + struct { + char *ptr; + union { + long capa; + VALUE shared; + } aux; + } heap; + + struct { + char ary[1]; + } embed; + } as; +}; + +struct RArray { + struct RBasic basic; + union { + struct { + long len; + union { + long capa; +#if defined(__clang__) /* <- clang++ is sane */ || \ + !defined(__cplusplus) /* <- C99 is sane */ || \ + (__cplusplus > 199711L) /* <- C++11 is sane */ + const +#endif + VALUE shared_root; + } aux; + const VALUE *ptr; + } heap; + const VALUE ary[1]; + } as; +}; + +struct iseq_insn_info_entry { + int line_no; +#ifdef USE_ISEQ_NODE_ID + int node_id; +#endif + rb_event_flag_t events; +}; + +typedef struct rb_id_item { + id_key_t key; + int collision; + VALUE val; +} item_t; + +struct rb_id_table { + int capa; + int num; + int used; + item_t *items; +}; + +typedef struct rb_code_position_struct { + int lineno; + int column; +} rb_code_position_t; + +typedef struct rb_code_location_struct { + rb_code_position_t beg_pos; + rb_code_position_t end_pos; +} rb_code_location_t; + +typedef struct rb_iseq_location_struct { + VALUE pathobj; /* String (path) or Array [path, realpath]. Frozen. */ + VALUE base_label; /* String */ + VALUE label; /* String */ + int first_lineno; + int node_id; + rb_code_location_t code_location; +} rb_iseq_location_t; + +enum rb_iseq_type { + ISEQ_TYPE_TOP, + ISEQ_TYPE_METHOD, + ISEQ_TYPE_BLOCK, + ISEQ_TYPE_CLASS, + ISEQ_TYPE_RESCUE, + ISEQ_TYPE_ENSURE, + ISEQ_TYPE_EVAL, + ISEQ_TYPE_MAIN, + ISEQ_TYPE_PLAIN +}; + +struct rb_iseq_struct; +typedef struct rb_iseq_struct rb_iseq_t; + +struct rb_iseq_constant_body { + enum rb_iseq_type type; + + unsigned int iseq_size; + VALUE *iseq_encoded; /* encoded iseq (insn addr and operands) */ + + struct { + struct { + unsigned int has_lead : 1; + unsigned int has_opt : 1; + unsigned int has_rest : 1; + unsigned int has_post : 1; + unsigned int has_kw : 1; + unsigned int has_kwrest : 1; + unsigned int has_block : 1; + + unsigned int ambiguous_param0 : 1; /* {|a|} */ + unsigned int accepts_no_kwarg : 1; + unsigned int ruby2_keywords : 1; + unsigned int anon_rest : 1; + unsigned int anon_kwrest : 1; + unsigned int use_block : 1; + unsigned int forwardable : 1; + } flags; + + unsigned int size; + + int lead_num; + int opt_num; + int rest_start; + int post_start; + int post_num; + int block_start; + + const VALUE *opt_table; /* (opt_num + 1) entries. */ + /* opt_num and opt_table: + * + * def foo o1=e1, o2=e2, ..., oN=eN + * #=> + * # prologue code + * A1: e1 + * A2: e2 + * ... + * AN: eN + * AL: body + * opt_num = N + * opt_table = [A1, A2, ..., AN, AL] + */ + + const struct rb_iseq_param_keyword { + int num; + int required_num; + int bits_start; + int rest_start; + const ID *table; + VALUE *default_values; + } *keyword; + } param; + + rb_iseq_location_t location; + + /* insn info, must be freed */ + struct iseq_insn_info { + const struct iseq_insn_info_entry *body; + unsigned int *positions; + unsigned int size; + // #if VM_INSN_INFO_TABLE_IMPL == 2 + struct succ_index_table *succ_index_table; + // #endif + } insns_info; + + const ID *local_table; /* must free */ + + /* catch table */ + struct iseq_catch_table *catch_table; + + /* for child iseq */ + const struct rb_iseq_struct *parent_iseq; + struct rb_iseq_struct *local_iseq; /* local_iseq->flip_cnt can be modified */ + + union iseq_inline_storage_entry + *is_entries; /* [ TS_IVC | TS_ICVARC | TS_ISE | TS_IC ] */ + struct rb_call_data *call_data; // struct rb_call_data calls[ci_size]; + + struct { + rb_snum_t flip_count; + VALUE script_lines; + VALUE coverage; + VALUE pc2branchindex; + VALUE *original_iseq; + } variable; + + unsigned int local_table_size; + unsigned int ic_size; // Number of IC caches + unsigned int ise_size; // Number of ISE caches + unsigned int ivc_size; // Number of IVC caches + unsigned int icvarc_size; // Number of ICVARC caches + unsigned int ci_size; + unsigned int stack_max; /* for stack overflow check */ + + unsigned int builtin_attrs; // Union of rb_builtin_attr + + bool prism; // ISEQ was generated from prism compiler + + union { + iseq_bits_t *list; /* Find references for GC */ + iseq_bits_t single; + } mark_bits; + + struct rb_id_table *outer_variables; + + const rb_iseq_t *mandatory_only_iseq; + +#if USE_RJIT || USE_YJIT + // Function pointer for JIT code on jit_exec() + rb_jit_func_t jit_entry; + // Number of calls on jit_exec() + long unsigned jit_entry_calls; +#endif + +#if USE_YJIT + // Function pointer for JIT code on jit_exec_exception() + rb_jit_func_t jit_exception; + // Number of calls on jit_exec_exception() + long unsigned jit_exception_calls; +#endif + +#if USE_RJIT + // RJIT stores some data on each iseq. + VALUE rjit_blocks; +#endif + +#if USE_YJIT + // YJIT stores some data on each iseq. + void *yjit_payload; + // Used to estimate how frequently this ISEQ gets called + uint64_t yjit_calls_at_interv; +#endif +}; + +struct rb_iseq_struct { + VALUE flags; /* 1 */ + VALUE wrapper; /* 2 */ + + struct rb_iseq_constant_body *body; /* 3 */ + + // ... +}; + +typedef struct rb_control_frame_struct { + const VALUE *pc; /* cfp[0] */ + VALUE *sp; /* cfp[1] */ + const rb_iseq_t *iseq; /* cfp[2] */ + VALUE self; /* cfp[3] / block[0] */ + const VALUE *ep; /* cfp[4] / block[1] */ + const void *block_code; + /* cfp[5] / block[2] */ /* iseq or ifunc or forwarded block handler */ + VALUE *__bp__; + /* cfp[6] */ /* outside vm_push_frame, use vm_base_ptr instead. */ + +#if VM_DEBUG_BP_CHECK + VALUE *bp_check; /* cfp[7] */ +#endif +} rb_control_frame_t; + +typedef struct rb_execution_context_struct { + /* execution information */ + VALUE *vm_stack; /* must free, must mark */ + size_t vm_stack_size; /* size in word (byte size / sizeof(VALUE)) */ + rb_control_frame_t *cfp; + // ... +} rb_execution_context_t; struct succ_index_table { - uint64_t imm_part[IMMEDIATE_TABLE_SIZE / 9]; - struct succ_dict_block { - unsigned int rank; - uint64_t small_block_ranks; /* 9 bits * 7 = 63 bits */ - uint64_t bits[512/64]; - } succ_part[0]; + uint64_t imm_part[IMMEDIATE_TABLE_SIZE / 9]; + struct succ_dict_block { + unsigned int rank; + uint64_t small_block_ranks; /* 9 bits * 7 = 63 bits */ + uint64_t bits[512 / 64]; + } succ_part[0]; +}; + +typedef enum { + RUBY_EVENT_HOOK_FLAG_SAFE = 0x01, + RUBY_EVENT_HOOK_FLAG_DELETED = 0x02, + RUBY_EVENT_HOOK_FLAG_RAW_ARG = 0x04 +} rb_event_hook_flag_t; + +// This type is too complicated, just jamming a dummy struct in that is the same +// size +typedef struct rb_thread_struct { + uint8_t dummy_array[480]; +} rb_thread_t; + +typedef struct rb_event_hook_struct { + rb_event_hook_flag_t hook_flags; + rb_event_flag_t events; + rb_event_hook_func_t func; + VALUE data; + struct rb_event_hook_struct *next; + + struct { + rb_thread_t *th; + unsigned int target_line; + } filter; +} rb_event_hook_t; + +typedef struct rb_hook_list_struct { + struct rb_event_hook_struct *hooks; + rb_event_flag_t events; + unsigned int running; + bool need_clean; + bool is_local; +} rb_hook_list_t; + +struct rb_ractor_pub { + VALUE self; + uint32_t id; + rb_hook_list_t hooks; +}; + +struct ccan_list_node { + struct ccan_list_node *next, *prev; +}; + +struct ccan_list_head { + struct ccan_list_node n; }; +struct rb_ractor_struct; +typedef struct rb_ractor_struct rb_ractor_t; + +struct rb_thread_sched { + rb_nativethread_lock_t lock_; +#if VM_CHECK_MODE + struct rb_thread_struct *lock_owner; +#endif + struct rb_thread_struct *running; // running thread or NULL + bool is_running; + bool is_running_timeslice; + bool enable_mn_threads; + + struct ccan_list_head readyq; + int readyq_cnt; + // ractor scheduling + struct ccan_list_node grq_node; +}; + +// just want this type, don't care about the members, just the size +struct rb_ractor_basket { + uint8_t dummy_array[32]; +}; + +struct rb_ractor_queue { + struct rb_ractor_basket *baskets; + int start; + int cnt; + int size; + unsigned int serial; + unsigned int reserved_cnt; +}; + +enum rb_ractor_wait_status { + wait_none = 0x00, + wait_receiving = 0x01, + wait_taking = 0x02, + wait_yielding = 0x04, + wait_moving = 0x08, +}; + +enum rb_ractor_wakeup_status { + wakeup_none, + wakeup_by_send, + wakeup_by_yield, + wakeup_by_take, + wakeup_by_close, + wakeup_by_interrupt, + wakeup_by_retry, +}; + +struct rb_ractor_sync { + // ractor lock + rb_nativethread_lock_t lock; +#if RACTOR_CHECK_MODE > 0 + VALUE locked_by; +#endif + + bool incoming_port_closed; + bool outgoing_port_closed; + + // All sent messages will be pushed into recv_queue + struct rb_ractor_queue recv_queue; + + // The following ractors waiting for the yielding by this ractor + struct rb_ractor_queue takers_queue; + + // Enabled if the ractor already terminated and not taken yet. + struct rb_ractor_basket will_basket; + + struct ractor_wait { + enum rb_ractor_wait_status status; + enum rb_ractor_wakeup_status wakeup_status; + rb_thread_t *waiting_thread; + } wait; +}; + +struct rb_ractor_struct { + struct rb_ractor_pub pub; + struct rb_ractor_sync sync; + + VALUE receiving_mutex; + + // vm wide barrier synchronization + rb_nativethread_cond_t barrier_wait_cond; + // thread management + struct { + struct ccan_list_head set; + unsigned int cnt; + unsigned int blocking_cnt; + unsigned int sleeper; + struct rb_thread_sched sched; + rb_execution_context_t *running_ec; + // ... + } threads; + // ... +}; // rb_ractor_t is defined in vm_core.h + int main(int argc, char *argv[]) { - struct succ_index_table _idx_table; - return 0; + struct succ_index_table _idx_table; + struct rb_execution_context_struct _exec_context; + struct rb_control_frame_struct _control_frame; + struct rb_iseq_struct _iseq_struct; + struct RString _string; + struct RArray _array; + struct rb_ractor_struct _ractor; + return 0; } From 6fa9283548bb2bda3e376dd05976ff763393248e Mon Sep 17 00:00:00 2001 From: Dale Hamel Date: Mon, 18 Aug 2025 13:19:16 -0400 Subject: [PATCH 05/14] Skip processing already processed names --- libpf/pfelf/dwarf.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libpf/pfelf/dwarf.go b/libpf/pfelf/dwarf.go index e876992c7..78651cfe0 100644 --- a/libpf/pfelf/dwarf.go +++ b/libpf/pfelf/dwarf.go @@ -229,6 +229,11 @@ func loadStructData(debugInfo, debugAbbrev, debugStr, debugLineStr *Section, nam processedTypes[name] = struct{}{} } else if entry_name, ok := entry.Val(dwarf.AttrName).(string); ok && slices.Contains(names, entry_name) { + + if _, ok := processedTypes[entry_name]; ok { + continue + } + // look for anything with the name t, err := dwarfData.Type(entry.Offset) if err != nil { @@ -239,6 +244,7 @@ func loadStructData(debugInfo, debugAbbrev, debugStr, debugLineStr *Section, nam name: entry_name, size: t.Size(), }) + processedTypes[entry_name] = struct{}{} } } } From db2f9ec4b0ff317bbde8c75b8acf4f7945c60542 Mon Sep 17 00:00:00 2001 From: Dale Hamel Date: Mon, 18 Aug 2025 13:20:02 -0400 Subject: [PATCH 06/14] Support zlib compressed DWARF sections for little-endian elf64 --- libpf/pfelf/file.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/libpf/pfelf/file.go b/libpf/pfelf/file.go index cf8cdf1de..fd034ef99 100644 --- a/libpf/pfelf/file.go +++ b/libpf/pfelf/file.go @@ -21,8 +21,10 @@ package pfelf // import "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" import ( "bytes" + "compress/zlib" "debug/buildinfo" "debug/elf" + "encoding/binary" "errors" "fmt" "hash/crc32" @@ -857,7 +859,27 @@ func (sh *Section) ReadAt(p []byte, off int64) (n int, err error) { // Data loads the whole section header referenced data, and returns it as a slice. func (sh *Section) Data(maxSize uint) ([]byte, error) { if sh.Flags&elf.SHF_COMPRESSED != 0 { - return nil, errors.New("compressed sections not supported") + if mapping, ok := sh.elfReader.(*mmap.ReaderAt); ok { + var chdr64 elf.Chdr64 + section := io.NewSectionReader(mapping, int64(sh.Offset), int64(sh.FileSize)) + err := binary.Read(section, binary.LittleEndian, &chdr64) + if err != nil { + return nil, err + } + + if elf.CompressionType(chdr64.Type) != elf.COMPRESS_ZLIB { + return nil, fmt.Errorf("unsupported compression type %d", elf.CompressionType(chdr64.Type)) + } + + compressed_section := io.NewSectionReader(mapping, int64(sh.Offset+uint64(binary.Size(chdr64))), int64(chdr64.Size)) + + zlibReader, err := zlib.NewReader(compressed_section) + if err != nil { + return nil, err + } + defer zlibReader.Close() + return io.ReadAll(zlibReader) + } } if mapping, ok := sh.elfReader.(*mmap.ReaderAt); ok { From d877b1bba9ad4f05d07592049abfb9bda500c9e4 Mon Sep 17 00:00:00 2001 From: Dale Hamel Date: Mon, 18 Aug 2025 13:21:17 -0400 Subject: [PATCH 07/14] Fix up test program --- test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test.go b/test.go index c2374e9d0..b2ca64a4e 100644 --- a/test.go +++ b/test.go @@ -7,28 +7,28 @@ import ( "fmt" ) -// /usr/bin/time -v go run test.go /home/dalehamel.linux/.rubies/ruby-3.4.4/bin/ruby rb_execution_context_struct rb_control_frame_struct rb_iseq_struct rb_iseq_constant_body rb_iseq_location_struct iseq_insn_info_entry RString RArray succ_dict_block succ_index_table +// /usr/bin/time -v go run test.go /home/dalehamel.linux/.rubies/ruby-3.4.4/bin/ruby rb_execution_context_type rb_control_frame_type rb_iseq_type rb_iseq_constant_body rb_iseq_location_type iseq_insn_info_entry RString RArray succ_dict_block succ_index_table func main() { if len(os.Args) < 2 { - fmt.Fprintf(os.Stderr, "Usage: %s [struct-name]\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Usage: %s [type-name]\n", os.Args[0]) os.Exit(1) } elfFile := os.Args[1] - specificStructs := []string{} + specificTypes := []string{} if len(os.Args) > 2 { - specificStructs = os.Args[2:] + specificTypes = os.Args[2:] } pf, err := pfelf.Open(elfFile) if err != nil { - fmt.Fprintf(os.Stderr, "Error opening ELF file for %s: %v\n", specificStructs, err) + fmt.Fprintf(os.Stderr, "Error opening ELF file for %s: %v\n", specificTypes, err) os.Exit(1) } defer pf.Close() - data, err := pf.StructData(specificStructs) + data, err := pf.TypeData(specificTypes) if err != nil { fmt.Fprintf(os.Stderr, "Error opening ELF file: %v\n", err) os.Exit(1) From 4b609498d4495ff9fde821899f9e1ae09aa4e4f5 Mon Sep 17 00:00:00 2001 From: Dale Hamel Date: Mon, 18 Aug 2025 13:49:28 -0400 Subject: [PATCH 08/14] Use size field of the section header for uncompressed size --- libpf/pfelf/file.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libpf/pfelf/file.go b/libpf/pfelf/file.go index fd034ef99..0ddf3b502 100644 --- a/libpf/pfelf/file.go +++ b/libpf/pfelf/file.go @@ -860,6 +860,7 @@ func (sh *Section) ReadAt(p []byte, off int64) (n int, err error) { func (sh *Section) Data(maxSize uint) ([]byte, error) { if sh.Flags&elf.SHF_COMPRESSED != 0 { if mapping, ok := sh.elfReader.(*mmap.ReaderAt); ok { + // Currently only supported for little endian 64 bit ELF Files var chdr64 elf.Chdr64 section := io.NewSectionReader(mapping, int64(sh.Offset), int64(sh.FileSize)) err := binary.Read(section, binary.LittleEndian, &chdr64) @@ -871,7 +872,7 @@ func (sh *Section) Data(maxSize uint) ([]byte, error) { return nil, fmt.Errorf("unsupported compression type %d", elf.CompressionType(chdr64.Type)) } - compressed_section := io.NewSectionReader(mapping, int64(sh.Offset+uint64(binary.Size(chdr64))), int64(chdr64.Size)) + compressed_section := io.NewSectionReader(mapping, int64(sh.Offset + uint64(binary.Size(chdr64))), int64(sh.Size)) zlibReader, err := zlib.NewReader(compressed_section) if err != nil { From 4be7bc5e1aa1f45f8a9ba5fa648882ce98a3b33d Mon Sep 17 00:00:00 2001 From: Dale Hamel Date: Mon, 18 Aug 2025 15:28:33 -0400 Subject: [PATCH 09/14] Improved memory bounding of decompressed elf sections --- libpf/pfelf/dwarf.go | 20 ++++++++++++++++++-- libpf/pfelf/file.go | 7 +++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/libpf/pfelf/dwarf.go b/libpf/pfelf/dwarf.go index 78651cfe0..57c265c0b 100644 --- a/libpf/pfelf/dwarf.go +++ b/libpf/pfelf/dwarf.go @@ -135,16 +135,32 @@ func (data *typeData) Size() int64 { func loadStructData(debugInfo, debugAbbrev, debugStr, debugLineStr *Section, names []string) ([]typeData, error) { results := []typeData{} + if debugInfo == nil { + return nil, fmt.Errorf("all DWARF sections must be available, %s is missing", ".debug_info") + } + if debugAbbrev == nil { + return nil, fmt.Errorf("all DWARF sections must be available, %s is missing", ".debug_abbrev") + } + if debugStr == nil { + return nil, fmt.Errorf("all DWARF sections must be available, %s is missing", ".debug_str") + } + if debugStr == nil { + return nil, fmt.Errorf("all DWARF sections must be available, %s is missing", ".debug_line_str") + } + // To reduce memory usage, we will use the Section's Data() accessor to // get a memory mapped "subslice" and avoid allocations // This prevents a substantial amount of memory bloat that elf.File's DWARF() accessor // otherwise incurs - abbrevData, err := debugAbbrev.Data(maxBytesLargeSection) + // For compressed data, we have to allocate the decompressed buffer but it is + // still cheaper than doing this through the debug/elf's DWARF() helper + // memory usage per section is still bounded to the size of maxBytesLargeSection + debugData, err := debugInfo.Data(maxBytesLargeSection) if err != nil { return nil, err } - debugData, err := debugInfo.Data(maxBytesLargeSection) + abbrevData, err := debugAbbrev.Data(maxBytesLargeSection) if err != nil { return nil, err } diff --git a/libpf/pfelf/file.go b/libpf/pfelf/file.go index 0ddf3b502..76d846f0c 100644 --- a/libpf/pfelf/file.go +++ b/libpf/pfelf/file.go @@ -871,15 +871,18 @@ func (sh *Section) Data(maxSize uint) ([]byte, error) { if elf.CompressionType(chdr64.Type) != elf.COMPRESS_ZLIB { return nil, fmt.Errorf("unsupported compression type %d", elf.CompressionType(chdr64.Type)) } + if chdr64.Size > uint64(maxSize) { + return nil, fmt.Errorf("unable to read full section %s, uncompressed size %d would exceed maximum size %d", sh.Name, chdr64.Size, maxSize) + } - compressed_section := io.NewSectionReader(mapping, int64(sh.Offset + uint64(binary.Size(chdr64))), int64(sh.Size)) + compressed_section := io.NewSectionReader(mapping, int64(sh.Offset+uint64(binary.Size(chdr64))), int64(chdr64.Size)) zlibReader, err := zlib.NewReader(compressed_section) if err != nil { return nil, err } defer zlibReader.Close() - return io.ReadAll(zlibReader) + return io.ReadAll(io.LimitReader(zlibReader, int64(maxSize))) } } From 8190b2015699e9f020ed25657ecaeaca6fbf9bcd Mon Sep 17 00:00:00 2001 From: Dale Hamel Date: Mon, 18 Aug 2025 16:38:43 -0400 Subject: [PATCH 10/14] Calculate ruby structs from DWARF when available --- interpreter/ruby/dwarf.go | 335 ++++++++++++++++++++++++++++++++++++++ interpreter/ruby/ruby.go | 46 +----- libpf/pfelf/dwarf.go | 30 ++-- libpf/pfelf/dwarf_test.go | 7 +- libpf/pfelf/file.go | 2 +- test.go | 40 ----- 6 files changed, 357 insertions(+), 103 deletions(-) create mode 100644 interpreter/ruby/dwarf.go delete mode 100644 test.go diff --git a/interpreter/ruby/dwarf.go b/interpreter/ruby/dwarf.go new file mode 100644 index 000000000..4f2d9f5f3 --- /dev/null +++ b/interpreter/ruby/dwarf.go @@ -0,0 +1,335 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ruby // import "go.opentelemetry.io/ebpf-profiler/interpreter/ruby" + +import ( + "fmt" + "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" +) + +func (r *rubyData) calculateTypesFromDWARF(ef *pfelf.File) error { + referenced_ruby_types := []string{ + "rb_execution_context_struct", + "rb_control_frame_struct", + "rb_iseq_struct", + "rb_iseq_constant_body", + "rb_iseq_location_struct", + "iseq_insn_info_entry", + "RString", + "RArray", + "succ_index_table", + "succ_dict_block", + "VALUE", + } + + if r.version >= rubyVersion(3, 0, 0) { + referenced_ruby_types = append(referenced_ruby_types, "rb_ractor_struct") + } + + type_info, err := ef.TypeData(referenced_ruby_types) + if err != nil { + return err + } + + if len(referenced_ruby_types) != len(type_info) { + return fmt.Errorf("unexpected number of returned types, expected %d, got %d", len(referenced_ruby_types), len(type_info)) + } + + types_by_name := map[string]pfelf.TypeData{} + + for _, info := range type_info { + types_by_name[info.Name] = info + } + + // rb_execution_context_struct fields + rb_execution_context_struct, ok := types_by_name["rb_execution_context_struct"] + if !ok { + return fmt.Errorf("unable to locate struct %s", "rb_execution_context_struct") + } + + vm_stack_offset, err := rb_execution_context_struct.FieldOffset("vm_stack") + if err != nil { + return err + } + r.vmStructs.execution_context_struct.vm_stack = uint8(vm_stack_offset) + + vm_stack_size_offset, err := rb_execution_context_struct.FieldOffset("vm_stack_size") + if err != nil { + return err + } + r.vmStructs.execution_context_struct.vm_stack_size = uint8(vm_stack_size_offset) + + cfp_offset, err := rb_execution_context_struct.FieldOffset("cfp") + if err != nil { + return err + } + r.vmStructs.execution_context_struct.cfp = uint8(cfp_offset) + + // rb_control_frame_struct fields + rb_control_frame_struct, ok := types_by_name["rb_control_frame_struct"] + if !ok { + return fmt.Errorf("unable to locate struct %s", "rb_control_frame_struct") + } + pc_offset, err := rb_control_frame_struct.FieldOffset("pc") + if err != nil { + return err + } + r.vmStructs.control_frame_struct.pc = uint8(pc_offset) + + iseq_offset, err := rb_control_frame_struct.FieldOffset("iseq") + if err != nil { + return err + } + r.vmStructs.control_frame_struct.iseq = uint8(iseq_offset) + + ep_offset, err := rb_control_frame_struct.FieldOffset("ep") + if err != nil { + return err + } + r.vmStructs.control_frame_struct.ep = uint8(ep_offset) + r.vmStructs.control_frame_struct.size_of_control_frame_struct = uint8(rb_control_frame_struct.Size()) + + // rb_iseq_struct fields + rb_iseq_struct, ok := types_by_name["rb_iseq_struct"] + if !ok { + return fmt.Errorf("unable to locate struct %s", "rb_iseq_struct") + } + iseq_body_offset, err := rb_iseq_struct.FieldOffset("body") + if err != nil { + return err + } + r.vmStructs.iseq_struct.body = uint8(iseq_body_offset) + + // rb_iseq_constant_body fields + rb_iseq_constant_body, ok := types_by_name["rb_iseq_constant_body"] + if !ok { + return fmt.Errorf("unable to locate struct %s", "rb_iseq_constant_body") + } + iseq_body_type, err := rb_iseq_constant_body.FieldOffset("type") + if err != nil { + return err + } + r.vmStructs.iseq_constant_body.iseq_type = uint8(iseq_body_type) + + iseq_body_size, err := rb_iseq_constant_body.FieldOffset("iseq_size") + if err != nil { + return err + } + r.vmStructs.iseq_constant_body.size = uint8(iseq_body_size) + + iseq_body_encoded, err := rb_iseq_constant_body.FieldOffset("iseq_encoded") + if err != nil { + return err + } + r.vmStructs.iseq_constant_body.encoded = uint8(iseq_body_encoded) + + iseq_body_location, err := rb_iseq_constant_body.FieldOffset("location") + if err != nil { + return err + } + r.vmStructs.iseq_constant_body.location = uint8(iseq_body_location) + + // insns_info is a sub_struct, calculate its offset as the base for it submembers + iseq_body_insns_info, err := rb_iseq_constant_body.FieldOffset("insns_info") + if err != nil { + return err + } + + iseq_body_insns_info_body, err := rb_iseq_constant_body.FieldOffset("insns_info.body") + if err != nil { + return err + } + r.vmStructs.iseq_constant_body.insn_info_body = uint8(iseq_body_insns_info) + uint8(iseq_body_insns_info_body) + + iseq_body_insns_info_size, err := rb_iseq_constant_body.FieldOffset("insns_info.size") + if err != nil { + return err + } + r.vmStructs.iseq_constant_body.insn_info_size = uint8(iseq_body_insns_info) + uint8(iseq_body_insns_info_size) + + iseq_body_insns_info_succ_index_table, err := rb_iseq_constant_body.FieldOffset("insns_info.succ_index_table") + if err != nil { + return err + } + r.vmStructs.iseq_constant_body.succ_index_table = uint8(iseq_body_insns_info) + uint8(iseq_body_insns_info_succ_index_table) + + r.vmStructs.iseq_constant_body.size_of_iseq_constant_body = uint16(rb_iseq_constant_body.Size()) + + // rb_iseq_location_struct fields + rb_iseq_location_struct, ok := types_by_name["rb_iseq_location_struct"] + if !ok { + return fmt.Errorf("unable to locate struct %s", "rb_iseq_location_struct") + } + pathobj_offset, err := rb_iseq_location_struct.FieldOffset("pathobj") + if err != nil { + return err + } + r.vmStructs.iseq_location_struct.pathobj = uint8(pathobj_offset) + + base_label_offset, err := rb_iseq_location_struct.FieldOffset("base_label") + if err != nil { + return err + } + r.vmStructs.iseq_location_struct.base_label = uint8(base_label_offset) + + // iseq_insn_info_entry fields + iseq_insn_info_entry, ok := types_by_name["iseq_insn_info_entry"] + if !ok { + return fmt.Errorf("unable to locate struct %s", "iseq_insn_info_entry") + } + position_offset, err := iseq_insn_info_entry.FieldOffset("position") + if err != nil { + // removed in 2.6+ + if r.version < rubyVersion(2, 6, 0) { + return err + } + } else { + r.vmStructs.iseq_insn_info_entry.position = uint8(position_offset) + } + + position_size, err := iseq_insn_info_entry.FieldSize("position") + if err != nil { + if r.version < rubyVersion(2, 6, 0) { + return err + } + } else { + r.vmStructs.iseq_insn_info_entry.size_of_position = uint8(position_size) + } + + lineno_offset, err := iseq_insn_info_entry.FieldOffset("line_no") + if err != nil { + return err + } + r.vmStructs.iseq_insn_info_entry.line_no = uint8(lineno_offset) + + lineno_size, err := iseq_insn_info_entry.FieldSize("line_no") + if err != nil { + return err + } + r.vmStructs.iseq_insn_info_entry.size_of_line_no = uint8(lineno_size) + + r.vmStructs.iseq_insn_info_entry.size_of_iseq_insn_info_entry = uint8(iseq_insn_info_entry.Size()) + + // RString fields + rstring, ok := types_by_name["RString"] + if !ok { + return fmt.Errorf("unable to locate struct %s", "RString") + } + rstring_as_offset, err := rstring.FieldOffset("as") + if err != nil { + return err + } + + rstring_as_embed_offset, err := rstring.FieldOffset("as.embed") + if err != nil { + return err + } + + rstring_as_embed_ary_offset, err := rstring.FieldOffset("as.embed.ary") + if err != nil { + return err + } + r.vmStructs.rstring_struct.as_ary = uint8(rstring_as_offset) + uint8(rstring_as_embed_offset) + uint8(rstring_as_embed_ary_offset) + + rstring_as_heap_offset, err := rstring.FieldOffset("as.heap") + if err != nil { + return err + } + + rstring_as_heap_ptr_offset, err := rstring.FieldOffset("as.heap.ptr") + if err != nil { + return err + } + r.vmStructs.rstring_struct.as_heap_ptr = uint8(rstring_as_offset) + uint8(rstring_as_heap_offset) + uint8(rstring_as_heap_ptr_offset) + + // RArray fields + rarray, ok := types_by_name["RArray"] + if !ok { + return fmt.Errorf("unable to locate struct %s", "RArray") + } + + rarray_as_offset, err := rarray.FieldOffset("as") + if err != nil { + return err + } + + rarray_as_ary_offset, err := rarray.FieldOffset("as.ary") + if err != nil { + return err + } + r.vmStructs.rarray_struct.as_ary = uint8(rarray_as_offset) + uint8(rarray_as_ary_offset) + + rarray_as_heap_offset, err := rarray.FieldOffset("as.heap") + if err != nil { + return err + } + + rarray_as_heap_ptr_offset, err := rarray.FieldOffset("as.heap.ptr") + if err != nil { + return err + } + r.vmStructs.rarray_struct.as_heap_ptr = uint8(rarray_as_offset) + uint8(rarray_as_heap_offset) + uint8(rarray_as_heap_ptr_offset) + + // succ_dict_block fields + succ_dict_block, ok := types_by_name["succ_dict_block"] + if !ok { + return fmt.Errorf("unable to locate struct %s", "succ_dict_block") + } + + small_block_ranks_offset, err := succ_dict_block.FieldOffset("small_block_ranks") + if err != nil { + return err + } + r.vmStructs.succ_index_table_struct.small_block_ranks = uint8(small_block_ranks_offset) + + succ_bits_offset, err := succ_dict_block.FieldOffset("bits") + if err != nil { + return err + } + r.vmStructs.succ_index_table_struct.block_bits = uint8(succ_bits_offset) + r.vmStructs.succ_index_table_struct.size_of_succ_dict_block = uint8(succ_dict_block.Size()) + + // succ_index_table fields + succ_index_table, ok := types_by_name["succ_index_table"] + if !ok { + return fmt.Errorf("unable to locate struct %s", "succ_index_table") + } + + succ_part_offset, err := succ_index_table.FieldOffset("succ_part") + if err != nil { + return err + } + r.vmStructs.succ_index_table_struct.succ_part = uint8(succ_part_offset) + + imm_part_size, err := succ_index_table.FieldOffset("imm_part") + if err != nil { + return err + } + r.vmStructs.size_of_immediate_table = uint8((imm_part_size * 9) / 8) // equivalent of "floor division" by 8 after multiplying by 9 + + // rb_ractor_struct not added until ruby 3.0.0+ + if r.version >= rubyVersion(3, 0, 0) { + // rb_ractor_struct fields + rb_ractor_struct, ok := types_by_name["rb_ractor_struct"] + if !ok { + return fmt.Errorf("unable to locate struct %s", "rb_ractor_struct") + } + + running_ec_offset, err := rb_ractor_struct.FieldOffset("running_ec") + if err != nil { + return err + } + r.vmStructs.rb_ractor_struct.running_ec = uint16(running_ec_offset) + } + + // VALUE + rb_value, ok := types_by_name["VALUE"] + if !ok { + return fmt.Errorf("unable to locate type %s", "VALUE") + } + + r.vmStructs.size_of_value = uint8(rb_value.Size()) + + return nil +} diff --git a/interpreter/ruby/ruby.go b/interpreter/ruby/ruby.go index 215acb40e..1e0cdff74 100644 --- a/interpreter/ruby/ruby.go +++ b/interpreter/ruby/ruby.go @@ -233,49 +233,6 @@ func (r *rubyData) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, bias libp func (r *rubyData) Unload(_ interpreter.EbpfHandler) { } -func (r *rubyData) calculateStructsFromDWARF(ef *pfelf.File) error { - - - //Vm_stack: r.vmStructs.execution_context_struct.vm_stack, - //Vm_stack_size: r.vmStructs.execution_context_struct.vm_stack_size, - //Cfp: r.vmStructs.execution_context_struct.cfp, - - //Pc: r.vmStructs.control_frame_struct.pc, - //Iseq: r.vmStructs.control_frame_struct.iseq, - //Ep: r.vmStructs.control_frame_struct.ep, - //Size_of_control_frame_struct: r.vmStructs.control_frame_struct.size_of_control_frame_struct, - - //Body: r.vmStructs.iseq_struct.body, - - //Iseq_size: r.vmStructs.iseq_constant_body.size, - //Iseq_encoded: r.vmStructs.iseq_constant_body.encoded, - - //Size_of_value: r.vmStructs.size_of_value, - - //Running_ec: r.vmStructs.rb_ractor_struct.running_ec, - - referenced_ruby_structs := []string{ - "rb_execution_context_struct", - "rb_control_frame_struct", - "rb_iseq_struct", - "rb_iseq_constant_body", - "rb_iseq_location_struct", - "iseq_insn_info_entry", - "RString", - "RArray", - "succ_index_table", - "succ_dict_block", - } - struct_info, err := ef.StructData() - if err != nil { - return err - } - - // TODO copy the values to populate vmstructs - - return nil -} - // rubyIseqBodyPC holds a reported address to a iseq_constant_body and Ruby VM program counter // combination and is used as key in the cache. type rubyIseqBodyPC struct { @@ -809,7 +766,6 @@ func determineRubyVersion(ef *pfelf.File) (uint32, error) { return rubyVersion(uint32(major), uint32(minor), uint32(release)), nil } - func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) { if !rubyRegex.MatchString(info.FileName()) { return nil, nil @@ -874,7 +830,7 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr vms := &rid.vmStructs - if err := calculateStructsFromDWARF(); err != nil { + if err := rid.calculateTypesFromDWARF(ef); err != nil { // Ruby does not provide introspection data, hard code the struct field offsets. Some // values can be fairly easily calculated from the struct definitions, but some are // looked up by using gdb and getting the field offset directly from debug data. diff --git a/libpf/pfelf/dwarf.go b/libpf/pfelf/dwarf.go index 57c265c0b..dcedfee90 100644 --- a/libpf/pfelf/dwarf.go +++ b/libpf/pfelf/dwarf.go @@ -15,17 +15,17 @@ import ( "strings" ) -type typeData struct { - name string +type TypeData struct { + Name string size int64 structTypeInfo *dwarf.StructType } -func (data typeData) String() string { +func (data TypeData) String() string { var str string = "" if data.structTypeInfo != nil { - str += fmt.Sprintf("\nstruct %s {\n", data.name) + str += fmt.Sprintf("\nstruct %s {\n", data.Name) // Print field information for _, field := range data.structTypeInfo.Field { @@ -41,14 +41,14 @@ func (data typeData) String() string { str += fmt.Sprintf("} // total size: %d bytes", (&data).Size()) str += "\n" } else { - str += fmt.Sprintf("\n%s ", data.name) + str += fmt.Sprintf("\n%s ", data.Name) str += fmt.Sprintf(" // total size: %d bytes", data.size) } return str } -func (data *typeData) FieldOffset(name string) (int64, error) { +func (data *TypeData) FieldOffset(name string) (int64, error) { field, err := data.field(name) if err != nil { return -1, err @@ -56,7 +56,7 @@ func (data *typeData) FieldOffset(name string) (int64, error) { return field.ByteOffset, nil } -func (data *typeData) FieldSize(name string) (int64, error) { +func (data *TypeData) FieldSize(name string) (int64, error) { field, err := data.field(name) if err != nil { return -1, err @@ -64,7 +64,7 @@ func (data *typeData) FieldSize(name string) (int64, error) { return field.Type.Size(), nil } -func (data *typeData) field(name string) (*dwarf.StructField, error) { +func (data *TypeData) field(name string) (*dwarf.StructField, error) { var found *dwarf.StructField = nil parts := strings.Split(name, ".") @@ -103,7 +103,7 @@ func (data *typeData) field(name string) (*dwarf.StructField, error) { return found, nil } -func (data *typeData) Size() int64 { +func (data *TypeData) Size() int64 { if data.structTypeInfo == nil { return data.size } @@ -132,8 +132,8 @@ func (data *typeData) Size() int64 { // This accepts a list of names to look up, as we want to try and get "everything in one go", // since DWARF is inherently O(n) to look up these symbols -func loadStructData(debugInfo, debugAbbrev, debugStr, debugLineStr *Section, names []string) ([]typeData, error) { - results := []typeData{} +func loadStructData(debugInfo, debugAbbrev, debugStr, debugLineStr *Section, names []string) ([]TypeData, error) { + results := []TypeData{} if debugInfo == nil { return nil, fmt.Errorf("all DWARF sections must be available, %s is missing", ".debug_info") @@ -238,8 +238,8 @@ func loadStructData(debugInfo, debugAbbrev, debugStr, debugLineStr *Section, nam continue } - results = append(results, typeData{ - name: name, + results = append(results, TypeData{ + Name: name, structTypeInfo: structTypeInfo, }) @@ -256,8 +256,8 @@ func loadStructData(debugInfo, debugAbbrev, debugStr, debugLineStr *Section, nam return nil, err } if t.Size() > 0 { - results = append(results, typeData{ - name: entry_name, + results = append(results, TypeData{ + Name: entry_name, size: t.Size(), }) processedTypes[entry_name] = struct{}{} diff --git a/libpf/pfelf/dwarf_test.go b/libpf/pfelf/dwarf_test.go index 57c6a0cbb..035e557d2 100644 --- a/libpf/pfelf/dwarf_test.go +++ b/libpf/pfelf/dwarf_test.go @@ -88,6 +88,9 @@ func TestDWARFParseStructs(t *testing.T) { "iseq_encoded": int64(8), "location": int64(64), "insns_info": int64(112), + "insns_info.body": int64(0), + "insns_info.size": int64(16), + "insns_info.succ_index_table": int64(24), }, "iseq_insn_info": map[string]int64{ // substruct of rb_iseq_constant_body, these offsets would be added to the insns_info offset "body": int64(0), @@ -156,10 +159,10 @@ func TestDWARFParseStructs(t *testing.T) { assert.Equal(len(tt.expectedTypes), len(type_data)) - types_by_name := map[string]typeData{} + types_by_name := map[string]TypeData{} for _, struct_info := range type_data { - types_by_name[struct_info.name] = struct_info + types_by_name[struct_info.Name] = struct_info } for _, name := range tt.expectedTypes { diff --git a/libpf/pfelf/file.go b/libpf/pfelf/file.go index 76d846f0c..64b3683f4 100644 --- a/libpf/pfelf/file.go +++ b/libpf/pfelf/file.go @@ -508,7 +508,7 @@ func (f *File) SymbolData(name libpf.SymbolName, maxCopy int) (*libpf.Symbol, [] // Gets TypeData for specific types from DWARF, without loading and parsing // the whole thing -func (f *File) TypeData(names []string) ([]typeData, error) { +func (f *File) TypeData(names []string) ([]TypeData, error) { return loadStructData(f.Section(".debug_info"), f.Section(".debug_abbrev"), f.Section(".debug_str"), f.Section(".debug_line_str"), names) } diff --git a/test.go b/test.go deleted file mode 100644 index b2ca64a4e..000000000 --- a/test.go +++ /dev/null @@ -1,40 +0,0 @@ -package main - - -import ( - "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" - "os" - "fmt" -) - -// /usr/bin/time -v go run test.go /home/dalehamel.linux/.rubies/ruby-3.4.4/bin/ruby rb_execution_context_type rb_control_frame_type rb_iseq_type rb_iseq_constant_body rb_iseq_location_type iseq_insn_info_entry RString RArray succ_dict_block succ_index_table - -func main() { - if len(os.Args) < 2 { - fmt.Fprintf(os.Stderr, "Usage: %s [type-name]\n", os.Args[0]) - os.Exit(1) - } - - elfFile := os.Args[1] - specificTypes := []string{} - if len(os.Args) > 2 { - specificTypes = os.Args[2:] - } - - pf, err := pfelf.Open(elfFile) - if err != nil { - fmt.Fprintf(os.Stderr, "Error opening ELF file for %s: %v\n", specificTypes, err) - os.Exit(1) - } - defer pf.Close() - - data, err := pf.TypeData(specificTypes) - if err != nil { - fmt.Fprintf(os.Stderr, "Error opening ELF file: %v\n", err) - os.Exit(1) - } - - for _, s := range data { - fmt.Printf("%s\n", s) - } -} From b812f2d0ac6866504dd8b7cda85f1192d8a3c940 Mon Sep 17 00:00:00 2001 From: Dale Hamel Date: Mon, 18 Aug 2025 19:41:24 -0400 Subject: [PATCH 11/14] Tidy up comment --- libpf/pfelf/dwarf.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libpf/pfelf/dwarf.go b/libpf/pfelf/dwarf.go index dcedfee90..44b4389c3 100644 --- a/libpf/pfelf/dwarf.go +++ b/libpf/pfelf/dwarf.go @@ -123,7 +123,7 @@ func (data *TypeData) Size() int64 { // Use calculated size if DWARF size is 0 or negative reportedSize := dwarfSize - if dwarfSize <= 0 { //|| (hasFlexibleArray && dwarfSize < flexArrayOffset) { // TODO verify if flex array offset logic actually needed + if dwarfSize <= 0 { reportedSize = calculatedSize } From b7d72cf26e1e7638a191a11088016100835c387e Mon Sep 17 00:00:00 2001 From: Dale Hamel Date: Tue, 19 Aug 2025 13:23:05 -0400 Subject: [PATCH 12/14] Fix segfault guard --- libpf/pfelf/dwarf.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libpf/pfelf/dwarf.go b/libpf/pfelf/dwarf.go index 44b4389c3..d0580055a 100644 --- a/libpf/pfelf/dwarf.go +++ b/libpf/pfelf/dwarf.go @@ -144,7 +144,7 @@ func loadStructData(debugInfo, debugAbbrev, debugStr, debugLineStr *Section, nam if debugStr == nil { return nil, fmt.Errorf("all DWARF sections must be available, %s is missing", ".debug_str") } - if debugStr == nil { + if debugLineStr == nil { return nil, fmt.Errorf("all DWARF sections must be available, %s is missing", ".debug_line_str") } From b3f392df9faf3cf45e2d09c3edd1167320c280f3 Mon Sep 17 00:00:00 2001 From: Dale Hamel Date: Tue, 19 Aug 2025 14:19:09 -0400 Subject: [PATCH 13/14] Linter fixes --- interpreter/ruby/dwarf.go | 38 ++++++++++++++++++++++++++------------ interpreter/ruby/ruby.go | 2 +- libpf/pfelf/dwarf_test.go | 16 ++++++++-------- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/interpreter/ruby/dwarf.go b/interpreter/ruby/dwarf.go index 4f2d9f5f3..9d79ccebd 100644 --- a/interpreter/ruby/dwarf.go +++ b/interpreter/ruby/dwarf.go @@ -5,6 +5,7 @@ package ruby // import "go.opentelemetry.io/ebpf-profiler/interpreter/ruby" import ( "fmt" + "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" ) @@ -33,7 +34,8 @@ func (r *rubyData) calculateTypesFromDWARF(ef *pfelf.File) error { } if len(referenced_ruby_types) != len(type_info) { - return fmt.Errorf("unexpected number of returned types, expected %d, got %d", len(referenced_ruby_types), len(type_info)) + return fmt.Errorf("unexpected number of returned types, expected %d, got %d", + len(referenced_ruby_types), len(type_info)) } types_by_name := map[string]pfelf.TypeData{} @@ -88,7 +90,8 @@ func (r *rubyData) calculateTypesFromDWARF(ef *pfelf.File) error { return err } r.vmStructs.control_frame_struct.ep = uint8(ep_offset) - r.vmStructs.control_frame_struct.size_of_control_frame_struct = uint8(rb_control_frame_struct.Size()) + r.vmStructs.control_frame_struct. + size_of_control_frame_struct = uint8(rb_control_frame_struct.Size()) // rb_iseq_struct fields rb_iseq_struct, ok := types_by_name["rb_iseq_struct"] @@ -140,19 +143,23 @@ func (r *rubyData) calculateTypesFromDWARF(ef *pfelf.File) error { if err != nil { return err } - r.vmStructs.iseq_constant_body.insn_info_body = uint8(iseq_body_insns_info) + uint8(iseq_body_insns_info_body) + r.vmStructs.iseq_constant_body.insn_info_body = uint8(iseq_body_insns_info) + + uint8(iseq_body_insns_info_body) iseq_body_insns_info_size, err := rb_iseq_constant_body.FieldOffset("insns_info.size") if err != nil { return err } - r.vmStructs.iseq_constant_body.insn_info_size = uint8(iseq_body_insns_info) + uint8(iseq_body_insns_info_size) + r.vmStructs.iseq_constant_body.insn_info_size = uint8(iseq_body_insns_info) + + uint8(iseq_body_insns_info_size) - iseq_body_insns_info_succ_index_table, err := rb_iseq_constant_body.FieldOffset("insns_info.succ_index_table") + iseq_body_insns_info_succ_index_table, err := rb_iseq_constant_body. + FieldOffset("insns_info.succ_index_table") if err != nil { return err } - r.vmStructs.iseq_constant_body.succ_index_table = uint8(iseq_body_insns_info) + uint8(iseq_body_insns_info_succ_index_table) + r.vmStructs.iseq_constant_body.succ_index_table = uint8(iseq_body_insns_info) + + uint8(iseq_body_insns_info_succ_index_table) r.vmStructs.iseq_constant_body.size_of_iseq_constant_body = uint16(rb_iseq_constant_body.Size()) @@ -209,7 +216,8 @@ func (r *rubyData) calculateTypesFromDWARF(ef *pfelf.File) error { } r.vmStructs.iseq_insn_info_entry.size_of_line_no = uint8(lineno_size) - r.vmStructs.iseq_insn_info_entry.size_of_iseq_insn_info_entry = uint8(iseq_insn_info_entry.Size()) + r.vmStructs.iseq_insn_info_entry. + size_of_iseq_insn_info_entry = uint8(iseq_insn_info_entry.Size()) // RString fields rstring, ok := types_by_name["RString"] @@ -230,7 +238,8 @@ func (r *rubyData) calculateTypesFromDWARF(ef *pfelf.File) error { if err != nil { return err } - r.vmStructs.rstring_struct.as_ary = uint8(rstring_as_offset) + uint8(rstring_as_embed_offset) + uint8(rstring_as_embed_ary_offset) + r.vmStructs.rstring_struct.as_ary = uint8(rstring_as_offset) + + uint8(rstring_as_embed_offset) + uint8(rstring_as_embed_ary_offset) rstring_as_heap_offset, err := rstring.FieldOffset("as.heap") if err != nil { @@ -241,7 +250,8 @@ func (r *rubyData) calculateTypesFromDWARF(ef *pfelf.File) error { if err != nil { return err } - r.vmStructs.rstring_struct.as_heap_ptr = uint8(rstring_as_offset) + uint8(rstring_as_heap_offset) + uint8(rstring_as_heap_ptr_offset) + r.vmStructs.rstring_struct.as_heap_ptr = uint8(rstring_as_offset) + + uint8(rstring_as_heap_offset) + uint8(rstring_as_heap_ptr_offset) // RArray fields rarray, ok := types_by_name["RArray"] @@ -269,7 +279,8 @@ func (r *rubyData) calculateTypesFromDWARF(ef *pfelf.File) error { if err != nil { return err } - r.vmStructs.rarray_struct.as_heap_ptr = uint8(rarray_as_offset) + uint8(rarray_as_heap_offset) + uint8(rarray_as_heap_ptr_offset) + r.vmStructs.rarray_struct.as_heap_ptr = uint8(rarray_as_offset) + + uint8(rarray_as_heap_offset) + uint8(rarray_as_heap_ptr_offset) // succ_dict_block fields succ_dict_block, ok := types_by_name["succ_dict_block"] @@ -306,12 +317,15 @@ func (r *rubyData) calculateTypesFromDWARF(ef *pfelf.File) error { if err != nil { return err } - r.vmStructs.size_of_immediate_table = uint8((imm_part_size * 9) / 8) // equivalent of "floor division" by 8 after multiplying by 9 + + // equivalent of "floor division" by 8 after multiplying by 9 + r.vmStructs.size_of_immediate_table = uint8((imm_part_size * 9) / 8) // rb_ractor_struct not added until ruby 3.0.0+ if r.version >= rubyVersion(3, 0, 0) { + var rb_ractor_struct pfelf.TypeData // rb_ractor_struct fields - rb_ractor_struct, ok := types_by_name["rb_ractor_struct"] + rb_ractor_struct, ok = types_by_name["rb_ractor_struct"] if !ok { return fmt.Errorf("unable to locate struct %s", "rb_ractor_struct") } diff --git a/interpreter/ruby/ruby.go b/interpreter/ruby/ruby.go index 1e0cdff74..7aa877e23 100644 --- a/interpreter/ruby/ruby.go +++ b/interpreter/ruby/ruby.go @@ -830,7 +830,7 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr vms := &rid.vmStructs - if err := rid.calculateTypesFromDWARF(ef); err != nil { + if err = rid.calculateTypesFromDWARF(ef); err != nil { // Ruby does not provide introspection data, hard code the struct field offsets. Some // values can be fairly easily calculated from the struct definitions, but some are // looked up by using gdb and getting the field offset directly from debug data. diff --git a/libpf/pfelf/dwarf_test.go b/libpf/pfelf/dwarf_test.go index 035e557d2..dc6e7775a 100644 --- a/libpf/pfelf/dwarf_test.go +++ b/libpf/pfelf/dwarf_test.go @@ -83,14 +83,14 @@ func TestDWARFParseStructs(t *testing.T) { "body": int64(16), }, "rb_iseq_constant_body": map[string]int64{ - "type": int64(0), - "iseq_size": int64(4), - "iseq_encoded": int64(8), - "location": int64(64), - "insns_info": int64(112), - "insns_info.body": int64(0), - "insns_info.size": int64(16), - "insns_info.succ_index_table": int64(24), + "type": int64(0), + "iseq_size": int64(4), + "iseq_encoded": int64(8), + "location": int64(64), + "insns_info": int64(112), + "insns_info.body": int64(0), + "insns_info.size": int64(16), + "insns_info.succ_index_table": int64(24), }, "iseq_insn_info": map[string]int64{ // substruct of rb_iseq_constant_body, these offsets would be added to the insns_info offset "body": int64(0), From e55d18d94f1145711d82add58b87c8a9195e25f3 Mon Sep 17 00:00:00 2001 From: Dale Hamel Date: Tue, 19 Aug 2025 14:27:41 -0400 Subject: [PATCH 14/14] More lint fixes --- libpf/pfelf/dwarf.go | 28 +++++++++++++----------- libpf/pfelf/dwarf_test.go | 45 +++++++++++++++++++++------------------ libpf/pfelf/file.go | 16 ++++++++++---- 3 files changed, 51 insertions(+), 38 deletions(-) diff --git a/libpf/pfelf/dwarf.go b/libpf/pfelf/dwarf.go index d0580055a..4620d9448 100644 --- a/libpf/pfelf/dwarf.go +++ b/libpf/pfelf/dwarf.go @@ -15,6 +15,8 @@ import ( "strings" ) +const missingSectionError string = "all DWARF sections must be available, %s is missing" + type TypeData struct { Name string size int64 @@ -48,7 +50,7 @@ func (data TypeData) String() string { return str } -func (data *TypeData) FieldOffset(name string) (int64, error) { +func (data TypeData) FieldOffset(name string) (int64, error) { field, err := data.field(name) if err != nil { return -1, err @@ -56,7 +58,7 @@ func (data *TypeData) FieldOffset(name string) (int64, error) { return field.ByteOffset, nil } -func (data *TypeData) FieldSize(name string) (int64, error) { +func (data TypeData) FieldSize(name string) (int64, error) { field, err := data.field(name) if err != nil { return -1, err @@ -64,7 +66,7 @@ func (data *TypeData) FieldSize(name string) (int64, error) { return field.Type.Size(), nil } -func (data *TypeData) field(name string) (*dwarf.StructField, error) { +func (data TypeData) field(name string) (*dwarf.StructField, error) { var found *dwarf.StructField = nil parts := strings.Split(name, ".") @@ -86,7 +88,6 @@ func (data *TypeData) field(name string) (*dwarf.StructField, error) { } } } - } else { for _, field := range data.structTypeInfo.Field { if field.Name == name { @@ -103,7 +104,7 @@ func (data *TypeData) field(name string) (*dwarf.StructField, error) { return found, nil } -func (data *TypeData) Size() int64 { +func (data TypeData) Size() int64 { if data.structTypeInfo == nil { return data.size } @@ -132,20 +133,21 @@ func (data *TypeData) Size() int64 { // This accepts a list of names to look up, as we want to try and get "everything in one go", // since DWARF is inherently O(n) to look up these symbols -func loadStructData(debugInfo, debugAbbrev, debugStr, debugLineStr *Section, names []string) ([]TypeData, error) { +func loadStructData(debugInfo, debugAbbrev, debugStr, debugLineStr *Section, + names []string) ([]TypeData, error) { results := []TypeData{} if debugInfo == nil { - return nil, fmt.Errorf("all DWARF sections must be available, %s is missing", ".debug_info") + return nil, fmt.Errorf(missingSectionError, ".debug_info") } if debugAbbrev == nil { - return nil, fmt.Errorf("all DWARF sections must be available, %s is missing", ".debug_abbrev") + return nil, fmt.Errorf(missingSectionError, ".debug_abbrev") } if debugStr == nil { - return nil, fmt.Errorf("all DWARF sections must be available, %s is missing", ".debug_str") + return nil, fmt.Errorf(missingSectionError, ".debug_str") } if debugLineStr == nil { - return nil, fmt.Errorf("all DWARF sections must be available, %s is missing", ".debug_line_str") + return nil, fmt.Errorf(missingSectionError, ".debug_line_str") } // To reduce memory usage, we will use the Section's Data() accessor to @@ -211,7 +213,7 @@ func loadStructData(debugInfo, debugAbbrev, debugStr, debugLineStr *Section, nam } name := nameVal - if _, ok := processedTypes[name]; ok { + if _, ok = processedTypes[name]; ok { continue } @@ -244,8 +246,8 @@ func loadStructData(debugInfo, debugAbbrev, debugStr, debugLineStr *Section, nam }) processedTypes[name] = struct{}{} - } else if entry_name, ok := entry.Val(dwarf.AttrName).(string); ok && slices.Contains(names, entry_name) { - + } else if entry_name, ok := entry.Val(dwarf.AttrName).(string); ok && + slices.Contains(names, entry_name) { if _, ok := processedTypes[entry_name]; ok { continue } diff --git a/libpf/pfelf/dwarf_test.go b/libpf/pfelf/dwarf_test.go index dc6e7775a..4a5184efc 100644 --- a/libpf/pfelf/dwarf_test.go +++ b/libpf/pfelf/dwarf_test.go @@ -31,13 +31,13 @@ func TestDWARFParseStructs(t *testing.T) { "some_typedef": 8, }, expectedFieldOffsets: map[string]map[string]int64{ - "some_struct": map[string]int64{ + "some_struct": { "some_array": int64(0), "some_int": int64(64), }, }, expectedFieldSizes: map[string]map[string]int64{ - "some_struct": map[string]int64{ + "some_struct": { "some_array": int64(64), "some_int": int64(8), }, @@ -69,20 +69,20 @@ func TestDWARFParseStructs(t *testing.T) { "VALUE": 8, }, expectedFieldOffsets: map[string]map[string]int64{ - "rb_execution_context_struct": map[string]int64{ + "rb_execution_context_struct": { "vm_stack": int64(0), "vm_stack_size": int64(8), "cfp": int64(16), }, - "rb_control_frame_struct": map[string]int64{ + "rb_control_frame_struct": { "pc": int64(0), "iseq": int64(16), "ep": int64(32), }, - "rb_iseq_struct": map[string]int64{ + "rb_iseq_struct": { "body": int64(16), }, - "rb_iseq_constant_body": map[string]int64{ + "rb_iseq_constant_body": { "type": int64(0), "iseq_size": int64(4), "iseq_encoded": int64(8), @@ -92,54 +92,57 @@ func TestDWARFParseStructs(t *testing.T) { "insns_info.size": int64(16), "insns_info.succ_index_table": int64(24), }, - "iseq_insn_info": map[string]int64{ // substruct of rb_iseq_constant_body, these offsets would be added to the insns_info offset + // substruct of rb_iseq_constant_body + // these offsets would be added to the insns_info offset + "iseq_insn_info": { "body": int64(0), "size": int64(16), "succ_index_table": int64(24), }, - "rb_iseq_location_struct": map[string]int64{ + "rb_iseq_location_struct": { "pathobj": int64(0), "base_label": int64(8), }, - "iseq_insn_info_entry": map[string]int64{ + "iseq_insn_info_entry": { // position was removed in 3.1 "line_no": int64(0), }, - "RString": map[string]int64{ + "RString": { "as": int64(24), "as.embed": int64(0), "as.embed.ary": int64(0), "as.heap": int64(0), "as.heap.ptr": int64(0), }, - "RArray": map[string]int64{ + "RArray": { "as": int64(16), "as.ary": int64(0), "as.heap": int64(0), "as.heap.ptr": int64(16), }, - "succ_index_table": map[string]int64{ + "succ_index_table": { "succ_part": int64(48), }, - "succ_dict_block": map[string]int64{ + "succ_dict_block": { "small_block_ranks": int64(8), "bits": int64(16), }, - "rb_ractor_struct": map[string]int64{ + "rb_ractor_struct": { "threads": int64(264), "threads.running_ec": int64(136), }, }, expectedFieldSizes: map[string]map[string]int64{ - "rb_execution_context_struct": map[string]int64{ + "rb_execution_context_struct": { "vm_stack": int64(8), }, - "iseq_insn_info_entry": map[string]int64{ + "iseq_insn_info_entry": { // position was removed in 3.1 "line_no": int64(4), }, - "succ_index_table": map[string]int64{ - "imm_part": int64(48), // note that in python they multiple this by 9, then divide by 8 https://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/078ae4d6ded761b513038440bc8525014fa6c016/tools/coredump/testsources/ruby/gdb-dump-offsets.py#L69 because of https://github.com/Shopify/ruby/blob/70b4b6fea0eeb66647539bcb3b9a50d027d92e51/iseq.c#L4265 + "succ_index_table": { + // note that in python they multiple this by 9, then divide by 8 + "imm_part": int64(48), "succ_part": int64(0), }, }, @@ -157,7 +160,7 @@ func TestDWARFParseStructs(t *testing.T) { type_data, err := elfFile.TypeData(tt.expectedTypes) require.NoError(err) - assert.Equal(len(tt.expectedTypes), len(type_data)) + assert.Len(type_data, len(tt.expectedTypes)) types_by_name := map[string]TypeData{} @@ -183,7 +186,7 @@ func TestDWARFParseStructs(t *testing.T) { for field, expected_offset := range fields { actual_offset, err := struct_info.FieldOffset(field) - assert.NoError(err) + require.NoError(err) assert.Equal(expected_offset, actual_offset) } @@ -195,7 +198,7 @@ func TestDWARFParseStructs(t *testing.T) { for field, expected_size := range fields { actual_size, err := struct_info.FieldSize(field) - assert.NoError(err) + require.NoError(err) assert.Equal(expected_size, actual_size) } diff --git a/libpf/pfelf/file.go b/libpf/pfelf/file.go index 64b3683f4..f642f5621 100644 --- a/libpf/pfelf/file.go +++ b/libpf/pfelf/file.go @@ -509,7 +509,11 @@ func (f *File) SymbolData(name libpf.SymbolName, maxCopy int) (*libpf.Symbol, [] // Gets TypeData for specific types from DWARF, without loading and parsing // the whole thing func (f *File) TypeData(names []string) ([]TypeData, error) { - return loadStructData(f.Section(".debug_info"), f.Section(".debug_abbrev"), f.Section(".debug_str"), f.Section(".debug_line_str"), names) + return loadStructData(f.Section(".debug_info"), + f.Section(".debug_abbrev"), + f.Section(".debug_str"), + f.Section(".debug_line_str"), + names) } // ReadVirtualMemory reads bytes from given virtual address @@ -869,13 +873,17 @@ func (sh *Section) Data(maxSize uint) ([]byte, error) { } if elf.CompressionType(chdr64.Type) != elf.COMPRESS_ZLIB { - return nil, fmt.Errorf("unsupported compression type %d", elf.CompressionType(chdr64.Type)) + return nil, fmt.Errorf("unsupported compression type %d", + elf.CompressionType(chdr64.Type)) } if chdr64.Size > uint64(maxSize) { - return nil, fmt.Errorf("unable to read full section %s, uncompressed size %d would exceed maximum size %d", sh.Name, chdr64.Size, maxSize) + return nil, fmt.Errorf("unable to read full section %s,"+ + "uncompressed size %d would exceed maximum size %d", + sh.Name, chdr64.Size, maxSize) } - compressed_section := io.NewSectionReader(mapping, int64(sh.Offset+uint64(binary.Size(chdr64))), int64(chdr64.Size)) + compressed_section := io.NewSectionReader(mapping, + int64(sh.Offset+uint64(binary.Size(chdr64))), int64(chdr64.Size)) zlibReader, err := zlib.NewReader(compressed_section) if err != nil {