From cae043fa7f754cfcb39363b0828c7ab931f259d0 Mon Sep 17 00:00:00 2001 From: kamran Date: Fri, 5 Jun 2026 03:55:29 +0500 Subject: [PATCH 1/3] Add inlined function tests and InlinedCall struct for issue #53 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add InlinedCall struct and InlinedList field to FuncMetadata - Add add(), multiply(), neverInlined() to testproject - Add comprehensive inline test suite (positive + negative cases) - Positive tests intentionally fail — implementation not yet written --- main.go | 7 ++++++ main_test.go | 58 +++++++++++++++++++++++++++++++++++++++++++++ testproject/main.go | 10 ++++++++ 3 files changed, 75 insertions(+) diff --git a/main.go b/main.go index 4ae946e..fae2b47 100644 --- a/main.go +++ b/main.go @@ -44,11 +44,18 @@ type PcLnTabMetadata struct { PointerSize uint32 } +type InlinedCall struct { + Funcname string + CallingPc uint64 + ParentEntry uint64 +} + 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 bf5bbe7..af9c0ce 100644 --- a/main_test.go +++ b/main_test.go @@ -118,6 +118,64 @@ 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 + for _, inl := range fn.InlinedList { + if inl.Funcname == "main.add" { + found_add = true + } + } + if !found_add { + t.Errorf("Go %s main.add 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)) + } + } + } + + // 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/testproject/main.go b/testproject/main.go index 8571d4b..31dc69e 100644 --- a/testproject/main.go +++ b/testproject/main.go @@ -7,6 +7,12 @@ type structurea struct { test string } +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 } + func sum(s []int, c chan int) { sum := 0 for _, v := range s { @@ -35,4 +41,8 @@ func main() { x := <-c fmt.Println(x) + + fmt.Println(add(1, 2)) + fmt.Println(multiply(3, 4)) + fmt.Println(neverInlined(5)) } From b566355767f061a991853113cc4753cd0d9a1b54 Mon Sep 17 00:00:00 2001 From: kamran Date: Fri, 10 Jul 2026 00:49:47 +0500 Subject: [PATCH 2/3] Use runtime-derived arguments in add/multiply test fixture Constant arguments let the compiler fold add(1,2)/multiply(3,4) into literals at compile time, leaving no inline-tree evidence to recover regardless of extraction-code correctness. Deriving from len(os.Args) forces the compiler to keep real, recoverable inlined instructions. Also strengthen main_test.go to check for main.multiply by name, not just main.add, now that both are genuinely recoverable. --- main_test.go | 7 +++++++ testproject/main.go | 10 +++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/main_test.go b/main_test.go index af9c0ce..da8c415 100644 --- a/main_test.go +++ b/main_test.go @@ -126,14 +126,21 @@ func TestAllVersions(t *testing.T) { 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)) } diff --git a/testproject/main.go b/testproject/main.go index 31dc69e..18975f4 100644 --- a/testproject/main.go +++ b/testproject/main.go @@ -1,7 +1,10 @@ /*Copyright (C) 2022 Mandiant, Inc. All Rights Reserved.*/ package main -import "fmt" +import ( + "fmt" + "os" +) type structurea struct { test string @@ -42,7 +45,8 @@ func main() { x := <-c fmt.Println(x) - fmt.Println(add(1, 2)) - fmt.Println(multiply(3, 4)) + n := len(os.Args) + fmt.Println(add(n, 2)) + fmt.Println(multiply(n, 4)) fmt.Println(neverInlined(5)) } From 82ecb06b6848f3139f47027c8eb5b67c0f1dc93f Mon Sep 17 00:00:00 2001 From: kamran Date: Fri, 10 Jul 2026 04:51:45 +0500 Subject: [PATCH 3/3] feat: add GoFunc field to moduledata layout system Reads moduledata.gofunc (go:func.* base) via the existing per-version layout tables, needed for FUNCDATA_InlTree offsets (issue #53). Offset 320/64-bit, 160/32-bit verified against runtime source and a real test binary; the Go 1.26-only layout bucket is left unset rather than guessed. --- objfile/internals.go | 1 + objfile/layouts.go | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) 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