diff --git a/src/debug/gosym/v2/cli/main.go b/src/debug/gosym/v2/cli/main.go new file mode 100644 index 00000000000000..f7ec67d4c79ba7 --- /dev/null +++ b/src/debug/gosym/v2/cli/main.go @@ -0,0 +1,199 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +package main + +import ( + "debug/gosym/v2" + "flag" + "fmt" + "os" + "strconv" +) + +var ( + inlines = flag.Bool("inlines", false, "List inline functions") + lines = flag.Bool("lines", false, "List function lines") +) + +func functions(table *gosym.Table) error { + for f := range table.Functions() { + name, err := f.Name() + if err != nil { + return err + } + entry, err := f.Entry() + if err != nil { + return err + } + end, err := f.End() + if err != nil { + return err + } + deferreturn, err := f.DeferReturn() + if err != nil { + return err + } + file, err := f.File() + if err != nil { + return err + } + startLine, err := f.StartLine() + if err != nil { + return err + } + fmt.Printf("%s@%s:%d pc=[0x%x, 0x%x) deferreturn=0x%x\n", name.Value(), file.Value(), startLine, entry, end, deferreturn) + } + return nil +} + +func describe(table *gosym.Table, pc uint64, inlines, lines bool) error { + f, err := table.ResolveFunction(pc) + name, err := f.Name() + if err != nil { + return err + } + entry, err := f.Entry() + if err != nil { + return err + } + end, err := f.End() + if err != nil { + return err + } + deferreturn, err := f.DeferReturn() + if err != nil { + return err + } + file, err := f.File() + if err != nil { + return err + } + startLine, err := f.StartLine() + if err != nil { + return err + } + fmt.Printf("%s@%s:%d pc=[0x%x, 0x%x) deferreturn=0x%x\n", + name.Value(), file.Value(), startLine, entry, end, deferreturn) + + if inlines { + inlines, err := f.InlineFunctions(nil) + if err != nil { + return err + } + if len(inlines) == 0 { + fmt.Println(" (no inlines)") + } + for _, inline := range inlines { + fmt.Printf(" %s@%s:%d\n", + inline.Name.Value(), inline.File.Value(), inline.StartLine) + } + } + if lines { + r, err := f.Lines(gosym.LinesResult{}) + if err != nil { + return err + } + if len(r.FunctionLines) == 0 { + fmt.Println(" (no lines)") + } + for _, line := range r.FunctionLines { + fmt.Printf(" [0x%x, 0x%x) %s@%s:%d parentPC=0x%x\n", + line.PCLo, line.PCHi, line.Name.Value(), line.File.Value(), line.Line, line.ParentPC) + } + } + return nil +} + +func locate(table *gosym.Table, pc uint64) error { + locs, err := table.ResolveLocations(pc, nil) + if err != nil { + return err + } + for _, loc := range locs { + fmt.Printf("%s@%s:%d\n", loc.Function.Value(), loc.File.Value(), loc.Line) + } + return nil +} + +func main() { + usage := func() { + fmt.Printf(`Usage: +$ %s functions # List all function symbols in this binary +$ %s describe (--inlines)? (--lines)? # Describe a function at a specific pc +$ %s locate # Locate a pc +`, os.Args[0], os.Args[0], os.Args[0]) + os.Exit(1) + } + if len(os.Args) < 3 { + fmt.Printf("missing subcommand or binary argument\n") + usage() + os.Exit(1) + } + + binary := os.Args[2] + reader, err := os.Open(binary) + if err != nil { + fmt.Printf("failed to open binary: %v\n", err) + os.Exit(1) + } + defer reader.Close() + table, err := gosym.NewMagic(reader) + if err != nil { + fmt.Printf("failed to parse binary: %v\n", err) + os.Exit(1) + } + + var pc uint64 + if len(os.Args) > 3 { + pc, err = strconv.ParseUint(os.Args[3], 0, 64) + if err != nil { + fmt.Printf("failed to parse pc: %v\n", err) + os.Exit(1) + } + } + + subcommand := os.Args[1] + switch subcommand { + case "functions": + if len(os.Args) != 3 { + fmt.Printf("functions expects 1 argument, got %d\n", len(os.Args)-2) + usage() + os.Exit(1) + } + err := functions(table) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + case "describe": + if len(os.Args) < 4 { + fmt.Printf("describe expects 2 arguments, got %d\n", len(os.Args)-2) + usage() + os.Exit(1) + } + flag.CommandLine.Parse(os.Args[4:]) + err := describe(table, pc, *inlines, *lines) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + case "locate": + if len(os.Args) < 4 { + fmt.Printf("locate expects 2 arguments, got %d\n", len(os.Args)-2) + usage() + os.Exit(1) + } + err := locate(table, pc) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + default: + fmt.Printf("unknown subcommand: %s\n", subcommand) + usage() + os.Exit(1) + } +} diff --git a/src/debug/gosym/v2/elf.go b/src/debug/gosym/v2/elf.go new file mode 100644 index 00000000000000..ae4a4c403ab4d4 --- /dev/null +++ b/src/debug/gosym/v2/elf.go @@ -0,0 +1,42 @@ +package gosym + +import ( + "debug/elf" + "encoding/binary" + "fmt" +) + +type elfObject struct { + *elf.File +} + +func (o elfObject) Endian() binary.ByteOrder { + return o.ByteOrder +} + +func (o elfObject) Sections() (headers []SectionHeader, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("%w: failed to parse section headers: %v", ErrCorrupted, r) + } + }() + headers = make([]SectionHeader, 0, len(o.File.Sections)) + for _, s := range o.File.Sections { + headers = append(headers, SectionHeader{ + Name: s.Name, + Addr: s.Addr, + Size: s.Size, + }) + } + return +} + +func (o elfObject) SectionData(i int8) (data []byte, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("%w: failed to read section: %d: %v", ErrCorrupted, i, r) + } + }() + data, err = o.File.Sections[i].Data() + return +} diff --git a/src/debug/gosym/v2/funcdata.go b/src/debug/gosym/v2/funcdata.go new file mode 100644 index 00000000000000..834f9705b9ca28 --- /dev/null +++ b/src/debug/gosym/v2/funcdata.go @@ -0,0 +1,146 @@ +package gosym + +import ( + "encoding/binary" + "fmt" + "math" +) + +func funcdataEntryOffsetSize(version version, ptrSize uint8) uint64 { + switch version { + case ver12, ver116: + return uint64(ptrSize) + default: + return 4 + } +} + +func (t *Table) funcdataFieldOffset(field uint64) uint64 { + return funcdataEntryOffsetSize(t.version, t.ptrSize) + (field-1)*4 +} + +func (t *Table) funcdataField(funcOff uint64, field uint64) (uint32, error) { + offset := t.funcdata[0] + funcOff + t.funcdataFieldOffset(field) + if offset+4 > t.funcdata[1] { + return 0, fmt.Errorf("%w: function data out of bounds: off=%d field=%d", ErrCorrupted, offset, field) + } + return binary.LittleEndian.Uint32(t.pclntab[offset:]), nil +} + +func (t *Table) funcdataNameOff(funcOff uint64) (uint32, error) { + return t.funcdataField(funcOff, 1) +} + +func (t *Table) funcdataDeferReturn(funcOff uint64) (uint32, error) { + return t.funcdataField(funcOff, 3) +} + +func (t *Table) funcdataPcfile(funcOff uint64) (uint32, error) { + return t.funcdataField(funcOff, 5) +} + +func (t *Table) funcdataPcln(funcOff uint64) (uint32, error) { + return t.funcdataField(funcOff, 6) +} + +func (t *Table) funcdataNpcdata(funcOff uint64) (uint32, error) { + return t.funcdataField(funcOff, 7) +} + +func (t *Table) funcdataCuOffset(funcOff uint64) (uint32, error) { + return t.funcdataField(funcOff, 8) +} + +func (t *Table) funcdataStartLine(funcOff uint64) (uint32, error) { + line, err := t.funcdataField(funcOff, 9) + if err != nil { + return 0, err + } + return line, nil +} + +func (t *Table) funcdataFuncID(funcOff uint64) (uint8, error) { + funcIDOff := t.funcdata[0] + funcOff + t.funcdataFieldOffset(10) + if funcIDOff >= t.funcdata[1] { + return 0, fmt.Errorf("%w: function data out of bounds, off=%d", ErrCorrupted, funcIDOff) + } + return t.pclntab[funcIDOff], nil +} + +func (t *Table) funcdataFileNoToOff(funcOff uint64, fno uint32) (uint32, error) { + if t.version == ver12 { + return uint32(fno * 4), nil + } + cuOff, err := t.funcdataCuOffset(funcOff) + if err != nil { + return 0, err + } + offOff := t.cutab[0] + uint64(cuOff)*4 + uint64(fno)*4 + if offOff+4 > t.cutab[1] { + return 0, fmt.Errorf("%w: file offset out of bounds", ErrCorrupted) + } + off := binary.LittleEndian.Uint32(t.pclntab[offOff:]) + if off == math.MaxUint32 { + // Valid for non-function entries. We skip them at higher levels, + // so here we can return an error. + return 0, fmt.Errorf("%w: no file entry", ErrCorrupted) + } + return off, nil +} + +func (t *Table) funcdataFileNo(funcOff uint64) (uint32, error) { + pcfile, err := t.funcdataPcfile(funcOff) + if err != nil { + return 0, err + } + fno, err := t.firstPcValue(pcfile) + if err != nil { + return 0, err + } + return uint32(fno), nil +} + +func (t *Table) funcdataFileOff(funcOff uint64) (uint32, error) { + fno, err := t.funcdataFileNo(funcOff) + if err != nil { + return 0, err + } + return t.funcdataFileNoToOff(funcOff, uint32(fno)) +} + +func (t *Table) funcdataInlTreeIndex(funcOff uint64) (uint32, error) { + npcdata, err := t.funcdataNpcdata(funcOff) + if err != nil { + return 0, err + } + const pcdataInlTreeIndex = 2 + if pcdataInlTreeIndex >= npcdata { + return 0, fmt.Errorf("%w: inl tree index out of bounds, off=%d", ErrCorrupted, funcOff) + } + return t.funcdataField(funcOff, uint64(11+pcdataInlTreeIndex)) +} + +func (t *Table) funcdataInlTree(funcOff uint64) ([]byte, error) { + npcdata, err := t.funcdataNpcdata(funcOff) + if err != nil { + return nil, err + } + nfuncdataOff := t.funcdata[0] + funcOff + t.funcdataFieldOffset(11) + if nfuncdataOff >= t.funcdata[1] { + return nil, fmt.Errorf("%w: function data out of bounds, off=%d", ErrCorrupted, nfuncdataOff) + } + nfuncdata := t.pclntab[nfuncdataOff] + const funcdataInlTree = 3 + if funcdataInlTree >= nfuncdata { + return nil, fmt.Errorf("%w: inl tree out of bounds, off=%d", ErrCorrupted, funcOff) + } + inlTreeOff, err := t.funcdataField(funcOff, uint64(11+npcdata+funcdataInlTree)) + if err != nil { + return nil, err + } + if inlTreeOff == math.MaxUint32 { + // Valid case - this function has no inline functions + return nil, nil + } + return t.gofunc[inlTreeOff:], nil +} diff --git a/src/debug/gosym/v2/functab.go b/src/debug/gosym/v2/functab.go new file mode 100644 index 00000000000000..cb479d1eedcd86 --- /dev/null +++ b/src/debug/gosym/v2/functab.go @@ -0,0 +1,73 @@ +package gosym + +import ( + "encoding/binary" + "fmt" +) + +func functabFieldSize(version version, ptrSize uint8) uint64 { + switch version { + case ver12, ver116: + return uint64(ptrSize) + default: + return 4 + } +} + +func (t *Table) functabCount() uint32 { + return t.nfunctab +} + +func (t *Table) functabField(idx uint32, field uint32) (uint64, error) { + fieldSize := functabFieldSize(t.version, t.ptrSize) + offset := t.functab[0] + uint64(2*idx+field)*fieldSize + + if offset+fieldSize > t.functab[1] { + return 0, fmt.Errorf("%w: function table entry out of bounds", ErrCorrupted) + } + + var pc uint64 + if fieldSize == 4 { + pc = uint64(binary.LittleEndian.Uint32(t.pclntab[offset:])) + } else { + pc = binary.LittleEndian.Uint64(t.pclntab[offset:]) + } + + return pc, nil +} + +func (t *Table) functabPc(idx uint32) (uint64, error) { + pc, err := t.functabField(idx, 0) + if err != nil { + return 0, err + } + pc += t.textRange[0] + return pc, nil +} + +func (t *Table) functabOff(idx uint32) (uint64, error) { + return t.functabField(idx, 1) +} + +func (t *Table) functabIdxByPc(pc uint64) (uint32, error) { + if pc < t.pcRange[0] || pc >= t.pcRange[1] { + return 0, fmt.Errorf("%w: pc out of global range", ErrPcNotFound) + } + + lo := uint32(0) + hi := t.functabCount() + for hi-lo > 1 { + mid := (lo + hi) / 2 + midPC, err := t.functabPc(mid) + if err != nil { + return 0, err + } + if pc < midPC { + hi = mid + } else { + lo = mid + } + } + + return lo, nil +} diff --git a/src/debug/gosym/v2/gosym.go b/src/debug/gosym/v2/gosym.go new file mode 100644 index 00000000000000..47dabf118b1860 --- /dev/null +++ b/src/debug/gosym/v2/gosym.go @@ -0,0 +1,238 @@ +// Package gosym implements access to the Go symbol tables embedded in Go binaries. +package gosym + +import ( + "debug/elf" + "debug/macho" + "encoding/binary" + "errors" + "fmt" + "io" + "iter" + "unique" +) + +var ( + // ErrPcNotFound is returned when: + // - PC doesn't belong to any function in case of table search + // - PC doesn't belong to the given function in case of function search + ErrPcNotFound = errors.New("pc not found") + // ErrCorrupted is returned in wide range of cases of binary data inconsistencies. + ErrCorrupted = errors.New("binary data is corrupted") +) + +// Table represents the Go symbols table. +type Table struct { + metadata + pclntab []byte + gofunc []byte +} + +// NewELF creates a new table from an ELF file. +func NewELF(elf *elf.File) (*Table, error) { + return parseObject(elfObject{elf}) +} + +// NewMacho creates a new table from a Mach-O file. +func NewMacho(mach *macho.File) (*Table, error) { + return parseObject(machObject{mach}) +} + +// TODO: support PE and Plan9 objects +// func NewPE(*pe.File) (*Table, error) +// func NewPlan9Obj(*plan9obj.File) (*Table, error) + +// NewMagic creates a new table from an object file, auto-detecting amongs the supported formats. +func NewMagic(r io.ReaderAt) (*Table, error) { + return newMagic(r) +} + +// NewObject creates a new table from an abstract representation of an object file. +func NewObject(obj Object) (*Table, error) { + return parseObject(obj) +} + +// Object represents an object file. +type Object interface { + Endian() binary.ByteOrder + Sections() ([]SectionHeader, error) + SectionData(i int8) ([]byte, error) +} + +// SectionHeader represents a header of a section in an object file. +type SectionHeader struct { + Name string + Addr uint64 + Size uint64 +} + +// ResolveLocations resolves source code locations associated with a given PC. +// Returns more than one item only in case of inline functions, which are +// returned in inner to outer-most inlining order. +func (t *Table) ResolveLocations(pc uint64, buf []Location) ([]Location, error) { + f, err := t.ResolveFunction(pc) + if err != nil { + return buf, err + } + return f.resolveLocations(pc, buf) +} + +// Location represents a source code location. +type Location struct { + Function unique.Handle[string] + File unique.Handle[string] + Line uint32 +} + +// Functions lists all functions in the table. Note that some functions +// may be inlined by the compiler, and not appear directly in the table. +// These are accessible by listing inline functions. +func (t *Table) Functions() iter.Seq2[Function, error] { + return func(yield func(Function, error) bool) { + for i := uint32(0); i < t.functabCount(); i++ { + off, err := t.functabOff(i) + if err != nil { + if !yield(Function{}, err) { + return + } + } + cuOff, err := t.funcdataCuOffset(off) + if err != nil { + if !yield(Function{}, err) { + return + } + } + if cuOff == ^uint32(0) { + // Non-function entry. + continue + } + if !yield(Function{ + table: t, + idx: i, + offset: off, + }, nil) { + return + } + } + } +} + +// ResolveFunction resolves function symbol that the given pc corresponds to. +func (t *Table) ResolveFunction(pc uint64) (Function, error) { + idx, err := t.functabIdxByPc(pc) + if err != nil { + return Function{}, err + } + off, err := t.functabOff(idx) + if err != nil { + return Function{}, err + } + cuOff, err := t.funcdataCuOffset(off) + if err != nil { + return Function{}, err + } + if cuOff == ^uint32(0) { + return Function{}, fmt.Errorf("%w: pc not in a function", ErrPcNotFound) + } + f := Function{ + table: t, + idx: idx, + offset: off, + } + end, err := f.End() + if err != nil { + return Function{}, err + } + if pc >= end { + return Function{}, fmt.Errorf("%w: pc not in a function", ErrPcNotFound) + } + return f, nil +} + +// Function represents a function entry in the table. +type Function struct { + table *Table + idx uint32 + offset uint64 +} + +// Name returns the name of the function. +func (f *Function) Name() (unique.Handle[string], error) { + nameOff, err := f.table.funcdataNameOff(f.offset) + if err != nil { + return unique.Handle[string]{}, err + } + return f.table.funcName(nameOff), nil +} + +// Entry returns the lowest PC of the function. +func (f *Function) Entry() (uint64, error) { + return f.table.functabPc(f.idx) +} + +// End returns the PC past the end of the function. +func (f *Function) End() (uint64, error) { + return f.endPc() +} + +// DeferReturn returns the deferreturn address if any, 0 otherwise. +func (f *Function) DeferReturn() (uint32, error) { + return f.table.funcdataDeferReturn(f.offset) +} + +// File returns the file that the function is defined in. +func (f *Function) File() (unique.Handle[string], error) { + off, err := f.table.funcdataFileOff(f.offset) + if err != nil { + return unique.Handle[string]{}, err + } + return f.table.fileName(off), nil +} + +// StartLine returns the line number the function is defined at. +func (f *Function) StartLine() (uint32, error) { + return f.table.funcdataStartLine(f.offset) +} + +// ResolveLocations resolves source code locations associated with a given PC +// that belongs to the function. Returns more than one item only in case of +// inline functions, which are returned in inner to outer-most inlining order. +func (f *Function) ResolveLocations(pc uint64, buf []Location) ([]Location, error) { + return f.resolveLocations(pc, buf) +} + +// InlineFunctions lists all functions that have been inlined in this function (directly or indirectly). +func (f *Function) InlineFunctions(buf []InlineFunction) ([]InlineFunction, error) { + return f.inlines(buf) +} + +// InlineFunction represents a function that has been inlined. +type InlineFunction struct { + Name unique.Handle[string] + File unique.Handle[string] + StartLine uint32 +} + +// Lines resolves all source code locations of the function. +func (f *Function) Lines(buf LinesResult) (LinesResult, error) { + return f.lines(buf) +} + +// LinesResult represents the result of function lines resolution. The +// object may be reused for any subsequent calls to optimize allocations. +// FunctionLines slice must be copied if access to its elements is needed +// after the result object is re-used for subsequent calls. +type LinesResult struct { + FunctionLines []FunctionLine + linesCache linesCache +} + +// FunctionLine represents a PC range that correpsonds to a specific source code location. +type FunctionLine struct { + PCLo uint64 + PCHi uint64 + Name unique.Handle[string] + File unique.Handle[string] + Line uint32 + ParentPC uint64 +} diff --git a/src/debug/gosym/v2/macho.go b/src/debug/gosym/v2/macho.go new file mode 100644 index 00000000000000..69091e30dacb06 --- /dev/null +++ b/src/debug/gosym/v2/macho.go @@ -0,0 +1,42 @@ +package gosym + +import ( + "debug/macho" + "encoding/binary" + "fmt" +) + +type machObject struct { + *macho.File +} + +func (o machObject) Endian() binary.ByteOrder { + return o.ByteOrder +} + +func (o machObject) Sections() (headers []SectionHeader, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("%w: failed to parse section headers: %v", ErrCorrupted, r) + } + }() + headers = make([]SectionHeader, 0, len(o.File.Sections)) + for _, s := range o.File.Sections { + headers = append(headers, SectionHeader{ + Name: s.Name, + Addr: s.Addr, + Size: s.Size, + }) + } + return +} + +func (o machObject) SectionData(i int8) (data []byte, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("%w: failed to read section: %d: %v", ErrCorrupted, i, r) + } + }() + data, err = o.File.Sections[i].Data() + return +} diff --git a/src/debug/gosym/v2/magic.go b/src/debug/gosym/v2/magic.go new file mode 100644 index 00000000000000..fc0d3680adfe11 --- /dev/null +++ b/src/debug/gosym/v2/magic.go @@ -0,0 +1,46 @@ +package gosym + +import ( + "bytes" + "debug/elf" + "debug/macho" + "encoding/binary" + "fmt" + "io" +) + +func newMagic(r io.ReaderAt) (*Table, error) { + magicBytes := make([]byte, 4) + _, err := r.ReadAt(magicBytes, 0) + if err != nil { + return nil, err + } + if bytes.Equal(magicBytes, []byte(elf.ELFMAG)) { + elf, err := elf.NewFile(r) + if err != nil { + return nil, err + } + return NewELF(elf) + } + magicLe := binary.LittleEndian.Uint32(magicBytes) + magicBe := binary.BigEndian.Uint32(magicBytes) + + switch magicLe { + case macho.Magic32, macho.Magic64, macho.MagicFat: + macho, err := macho.NewFile(r) + if err != nil { + return nil, err + } + return NewMacho(macho) + } + switch magicBe { + case macho.Magic32, macho.Magic64, macho.MagicFat: + macho, err := macho.NewFile(r) + if err != nil { + return nil, err + } + return NewMacho(macho) + } + + return nil, fmt.Errorf("%w: unrecognized magic: %x", ErrCorrupted, magicBytes) +} diff --git a/src/debug/gosym/v2/pcdata.go b/src/debug/gosym/v2/pcdata.go new file mode 100644 index 00000000000000..79eace81dc4538 --- /dev/null +++ b/src/debug/gosym/v2/pcdata.go @@ -0,0 +1,417 @@ +package gosym + +import ( + "cmp" + "encoding/binary" + "fmt" + "iter" + "slices" + "unique" +) + +func (f *Function) endPc() (uint64, error) { + // There is no direct way to get the end pc of a function. + // We need to iterate one of pc sequences. Shortest should be the pcfile. + t := f.table + offset, err := t.funcdataPcfile(f.offset) + if err != nil { + return 0, err + } + pc, err := t.functabPc(f.idx) + if err != nil { + return 0, err + } + return t.lastPc(offset, pc) +} + +// Parse inline tree. +func (f *Function) visitInlTree( + entryPc uint64, + includeSelf bool, + visitor func( + pcLo, pcHi uint64, + funcNameOff, fileNo, startLine uint32, + parentPC uint64, + ) error, +) error { + t := f.table + + var selfNameOff uint32 + var selfStartLine uint32 + if includeSelf { + var err error + selfNameOff, err = t.funcdataNameOff(f.offset) + if err != nil { + return err + } + selfStartLine, err = t.funcdataStartLine(f.offset) + if err != nil { + return err + } + } + + offset, err := t.funcdataPcfile(f.offset) + if err != nil { + return err + } + pcFileIter, err := t.iterPCValues(offset, entryPc) + if err != nil { + return err + } + nextFile, stopFile := iter.Pull(pcFileIter) + defer stopFile() + + inlTree, err := t.funcdataInlTree(f.offset) + if err != nil { + return err + } + if inlTree == nil { + // This function has no inline ranges. + if includeSelf { + file, ok := nextFile() + if !ok { + return fmt.Errorf("%w: missing file entry for pc %x", ErrCorrupted, entryPc) + } + return visitor(entryPc, file.PC, selfNameOff, uint32(file.Val), selfStartLine, 0) + } + return nil + } + + offset, err = t.funcdataInlTreeIndex(f.offset) + if err != nil { + return err + } + pcValueSeq, err := t.iterPCValues(offset, entryPc) + if err != nil { + return err + } + pc := entryPc + curFile := pcValue{ + PC: 0, + Val: -1, + } + for inlFuncIdx := range pcValueSeq { + // [pc, inlFuncIdx.PC) maps to inline function data at inlFuncIdx.Val. + var funcNameOff uint32 + var startLine uint32 + var parentPC uint64 + if inlFuncIdx.Val < 0 { + // This range doesn't correspond to any inline function, it belongs to f itself. + if !includeSelf { + pc = inlFuncIdx.PC + continue + } + funcNameOff = selfNameOff + startLine = selfStartLine + } else { + call, err := readInlTree(inlTree, uint32(inlFuncIdx.Val)) + if err != nil { + return err + } + if call.funcID == t.wrapperFuncID { + pc = inlFuncIdx.PC + continue + } + funcNameOff = call.nameOff + startLine = call.startLine + parentPC = entryPc + uint64(call.parentPC) + } + // Find the file that covers the beginning of the current PC range. + for pc >= curFile.PC { + var ok bool + curFile, ok = nextFile() + if !ok { + return fmt.Errorf("%w: missing file entry for pc %x", ErrCorrupted, pc) + } + } + err := visitor(pc, inlFuncIdx.PC, funcNameOff, uint32(curFile.Val), startLine, parentPC) + if err != nil { + return err + } + pc = inlFuncIdx.PC + } + return nil +} + +// Parse inline tree to extract list of inline functions. +func (f *Function) inlines(buf []InlineFunction) ([]InlineFunction, error) { + t := f.table + entryPc, err := t.functabPc(f.idx) + if err != nil { + return buf, err + } + err = f.visitInlTree(entryPc, false, /*includeSelf*/ + func(_, _ uint64, funcNameOff, fileNo, startLine uint32, _ uint64) error { + funcName := t.funcName(funcNameOff) + offset, err := t.funcdataFileNoToOff(f.offset, fileNo) + if err != nil { + return err + } + file := t.fileName(offset) + buf = append(buf, InlineFunction{ + Name: funcName, + File: file, + StartLine: startLine, + }) + return nil + }) + if err != nil { + return nil, err + } + slices.SortFunc(buf, func(a, b InlineFunction) int { + return cmp.Compare(a.Name.Value(), b.Name.Value()) + }) + buf = slices.CompactFunc(buf, func(a, b InlineFunction) bool { + return a.Name.Value() == b.Name.Value() + }) + return buf, nil +} + +type inlineRange struct { + pcLo, pcHi uint64 + funcNameOff uint32 + fileNo uint32 + parentPC uint64 +} + +// Parse inline tree lookup table for function f, to produce list of its +// pc subranges mapped to either some inline function or f itself. Returned +// ranges are sorted by pcLo and non-overlapping. Ranges corresponding to +// wrapper functions are skipped. If the function has no inline functions, +// returns nil. +func (f *Function) inlineMapping(entryPc uint64, buf []inlineRange) ([]inlineRange, error) { + err := f.visitInlTree(entryPc, true, /*includeSelf*/ + func(pcLo, pcHi uint64, funcNameOff uint32, fileNo uint32, _ uint32, parentPC uint64) error { + buf = append(buf, inlineRange{ + pcLo: pcLo, + pcHi: pcHi, + funcNameOff: funcNameOff, + fileNo: fileNo, + parentPC: parentPC, + }) + return nil + }) + if err != nil { + return nil, err + } + return buf, nil +} + +type inlinedCall struct { + funcID uint8 + nameOff uint32 + parentPC uint32 + startLine uint32 +} + +func readInlTree(inlineTree []byte, fIdx uint32) (inlinedCall, error) { + const ( + inlinedCallSize = 16 + funcIDOffset = 0 + functionNameOffOffset = 4 + parentPCOffset = 8 + startLineOffset = 12 + ) + offset := inlinedCallSize * int(fIdx) + if offset+inlinedCallSize > len(inlineTree) { + return inlinedCall{}, fmt.Errorf("%w: inl tree out of bounds, off=%d", ErrCorrupted, offset) + } + return inlinedCall{ + funcID: inlineTree[offset+funcIDOffset], + nameOff: binary.LittleEndian.Uint32(inlineTree[offset+functionNameOffOffset:]), + parentPC: binary.LittleEndian.Uint32(inlineTree[offset+parentPCOffset:]), + startLine: binary.LittleEndian.Uint32(inlineTree[offset+startLineOffset:]), + }, nil +} + +type linesCache struct { + inlines []inlineRange + funcs map[uint32]unique.Handle[string] + files map[uint32]unique.Handle[string] +} + +func (f *Function) lines(buf LinesResult) (LinesResult, error) { + t := f.table + + if buf.linesCache.funcs == nil { + buf.linesCache.funcs = make(map[uint32]unique.Handle[string]) + } + if buf.linesCache.files == nil { + buf.linesCache.files = make(map[uint32]unique.Handle[string]) + } + + entryPc, err := t.functabPc(f.idx) + if err != nil { + return buf, err + } + + // Calculate mapping from pc ranges to functions. + defer clear(buf.linesCache.inlines) + buf.linesCache.inlines, err = f.inlineMapping(entryPc, buf.linesCache.inlines) + if err != nil { + return buf, err + } + funcs := buf.linesCache.inlines + funcIdx := 0 + + clear(buf.FunctionLines) + defer clear(buf.linesCache.funcs) + defer clear(buf.linesCache.files) + upsert := func( + pcLo, pcHi uint64, + funcNameOff, fileNo, line uint32, + parentPC uint64, + ) error { + var funcName unique.Handle[string] + var ok bool + if funcName, ok = buf.linesCache.funcs[funcNameOff]; !ok { + funcName = t.funcName(funcNameOff) + buf.linesCache.funcs[funcNameOff] = funcName + } + var file unique.Handle[string] + if file, ok = buf.linesCache.files[fileNo]; !ok { + offset, err := t.funcdataFileNoToOff(f.offset, fileNo) + if err != nil { + return err + } + file = t.fileName(offset) + buf.linesCache.files[fileNo] = file + } + buf.FunctionLines = append(buf.FunctionLines, FunctionLine{ + PCLo: pcLo, + PCHi: pcHi, + Name: funcName, + File: file, + Line: line, + ParentPC: parentPC, + }) + return nil + } + + // Join function mapping with line mapping. This mapping can be many-to-many. + offset, err := t.funcdataPcln(f.offset) + if err != nil { + return buf, err + } + pcValueSeq, err := t.iterPCValues(offset, entryPc) + if err != nil { + return buf, err + } + pc := entryPc + for line := range pcValueSeq { + // [pc, line.PC) maps to line.Val. + // Skip function ranges that end before current line range. + for funcIdx < len(funcs) && pc >= funcs[funcIdx].pcHi { + funcIdx++ + } + // Iterate over function ranges that end within the current line range. + for funcIdx < len(funcs) && line.PC >= funcs[funcIdx].pcHi { + err := upsert( + max(pc, funcs[funcIdx].pcLo), + funcs[funcIdx].pcHi, + funcs[funcIdx].funcNameOff, + funcs[funcIdx].fileNo, + uint32(line.Val), + funcs[funcIdx].parentPC, + ) + if err != nil { + return buf, nil + } + funcIdx++ + } + // Check if the next function range starts within the current line range. + if funcIdx < len(funcs) && pc >= funcs[funcIdx].pcLo { + err := upsert( + max(pc, funcs[funcIdx].pcLo), + line.PC, + funcs[funcIdx].funcNameOff, + funcs[funcIdx].fileNo, + uint32(line.Val), + funcs[funcIdx].parentPC, + ) + if err != nil { + return buf, nil + } + } + pc = line.PC + } + return buf, nil +} + +func (f *Function) resolveLocations(pc uint64, buf []Location) ([]Location, error) { + t := f.table + + entryPc, err := t.functabPc(f.idx) + if err != nil { + return buf, err + } + pcfile, err := t.funcdataPcfile(f.offset) + if err != nil { + return buf, err + } + pcln, err := t.funcdataPcln(f.offset) + if err != nil { + return buf, err + } + + addLocation := func(nameOff uint32, pc uint64) error { + fno, err := t.pcValue(pcfile, entryPc, pc) + if err != nil { + return err + } + foff, err := t.funcdataFileNoToOff(f.offset, uint32(fno)) + if err != nil { + return err + } + line, err := t.pcValue(pcln, entryPc, pc) + if err != nil { + return err + } + buf = append(buf, Location{ + Function: t.funcName(nameOff), + File: t.fileName(foff), + Line: uint32(line), + }) + return nil + } + + inlTree, err := t.funcdataInlTree(f.offset) + if err != nil { + return buf, err + } + if inlTree != nil { + offset, err := t.funcdataInlTreeIndex(f.offset) + if err != nil { + return buf, err + } + for { + inlIdx, err := t.pcValue(offset, entryPc, pc) + if err != nil { + return buf, fmt.Errorf("%w: pc outside of any function", ErrPcNotFound) + } + if inlIdx < 0 { + break + } + call, err := readInlTree(inlTree, uint32(inlIdx)) + if err != nil { + return buf, err + } + if call.funcID != t.wrapperFuncID { + err := addLocation(call.nameOff, pc) + if err != nil { + return buf, err + } + } + pc = entryPc + uint64(call.parentPC) + } + } + nameOff, err := t.funcdataNameOff(f.offset) + if err != nil { + return buf, err + } + err = addLocation(nameOff, pc) + if err != nil { + return buf, err + } + return buf, nil +} diff --git a/src/debug/gosym/v2/pcvalue.go b/src/debug/gosym/v2/pcvalue.go new file mode 100644 index 00000000000000..e5a6cbfda9f72e --- /dev/null +++ b/src/debug/gosym/v2/pcvalue.go @@ -0,0 +1,106 @@ +package gosym + +import ( + "fmt" + "iter" +) + +// pcValue represents a program counter and its associated value from the pctab. +type pcValue struct { + // PC is the program counter address. + PC uint64 + // Val is the associated value (line number, file index, etc.). + Val int32 +} + +func (t *Table) firstPcValue(offset uint32) (int32, error) { + pcValueSeq, err := t.iterPCValues(offset, 0) + if err != nil { + return 0, err + } + for pcValue := range pcValueSeq { + return pcValue.Val, nil + } + return 0, fmt.Errorf("%w: no pc value found", ErrCorrupted) +} + +func (t *Table) lastPc(offset uint32, entryPc uint64) (uint64, error) { + pcValueSeq, err := t.iterPCValues(offset, entryPc) + if err != nil { + return 0, err + } + pc := entryPc + for pcValue := range pcValueSeq { + pc = pcValue.PC + } + return pc, nil +} + +func (t *Table) pcValue(offset uint32, entryPc uint64, pc uint64) (int32, error) { + pcValueSeq, err := t.iterPCValues(offset, entryPc) + if err != nil { + return 0, err + } + for pcValue := range pcValueSeq { + if pcValue.PC > pc { + return pcValue.Val, nil + } + } + return 0, fmt.Errorf("%w: pc %#x value outside of the function", ErrPcNotFound, pc) +} + +func (t *Table) iterPCValues(off uint32, entryPc uint64) (iter.Seq[pcValue], error) { + offset := t.pcTab[0] + uint64(off) + if offset >= t.pcTab[1] { + return nil, fmt.Errorf("%w: pctab offset out of bounds", ErrCorrupted) + } + pcValueSeq := t.pclntab[offset:t.pcTab[1]] + val := int32(-1) + return func(yield func(pcValue) bool) { + first := true + for { + uvdelta, n := decodeVarint(pcValueSeq) + if !first && uvdelta == 0 { + return + } + first = false + var vdelta int32 + if (uvdelta & 1) != 0 { + vdelta = int32(^(uvdelta >> 1)) + } else { + vdelta = int32(uvdelta >> 1) + } + pcValueSeq = pcValueSeq[n:] + pcdelta, n := decodeVarint(pcValueSeq) + pcValueSeq = pcValueSeq[n:] + entryPc += uint64(pcdelta * uint32(t.pcQuantum)) + val += vdelta + if !yield(pcValue{PC: entryPc, Val: val}) { + return + } + } + }, nil +} + +func decodeVarint(buf []byte) (uint32, int) { + var result uint32 + var shift uint + var bytesRead int + + for i, b := range buf { + if i >= 5 { + return 0, 0 + } + + result |= uint32(b&0x7F) << shift + bytesRead++ + + if b&0x80 == 0 { + return result, bytesRead + } + + shift += 7 + } + + return 0, 0 +} diff --git a/src/debug/gosym/v2/strings.go b/src/debug/gosym/v2/strings.go new file mode 100644 index 00000000000000..b64f9a52631a72 --- /dev/null +++ b/src/debug/gosym/v2/strings.go @@ -0,0 +1,27 @@ +package gosym + +import "unique" + +func (t *Table) funcName(off uint32) unique.Handle[string] { + offset := t.funcnametab[0] + uint64(off) + if offset >= t.funcnametab[1] { + return unique.Handle[string]{} + } + end := offset + for end < t.funcnametab[1] && t.pclntab[end] != 0 { + end++ + } + return unique.Make(string(t.pclntab[offset:end])) +} + +func (t *Table) fileName(off uint32) unique.Handle[string] { + offset := t.filetab[0] + uint64(off) + if offset >= t.filetab[1] { + return unique.Handle[string]{} + } + end := offset + for end < t.filetab[1] && t.pclntab[end] != 0 { + end++ + } + return unique.Make(string(t.pclntab[offset:end])) +} diff --git a/src/debug/gosym/v2/table.go b/src/debug/gosym/v2/table.go new file mode 100644 index 00000000000000..76d7d38b7f3392 --- /dev/null +++ b/src/debug/gosym/v2/table.go @@ -0,0 +1,522 @@ +package gosym + +import ( + "bytes" + "encoding/binary" + "fmt" +) + +// version represents the supported pclntab versions. +type version int + +// pclnTabVersion constants +const ( + ver12 version = iota + ver116 + ver118 + ver120 +) + +const ( + go12Magic = 0xfffffffb + go116Magic = 0xfffffffa + go118Magic = 0xfffffff0 + go120Magic = 0xfffffff1 +) + +type metadata struct { + version version + wrapperFuncID uint8 + + // Quantum for pc values iteration + pcQuantum uint8 + // The pointer size + ptrSize uint8 + // The number of function entries + nfunctab uint32 + // The number of file entries + nfiletab uint32 + + // For ver118/120, the text start address (used to relocate PCs); otherwise zeros + textRange [2]uint64 + // Pc range + pcRange [2]uint64 + + // Data offsets within the pclntab + // The offset of the file table + filetab [2]uint64 + // The offset of the function table + functab [2]uint64 + // The blob of function metadata + funcdata [2]uint64 + // The function name table + funcnametab [2]uint64 + // The compile unit table + cutab [2]uint64 + // The pc table + pcTab [2]uint64 +} + +func parseObject(obj Object) (*Table, error) { + var ( + pclntab SectionHeader + pclntabData []byte + noptrdataData []byte + rodata []SectionHeader + rodataData [][]byte + text SectionHeader + ) + + headers, err := obj.Sections() + if err != nil { + return nil, err + } + for i, h := range headers { + switch h.Name { + case ".gopclntab", "__gopclntab": + pclntab = h + pclntabData, err = obj.SectionData(int8(i)) + if err != nil { + return nil, err + } + case ".noptrdata", "__noptrdata": + noptrdataData, err = obj.SectionData(int8(i)) + if err != nil { + return nil, err + } + case ".rodata", "__rodata": + rodata = append(rodata, h) + data, err := obj.SectionData(int8(i)) + if err != nil { + return nil, err + } + rodataData = append(rodataData, data) + case ".text", "__text": + text = h + } + } + + // Search for the moduledata structure + addrBytes := make([]byte, 8) + obj.Endian().PutUint64(addrBytes, pclntab.Addr) + + offsets := findAll(noptrdataData, addrBytes) + var moduledata moduledata + for _, offset := range offsets { + var valid bool + moduledata, valid = tryParseModuleDataAt( + text, rodata, + noptrdataData, rodataData, + offset, + ) + if valid { + goto found + } + } + return nil, fmt.Errorf("%w: moduledata not found", ErrCorrupted) + +found: + return parseTable(pclntabData, moduledata.gofunc, moduledata.textRange, moduledata.pcRange) +} + +type moduledata struct { + textRange, pcRange [2]uint64 + gofunc []byte +} + +// Module data offsets +const ( + moduledataMinPCOffset = 160 + moduledataTextOffset = moduledataMinPCOffset + 16 + moduledataBssOffset = moduledataTextOffset + 48 + moduledataGcdataOffset = moduledataBssOffset + 56 + moduledataTypesOffset = moduledataGcdataOffset + 16 + moduledataGofuncOffset = moduledataTypesOffset + 24 + moduledataTextsectMapOffset = moduledataTypesOffset + 32 +) + +func tryParseModuleDataAt( + textHeader SectionHeader, + rodataHeaders []SectionHeader, + noptrdataData []byte, + rodataData [][]byte, + offset int, +) (moduledata, bool) { + // Parse text range + textStart := offset + moduledataTextOffset + if textStart+16 > len(noptrdataData) { + return moduledata{}, false + } + text := binary.LittleEndian.Uint64(noptrdataData[textStart:]) + etext := binary.LittleEndian.Uint64(noptrdataData[textStart+8:]) + if text > etext || text < textHeader.Addr || etext > textHeader.Addr+textHeader.Size { + return moduledata{}, false + } + + // Parse types range + typesStart := offset + moduledataTypesOffset + if typesStart+16 > len(noptrdataData) { + return moduledata{}, false + } + types := binary.LittleEndian.Uint64(noptrdataData[typesStart:]) + etypes := binary.LittleEndian.Uint64(noptrdataData[typesStart+8:]) + if types > etypes { + return moduledata{}, false + } + var valid bool + for _, h := range rodataHeaders { + if h.Addr <= types && etypes <= h.Addr+h.Size { + valid = true + break + } + } + if !valid { + return moduledata{}, false + } + + // Parse textsect map + textsectMapOffset := offset + moduledataTextsectMapOffset + if textsectMapOffset+16 > len(noptrdataData) { + return moduledata{}, false + } + textsectMapPtr := binary.LittleEndian.Uint64(noptrdataData[textsectMapOffset:]) + textsectMapLen := binary.LittleEndian.Uint64(noptrdataData[textsectMapOffset+8:]) + + valid = false + for _, h := range rodataHeaders { + if textsectMapPtr < h.Addr { + continue + } + textsectMapDataOffset := int(textsectMapPtr - h.Addr) + textsectMapDataLen := int(textsectMapLen * 24) + if textsectMapDataOffset+textsectMapDataLen < int(h.Size) { + valid = true + break + } + } + if !valid { + return moduledata{}, false + } + + // Parse BSS range + bssOffset := offset + moduledataBssOffset + if bssOffset+16 > len(noptrdataData) { + return moduledata{}, false + } + + // Parse gofunc offset + gofuncOffset := offset + moduledataGofuncOffset + if gofuncOffset+8 > len(noptrdataData) { + return moduledata{}, false + } + gofunc := binary.LittleEndian.Uint64(noptrdataData[gofuncOffset:]) + + // Parse gcdata offset + gcdataOffset := offset + moduledataGcdataOffset + if gcdataOffset+8 > len(noptrdataData) { + return moduledata{}, false + } + gcdata := binary.LittleEndian.Uint64(noptrdataData[gcdataOffset:]) + + rodataIdx := -1 + for i, h := range rodataHeaders { + if h.Addr <= gofunc && gcdata <= h.Addr+h.Size { + rodataIdx = i + gofunc -= h.Addr + gcdata -= h.Addr + break + } + } + if rodataIdx == -1 { + return moduledata{}, false + } + + // Parse min/max PC + minPCOffset := offset + moduledataMinPCOffset + if minPCOffset+16 > len(noptrdataData) { + return moduledata{}, false + } + minPC := binary.LittleEndian.Uint64(noptrdataData[minPCOffset:]) + maxPC := binary.LittleEndian.Uint64(noptrdataData[minPCOffset+8:]) + + return moduledata{ + textRange: [2]uint64{text, etext}, + pcRange: [2]uint64{minPC, maxPC}, + gofunc: rodataData[rodataIdx][gofunc:gcdata], + }, true +} + +func findAll(haystack, needle []byte) []int { + var offsets []int + start := 0 + for { + idx := bytes.Index(haystack[start:], needle) + if idx == -1 { + break + } + offsets = append(offsets, start+idx) + start += idx + 1 + } + return offsets +} + +func parseTable( + pclntab, gofunc []byte, + textRange, pcRange [2]uint64, +) (*Table, error) { + magic := binary.LittleEndian.Uint32(pclntab[0:4]) + var version version + switch magic { + case go12Magic: + version = ver12 + case go116Magic: + version = ver116 + case go118Magic: + version = ver118 + case go120Magic: + version = ver120 + default: + return nil, fmt.Errorf("%w: unsupported pclntab magic: %x", ErrCorrupted, magic) + } + + if pclntab[4] != 0 || pclntab[5] != 0 { + return nil, fmt.Errorf("%w: unexpected pclntab non-zero header padding", ErrCorrupted) + } + + quantum := uint8(pclntab[6]) + ptrSize := uint8(pclntab[7]) + + if ptrSize != 4 && ptrSize != 8 { + return nil, fmt.Errorf("%w: invalid pointer size in pclntab: %d", ErrCorrupted, ptrSize) + } + + t := &Table{ + metadata: metadata{ + version: version, + pcQuantum: quantum, + ptrSize: ptrSize, + textRange: textRange, + pcRange: pcRange, + }, + pclntab: pclntab, + gofunc: gofunc, + } + + switch version { + case ver118, ver120: + err := parseMetadata118(t) + if err != nil { + return nil, err + } + case ver116: + err := parseMetadata116(t) + if err != nil { + return nil, err + } + case ver12: + err := parseMetadata12(t) + if err != nil { + return nil, err + } + } + + // Now we need to figure out the wrapper function ID. The value is not + // stable across versions, and it's not always easy to get the go version. + // Instead we look for known wrapper function. + wrapperFuncID := uint8(255) + for f := range t.Functions() { + name, err := f.Name() + if err == nil && name.Value() == "runtime.deferreturn" { + wrapperFuncID, err = t.funcdataFuncID(f.offset) + if err != nil { + break + } + } + } + t.wrapperFuncID = wrapperFuncID + + return t, nil +} + +func parseMetadata118(t *Table) error { + pclntablen := uint64(len(t.pclntab)) + readWord := func(offset uint32) (uint64, error) { + start := 8 + int(offset)*int(t.ptrSize) + if start+int(t.ptrSize) > int(pclntablen) { + return 0, fmt.Errorf("%w: pclntab too short for word at offset: %d", ErrCorrupted, offset) + } + if t.ptrSize == 8 { + return binary.LittleEndian.Uint64(t.pclntab[start : start+8]), nil + } + return uint64(binary.LittleEndian.Uint32(t.pclntab[start : start+4])), nil + } + + nfunctab, err := readWord(0) + if err != nil { + return err + } + nfiletab, err := readWord(1) + if err != nil { + return err + } + funcnametab, err := readWord(3) + if err != nil { + return err + } + if funcnametab >= pclntablen { + return fmt.Errorf("%w: funcnametab out of bounds", ErrCorrupted) + } + cutab, err := readWord(4) + if err != nil { + return err + } + if cutab >= pclntablen { + return fmt.Errorf("%w: cutab out of bounds", ErrCorrupted) + } + filetab, err := readWord(5) + if err != nil { + return err + } + if filetab >= pclntablen { + return fmt.Errorf("%w: filetab out of bounds", ErrCorrupted) + } + pctab, err := readWord(6) + if err != nil { + return err + } + if pctab >= pclntablen { + return fmt.Errorf("%w: pctab out of bounds", ErrCorrupted) + } + functab, err := readWord(7) + if err != nil { + return err + } + functablen := (nfunctab*2 + 1) * functabFieldSize(t.version, t.ptrSize) + if functab+functablen > pclntablen { + return fmt.Errorf("%w: functab out of bounds", ErrCorrupted) + } + t.nfunctab = uint32(nfunctab) + t.nfiletab = uint32(nfiletab) + t.filetab = [2]uint64{filetab, pclntablen} + t.functab = [2]uint64{functab, functab + functablen} + t.funcdata = [2]uint64{functab, pclntablen} + t.funcnametab = [2]uint64{funcnametab, pclntablen} + t.cutab = [2]uint64{cutab, pclntablen} + t.pcTab = [2]uint64{pctab, pclntablen} + return nil +} + +func parseMetadata116(t *Table) error { + pclntablen := uint64(len(t.pclntab)) + readWord := func(offset uint32) (uint64, error) { + start := 8 + int(offset)*int(t.ptrSize) + if start+int(t.ptrSize) > int(pclntablen) { + return 0, fmt.Errorf("%w: pclntab too short for word at offset: %d", ErrCorrupted, offset) + } + if t.ptrSize == 8 { + return binary.LittleEndian.Uint64(t.pclntab[start : start+8]), nil + } + return uint64(binary.LittleEndian.Uint32(t.pclntab[start : start+4])), nil + } + + nfunctab, err := readWord(0) + if err != nil { + return err + } + nfiletab, err := readWord(1) + if err != nil { + return err + } + funcnametab, err := readWord(2) + if err != nil { + return err + } + if funcnametab >= pclntablen { + return fmt.Errorf("%w: funcnametab out of bounds", ErrCorrupted) + } + cutab, err := readWord(3) + if err != nil { + return err + } + if cutab >= pclntablen { + return fmt.Errorf("%w: cutab out of bounds", ErrCorrupted) + } + filetab, err := readWord(4) + if err != nil { + return err + } + if filetab >= pclntablen { + return fmt.Errorf("%w: filetab out of bounds", ErrCorrupted) + } + pctab, err := readWord(5) + if err != nil { + return err + } + if pctab >= pclntablen { + return fmt.Errorf("%w: pctab out of bounds", ErrCorrupted) + } + functab, err := readWord(6) + if err != nil { + return err + } + functablen := (nfunctab*2 + 1) * functabFieldSize(t.version, t.ptrSize) + if functab+functablen > pclntablen { + return fmt.Errorf("%w: functab out of bounds", ErrCorrupted) + } + t.nfunctab = uint32(nfunctab) + t.nfiletab = uint32(nfiletab) + t.filetab = [2]uint64{filetab, pclntablen} + t.functab = [2]uint64{functab, functab + functablen} + t.funcdata = [2]uint64{functab, pclntablen} + t.funcnametab = [2]uint64{funcnametab, pclntablen} + t.cutab = [2]uint64{cutab, pclntablen} + t.pcTab = [2]uint64{pctab, pclntablen} + return nil +} + +func parseMetadata12(t *Table) error { + pclntablen := uint64(len(t.pclntab)) + readWord := func(offset uint32) (uint64, error) { + start := 8 + int(offset)*int(t.ptrSize) + if start+int(t.ptrSize) > int(pclntablen) { + return 0, fmt.Errorf("%w: pclntab too short for word at offset: %d", ErrCorrupted, offset) + } + if t.ptrSize == 8 { + return binary.LittleEndian.Uint64(t.pclntab[start : start+8]), nil + } + return uint64(binary.LittleEndian.Uint32(t.pclntab[start : start+4])), nil + } + + nfunctab, err := readWord(0) + if err != nil { + return err + } + functab := uint64(8 + t.ptrSize) + functablen := (nfunctab*2 + 1) * functabFieldSize(t.version, t.ptrSize) + if functab+functablen > pclntablen { + return fmt.Errorf("%w: functab out of bounds", ErrCorrupted) + } + readUint32 := func(offset uint32) (uint32, error) { + if uint64(offset+4) > pclntablen { + return 0, fmt.Errorf("%w: pclntab too short for uint32 at offset: %d", ErrCorrupted, offset) + } + return binary.LittleEndian.Uint32(t.pclntab[offset : offset+4]), nil + } + filetab, err := readUint32(uint32(functab + functablen)) + if err != nil { + return err + } + nfiletab, err := readUint32(filetab) + if err != nil { + return err + } + t.nfunctab = uint32(nfunctab) + t.nfiletab = uint32(nfiletab) + t.filetab = [2]uint64{uint64(filetab), uint64(filetab + nfiletab*4)} + t.functab = [2]uint64{functab, functab + functablen} + t.funcdata = [2]uint64{0, pclntablen} + t.funcnametab = [2]uint64{0, pclntablen} + t.cutab = [2]uint64{0, 0} + t.pcTab = [2]uint64{0, pclntablen} + return nil +}