diff --git a/main.go b/main.go index 9958f55..9196fae 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 df14df7..bcdb0bf 100644 --- a/main_test.go +++ b/main_test.go @@ -118,6 +118,71 @@ 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)) + } + } + } + + // 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..fc8b055 100644 --- a/testproject/main.go +++ b/testproject/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" + "os" ) type TaggedStruct struct { @@ -12,6 +13,12 @@ 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 } + func sum(s []int, c chan int) { sum := 0 for _, v := range s { @@ -43,4 +50,9 @@ 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)) }