Skip to content
81 changes: 81 additions & 0 deletions debug/gosym/pclntab.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
103 changes: 103 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions objfile/internals.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions objfile/layouts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -90,6 +92,8 @@ func (f FieldName) String() string {
return "Entryoffset"
case FieldFuncoffset:
return "Funcoffset"
case FieldGoFunc:
return "GoFunc"
default:
return "Unknown"
}
Expand Down Expand Up @@ -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},
Expand All @@ -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},
Expand All @@ -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},
Expand All @@ -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},
},
},
Expand Down Expand Up @@ -446,6 +463,7 @@ type ModuleDataIntermediate struct {
Textsectmap GoSlice64
Typelinks GoSlice64
Itablinks GoSlice64
GoFunc uint64
}

// parseModuleDataGeneric parses moduledata from raw bytes using layout tables
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -582,6 +602,7 @@ func (e *Entry) validateAndConvertModuleData(
ETypes: md.Etypes,
Typelinks: md.Typelinks,
ITablinks: md.Itablinks,
GoFunc: md.GoFunc,
}

return result, ignorelist, nil
Expand Down
36 changes: 36 additions & 0 deletions testproject/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main

import (
"fmt"
"os"
)

type TaggedStruct struct {
Expand All @@ -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 {
Expand Down Expand Up @@ -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))
}
Loading