diff --git a/debug/gosym/pclntab.go b/debug/gosym/pclntab.go index 9bd6b8a..5e0b427 100644 --- a/debug/gosym/pclntab.go +++ b/debug/gosym/pclntab.go @@ -523,6 +523,87 @@ func (f funcData) deferreturn() uint32 { return f.field(3) } func (f funcData) pcfile() uint32 { return f.field(5) } func (f funcData) pcln() uint32 { return f.field(6) } func (f funcData) cuOffset() uint32 { return f.field(8) } +func (f funcData) npcdata() uint32 { return f.field(7) } + +// nfuncdata returns the number of funcdata entries. It's a single trailing +// byte after the _func struct's nine 4-byte fields (funcID/flag/pad precede +// it), so it can't go through field(), which only covers n <= 9. +func (f funcData) nfuncdata() uint8 { + sz0 := f.t.Ptrsize + if f.t.Version >= ver118 { + sz0 = 4 + } + return f.data[sz0+39] +} + +// headerSize returns the size of the fixed _func header: sz0 (entry field) +// + 36 (fields 1-9, 4 bytes each) + 4 (funcID/flag/pad/nfuncdata, 1 byte each). +// pcdata[] starts here; funcdata[] starts npcdata*4 bytes after that. +func (f funcData) headerSize() uint32 { + sz0 := f.t.Ptrsize + if f.t.Version >= ver118 { + sz0 = 4 + } + return sz0 + 40 +} + +// pcdataOffset returns the offset into t.pctab of the i'th pcdata table +// (e.g. PCDATA_InlTreeIndex). Sentinel 0xffffffff means absent. +func (f funcData) pcdataOffset(i uint32) uint32 { + off := f.headerSize() + i*4 + return f.t.Binary.Uint32(f.data[off:]) +} + +// funcdataOffset returns the offset into moduledata.GoFunc (go:func.*) of the +// i'th funcdata blob (e.g. FUNCDATA_InlTree). Sentinel 0xffffffff means absent. +func (f funcData) funcdataOffset(i uint32) uint32 { + off := f.headerSize() + f.npcdata()*4 + i*4 + return f.t.Binary.Uint32(f.data[off:]) +} + +// pcValueRange is one (value, pcStart, pcEnd) transition decoded from a +// pc-value delta table -- the value applies to every PC in [pcStart, pcEnd). +type pcValueRange struct { + val int32 + pcStart uint64 + pcEnd uint64 +} + +// decodePcValueRanges walks the same delta-encoded table pcvalue() reads +// (LEB128 varint + zigzag, via step()), but returns every transition instead +// of stopping at one target PC. off is the pc-value table offset (e.g. from +// funcData.pcdataOffset(objabi.PCDATA_InlTreeIndex)) and entry is the +// function's start PC. +func (t *LineTable) decodePcValueRanges(off uint32, entry uint64) []pcValueRange { + p := t.pctab[off:] + var ranges []pcValueRange + val := int32(-1) + pc := entry + for { + prevPc := pc + if !t.step(&p, &pc, &val, pc == entry) { + break + } + ranges = append(ranges, pcValueRange{val: val, pcStart: prevPc, pcEnd: pc}) + } + return ranges +} + +// countInlineRecords returns the number of InlTree records for a function, +// computed as max(all non-negative PCDATA_InlTreeIndex values) + 1 -- the +// same approach Delve uses (maxInlineTreeIndexValue()), since nothing on +// disk states the record count directly. Verified against hand-decoded +// data on learning/01_basic_inline and learning/02_runtime_inline; see +// docs/inline_functions/ghidra_inline_exercise_log.md. +func countInlineRecords(ranges []pcValueRange) int { + max := int32(-1) + for _, r := range ranges { + if r.val > max { + max = r.val + } + } + return int(max) + 1 +} // field returns the nth field of the _func struct. // It panics if n == 0 or n > 9; for n == 0, call f.entryPC. diff --git a/main.go b/main.go index 9958f55..1e9e773 100644 --- a/main.go +++ b/main.go @@ -44,11 +44,19 @@ type PcLnTabMetadata struct { PointerSize uint32 } +type InlinedCall struct { + Funcname string + CallingPc uint64 + ParentEntry uint64 + ParentIndex int // -1 = direct parent is ParentEntry (the physical function); else index into this same InlinedList +} + type FuncMetadata struct { Start uint64 End uint64 PackageName string FullName string + InlinedList []InlinedCall } type ExtractMetadata struct { diff --git a/main_test.go b/main_test.go index df14df7..d6d6541 100644 --- a/main_test.go +++ b/main_test.go @@ -118,6 +118,109 @@ func TestAllVersions(t *testing.T) { t.Errorf("Go %s no Func type with _ptr_ in CStr found", v) } + // --- Inline function recovery tests --- + // Positive: main.main must have inlined functions (add, multiply) + for _, fn := range data.UserFunctions { + if fn.FullName == "main.main" { + if len(fn.InlinedList) == 0 { + t.Errorf("Go %s main.main expected inlined functions, got none", v) + } + found_add := false + found_multiply := false + for _, inl := range fn.InlinedList { + if inl.Funcname == "main.add" { + found_add = true + } + if inl.Funcname == "main.multiply" { + found_multiply = true + } + } + if !found_add { + t.Errorf("Go %s main.add not found in main.main InlinedList", v) + } + if !found_multiply { + t.Errorf("Go %s main.multiply not found in main.main InlinedList", v) + } + if len(fn.InlinedList) < 2 { + t.Errorf("Go %s main.main expected >= 2 inlines, got %d", v, len(fn.InlinedList)) + } + } + } + + // Positive: main.main's InlinedList must preserve the real parent chain, + // not flatten it. main.nestedOuter calls main.nestedInner; the compiler + // inlines nestedInner into nestedOuter, then nestedOuter into main.main + // -- so nestedInner's *direct* logical parent is nestedOuter's own + // InlinedList entry, not main.main. See docs/inline_functions/ + // parentpc_verification_exercise.md for the by-hand proof this mirrors, + // and progress_summary.md's "Maintainer decision on flat vs. tree" for + // why this must hold (Steve, 2026-09-13: tree version required). + for _, fn := range data.UserFunctions { + if fn.FullName == "main.main" { + outerIdx, innerIdx := -1, -1 + for i, inl := range fn.InlinedList { + if inl.Funcname == "main.nestedOuter" { + outerIdx = i + } + if inl.Funcname == "main.nestedInner" { + innerIdx = i + } + } + if outerIdx == -1 { + t.Errorf("Go %s main.nestedOuter not found in main.main InlinedList", v) + } + if innerIdx == -1 { + t.Errorf("Go %s main.nestedInner not found in main.main InlinedList", v) + } + if outerIdx != -1 && innerIdx != -1 { + if fn.InlinedList[outerIdx].ParentIndex != -1 { + t.Errorf("Go %s main.nestedOuter.ParentIndex = %d, want -1 (direct parent is main.main itself)", + v, fn.InlinedList[outerIdx].ParentIndex) + } + if fn.InlinedList[innerIdx].ParentIndex != outerIdx { + t.Errorf("Go %s main.nestedInner.ParentIndex = %d, want %d (main.nestedOuter's own index -- not flattened to main.main)", + v, fn.InlinedList[innerIdx].ParentIndex, outerIdx) + } + } + } + } + + // Negative: neverInlined must have empty InlinedList + for _, fn := range data.UserFunctions { + if fn.FullName == "main.neverInlined" && len(fn.InlinedList) != 0 { + t.Errorf("Go %s main.neverInlined should have no InlinedList entries", v) + } + } + + // Negative: validate all InlinedList entries across all functions + for _, fn := range data.UserFunctions { + seen := map[uint64]bool{} + funcSize := fn.End - fn.Start + for _, inl := range fn.InlinedList { + if inl.Funcname == "" { + t.Errorf("Go %s empty Funcname in %s InlinedList", v, fn.FullName) + } + for _, c := range inl.Funcname { + if c < 32 || c > 126 { + t.Errorf("Go %s non-printable char in Funcname %q in %s", v, inl.Funcname, fn.FullName) + } + } + if !strings.Contains(inl.Funcname, ".") { + t.Errorf("Go %s Funcname %q missing package prefix in %s", v, inl.Funcname, fn.FullName) + } + if inl.CallingPc >= funcSize { + t.Errorf("Go %s CallingPc %x >= funcSize %x in %s", v, inl.CallingPc, funcSize, fn.FullName) + } + if inl.ParentEntry != fn.Start { + t.Errorf("Go %s ParentEntry %x != fn.Start %x in %s", v, inl.ParentEntry, fn.Start, fn.FullName) + } + if seen[inl.CallingPc] { + t.Errorf("Go %s duplicate CallingPc %x in %s", v, inl.CallingPc, fn.FullName) + } + seen[inl.CallingPc] = true + } + } + } else { found_interface := false for _, typ := range data.Types { diff --git a/objfile/internals.go b/objfile/internals.go index 3ac77cc..562d816 100644 --- a/objfile/internals.go +++ b/objfile/internals.go @@ -157,6 +157,7 @@ type ModuleData struct { ETypes uint64 // points to end of type information Typelinks GoSlice64 // points to metadata about offsets into types for structures and other types ITablinks GoSlice64 // points to metadata about offsets into types for interfaces + GoFunc uint64 // go:func.* base address; funcdata[FUNCDATA_InlTree] offsets are relative to this // Some versions of go with 1.2 moduledata use a slice instead of the types + offset typelinks list LegacyTypes GoSlice64 diff --git a/objfile/layouts.go b/objfile/layouts.go index 3f96d7e..11bfde2 100644 --- a/objfile/layouts.go +++ b/objfile/layouts.go @@ -37,6 +37,8 @@ const ( // FuncTab fields FieldEntryoffset // 22 FieldFuncoffset // 23 + // ModuleData fields (added later) + FieldGoFunc // 24 -- go:func.* base (moduledata.gofunc), needed for FUNCDATA_InlTree ) // String representation for debugging/logging @@ -90,6 +92,8 @@ func (f FieldName) String() string { return "Entryoffset" case FieldFuncoffset: return "Funcoffset" + case FieldGoFunc: + return "GoFunc" default: return "Unknown" } @@ -280,6 +284,12 @@ var functabLayoutLegacy = &StructLayout{ // Only fields that GoReSym actually uses are included var moduleDataLayouts = map[string]*ModuleDataLayout{ "1.22": { + // NOTE: this bucket is real Go 1.26 only (see getModuleDataLayout) -- + // FieldGoFunc offset intentionally omitted here, unverified. The + // gap between Etypes and Textsectmap is 8 bytes larger than the + // "1.21"/"1.20" buckets, so GoFunc's offset for this bucket is NOT + // simply 320/160 -- confirm against a real Go 1.26 binary before + // adding it. Version: "1.22", Fields: []FieldInfo{ {Name: FieldFtab, Offset64: 128, Offset32: 64, Type: FieldTypeSlice}, @@ -293,6 +303,10 @@ var moduleDataLayouts = map[string]*ModuleDataLayout{ }, }, "1.21": { + // Real Go 1.21-1.25 (see getModuleDataLayout). GoFunc offset + // verified against runtime/symtab.go's moduledata struct (go1.22.5 + // source) and empirically against a real binary -- see + // docs/inline_functions/ghidra_inline_exercise_log.md. Version: "1.21", Fields: []FieldInfo{ {Name: FieldFtab, Offset64: 128, Offset32: 64, Type: FieldTypeSlice}, @@ -303,9 +317,11 @@ var moduleDataLayouts = map[string]*ModuleDataLayout{ {Name: FieldTextsectmap, Offset64: 328, Offset32: 164, Type: FieldTypeSlice}, {Name: FieldTypelinks, Offset64: 352, Offset32: 176, Type: FieldTypeSlice}, {Name: FieldItablinks, Offset64: 376, Offset32: 188, Type: FieldTypeSlice}, + {Name: FieldGoFunc, Offset64: 320, Offset32: 160, Type: FieldTypePvoid}, }, }, "1.20": { + // Real Go 1.20 only. Same field layout as "1.21" bucket. Version: "1.20", Fields: []FieldInfo{ {Name: FieldFtab, Offset64: 128, Offset32: 64, Type: FieldTypeSlice}, @@ -315,6 +331,7 @@ var moduleDataLayouts = map[string]*ModuleDataLayout{ {Name: FieldEtypes, Offset64: 304, Offset32: 152, Type: FieldTypePvoid}, {Name: FieldTextsectmap, Offset64: 328, Offset32: 164, Type: FieldTypeSlice}, {Name: FieldTypelinks, Offset64: 352, Offset32: 176, Type: FieldTypeSlice}, + {Name: FieldGoFunc, Offset64: 320, Offset32: 160, Type: FieldTypePvoid}, {Name: FieldItablinks, Offset64: 376, Offset32: 188, Type: FieldTypeSlice}, }, }, @@ -446,6 +463,7 @@ type ModuleDataIntermediate struct { Textsectmap GoSlice64 Typelinks GoSlice64 Itablinks GoSlice64 + GoFunc uint64 } // parseModuleDataGeneric parses moduledata from raw bytes using layout tables @@ -502,6 +520,8 @@ func parseModuleDataGeneric(rawData []byte, runtimeVersion string, layoutVersion case FieldItablinks: data, len := readSlice(rawData, offset, is64bit, littleendian) md.Itablinks = GoSlice64{Data: pvoid64(data), Len: len} + case FieldGoFunc: + md.GoFunc = readPointer(rawData, offset, is64bit, littleendian) } } @@ -582,6 +602,7 @@ func (e *Entry) validateAndConvertModuleData( ETypes: md.Etypes, Typelinks: md.Typelinks, ITablinks: md.Itablinks, + GoFunc: md.GoFunc, } return result, ignorelist, nil diff --git a/testproject/main.go b/testproject/main.go index 494cb40..7b75f44 100644 --- a/testproject/main.go +++ b/testproject/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" + "os" ) type TaggedStruct struct { @@ -12,6 +13,35 @@ type TaggedStruct struct { Active bool } +func add(a, b int) int { return a + b } +func multiply(a, b int) int { return a * b } + +//go:noinline +func neverInlined(x int) int { return x * x } + +// nestedInlineSink forces a real memory write inside nestedInner, so its +// contribution can't be algebraically fused into nestedOuter's own arithmetic +// and optimized away entirely (this happened on the first attempt at this +// fixture -- nestedInner's code vanished with no trace to recover). +var nestedInlineSink int + +// nestedInner is small enough to inline, but the store to nestedInlineSink is +// a real, non-foldable side effect. +func nestedInner(x int) int { + nestedInlineSink = x + return x + 1 +} + +// nestedOuter calls nestedInner. If both are small enough, the compiler can +// inline nestedInner into nestedOuter, AND THEN inline that already-inlined +// copy of nestedOuter into main.main -- this is the "mid-stack inlining" case: +// nestedInner ends up nested two levels deep inside main.main, with its +// logical caller being nestedOuter (itself inlined), not main.main directly. +func nestedOuter(x int) int { + y := nestedInner(x) + return y * 2 +} + func sum(s []int, c chan int) { sum := 0 for _, v := range s { @@ -43,4 +73,10 @@ func main() { x := <-c fmt.Println(x) + + n := len(os.Args) + fmt.Println(add(n, 2)) + fmt.Println(multiply(n, 4)) + fmt.Println(neverInlined(5)) + fmt.Println(nestedOuter(n)) }