Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
65 changes: 65 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
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
12 changes: 12 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,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 {
Expand Down Expand Up @@ -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))
}
Loading