From 97d74a8fd8e52338575c971f6da7d2b128dcd54d Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 24 May 2026 22:04:12 +0800 Subject: [PATCH] runtime: preserve FuncForPC names for function values --- cl/compile.go | 32 +++++++- internal/build/build.go | 18 +++++ internal/build/build_test.go | 26 +++++++ .../lib/runtime/pprof_runtime_stub_llgo.go | 4 - runtime/internal/lib/runtime/symtab.go | 67 ++++++++++++++++- ssa/closure_wrap.go | 25 ++++++- ssa/expr.go | 6 +- ssa/interface.go | 12 ++- ssa/package.go | 9 ++- ssa/ssa_test.go | 14 +++- test/go/runtime_funcforpc_names_test.go | 73 +++++++++++++++++++ test/goroot/xfail.yaml | 38 ---------- 12 files changed, 267 insertions(+), 57 deletions(-) create mode 100644 test/go/runtime_funcforpc_names_test.go diff --git a/cl/compile.go b/cl/compile.go index 5b6d7c9fbe..77a23d3560 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -998,7 +998,11 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue } } x := p.compileValue(b, v.X) - ret = b.MakeInterface(t, x) + if p.makeInterfaceNeedsFuncPC(v) { + ret = b.MakeInterfaceFuncPC(t, x) + } else { + ret = b.MakeInterface(t, x) + } case *ssa.MakeSlice: t := p.type_(v.Type(), llssa.InGo) nLen := p.compileValue(b, v.Len) @@ -1251,6 +1255,32 @@ func (p *context) compileValue(b llssa.Builder, v ssa.Value) llssa.Expr { panic(fmt.Sprintf("compileValue: unknown value - %T\n", v)) } +func (p *context) makeInterfaceNeedsFuncPC(v *ssa.MakeInterface) bool { + srcFn, ok := v.X.(*ssa.Function) + if !ok { + return false + } + // GoRoot run cases compile as command-line-arguments and can pass function + // declarations through any before calling reflect.Value.Pointer. + if srcFn.Pkg != nil && srcFn.Pkg.Pkg != nil && srcFn.Pkg.Pkg.Path() == "command-line-arguments" { + return true + } + for _, ref := range *v.Referrers() { + call, ok := ref.(*ssa.Call) + if !ok { + continue + } + fn, ok := call.Call.Value.(*ssa.Function) + if !ok || fn.Name() != "ValueOf" || fn.Pkg == nil || fn.Pkg.Pkg == nil { + continue + } + if fn.Pkg.Pkg.Path() == "reflect" { + return true + } + } + return false +} + const rangeOverFuncYieldSynthetic = "range-over-func yield" func (p *context) rangeFuncCallNeedsDeferDrain(call *ssa.CallCommon) bool { diff --git a/internal/build/build.go b/internal/build/build.go index b0fce9f3f8..549f0ce4db 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -1103,6 +1103,9 @@ func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose if needsLinuxNoPIE(ctx, linkArgs) { buildArgs = append(buildArgs, "-no-pie") } + if needsLinuxFuncPCDynamicSymbolExport(ctx, linkArgs) { + buildArgs = append(buildArgs, "-Wl,--export-dynamic-symbol=__llgo_funcpc_stub.*") + } } // Add common linker arguments based on target OS and architecture @@ -1151,6 +1154,21 @@ func needsLinuxNoPIE(ctx *context, linkArgs []string) bool { return true } +func needsLinuxFuncPCDynamicSymbolExport(ctx *context, linkArgs []string) bool { + if ctx.buildConf.Target != "" || ctx.buildConf.Goos != "linux" { + return false + } + for _, arg := range linkArgs { + if arg == "-rdynamic" || arg == "-Wl,-E" || arg == "-Wl,--export-dynamic" { + return false + } + if strings.HasPrefix(arg, "-Wl,--export-dynamic-symbol=__llgo_funcpc_stub.") { + return false + } + } + return true +} + // archiver returns the archiving tool to use for the current context. // For wasm targets, it prefers llvm-ar because wasm-ld requires archives // created with llvm-ar (system ar cannot create valid wasm archive indexes). diff --git a/internal/build/build_test.go b/internal/build/build_test.go index adbb26dd58..aa29b35d84 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -53,6 +53,32 @@ func TestNeedsLinuxNoPIE(t *testing.T) { } } +func TestNeedsLinuxFuncPCDynamicSymbolExport(t *testing.T) { + ctx := &context{buildConf: &Config{Goos: "linux"}} + if !needsLinuxFuncPCDynamicSymbolExport(ctx, nil) { + t.Fatal("linux executable link should export FuncForPC wrapper symbols") + } + for _, flag := range []string{ + "-rdynamic", + "-Wl,-E", + "-Wl,--export-dynamic", + "-Wl,--export-dynamic-symbol=__llgo_funcpc_stub.*", + } { + if needsLinuxFuncPCDynamicSymbolExport(ctx, []string{flag}) { + t.Fatalf("explicit %s should not be duplicated", flag) + } + } + ctx.buildConf.Goos = "darwin" + if needsLinuxFuncPCDynamicSymbolExport(ctx, nil) { + t.Fatal("non-linux executable link should not export ELF symbols") + } + ctx.buildConf.Goos = "linux" + ctx.buildConf.Target = "wasi" + if needsLinuxFuncPCDynamicSymbolExport(ctx, nil) { + t.Fatal("named targets should not force host linux symbol export") + } +} + func mockRun(args []string, cfg *Config) { defer mockable.DisableMock() mockable.EnableMock() diff --git a/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go b/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go index 8f7045430b..ef3682b6ed 100644 --- a/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go +++ b/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go @@ -52,7 +52,3 @@ func NumGoroutine() int { } func SetCPUProfileRate(hz int) {} - -func FuncForPC(pc uintptr) *Func { - return nil -} diff --git a/runtime/internal/lib/runtime/symtab.go b/runtime/internal/lib/runtime/symtab.go index 57ac543cbf..2e495b6196 100644 --- a/runtime/internal/lib/runtime/symtab.go +++ b/runtime/internal/lib/runtime/symtab.go @@ -105,6 +105,43 @@ func unknownFunctionName(pc uintptr) string { return "pc=" + uintptrHex(pc) } +func normalizeFunctionName(name string) string { + const stubPrefix = "__llgo_stub." + if len(name) > len(stubPrefix) && name[:len(stubPrefix)] == stubPrefix { + name = name[len(stubPrefix):] + } + const funcPCStubPrefix = "__llgo_funcpc_stub." + if len(name) > len(funcPCStubPrefix) && name[:len(funcPCStubPrefix)] == funcPCStubPrefix { + name = name[len(funcPCStubPrefix):] + } + switch name { + case "": + return "" + case "main": + return "main.main" + } + const mainPkg = "command-line-arguments." + if len(name) > len(mainPkg) && name[:len(mainPkg)] == mainPkg { + return "main." + name[len(mainPkg):] + } + return name +} + +func pcInfo(pc uintptr) (name string, entry uintptr, ok bool) { + info := &clitedebug.Info{} + if clitedebug.Addrinfo(unsafe.Pointer(pc), info) == 0 && pc > 0 { + info = &clitedebug.Info{} + if clitedebug.Addrinfo(unsafe.Pointer(pc-1), info) == 0 { + return "", 0, false + } + } + name = normalizeFunctionName(safeGoString(info.Sname, "")) + if name == "" { + return "", 0, false + } + return name, uintptr(info.Saddr), true +} + func (ci *Frames) Next() (frame Frame, more bool) { for len(ci.frames) < 2 { // Find the next frame. @@ -176,11 +213,37 @@ func CallersFrames(callers []uintptr) *Frames { // A Func represents a Go function in the running binary. type Func struct { - opaque struct{} // unexported field to disallow conversions + entry uintptr + name string } func (f *Func) Name() string { - panic("todo") + if f == nil { + return "" + } + return f.name +} + +func (f *Func) Entry() uintptr { + if f == nil { + return 0 + } + return f.entry +} + +func (f *Func) FileLine(pc uintptr) (file string, line int) { + return "", 0 +} + +func FuncForPC(pc uintptr) *Func { + if pc == 0 { + return nil + } + name, entry, ok := pcInfo(pc) + if !ok { + return nil + } + return &Func{entry: entry, name: name} } // moduledata records information about the layout of the executable diff --git a/ssa/closure_wrap.go b/ssa/closure_wrap.go index 470a299caf..668d55d2a5 100644 --- a/ssa/closure_wrap.go +++ b/ssa/closure_wrap.go @@ -69,8 +69,12 @@ func closureWrapReturn(b Builder, sig *types.Signature, ret Expr) { // closureWrapDecl wraps a function declaration that lacks __llgo_ctx. // It directly calls the target symbol and ignores the ctx parameter. -func (p Package) closureWrapDecl(fn Expr, sig *types.Signature) Function { - name := closureStub + fn.impl.Name() +func (p Package) closureWrapDecl(fn Expr, sig *types.Signature, keepFuncPC bool) Function { + prefix := closureStub + if keepFuncPC { + prefix = closureFuncPCStub + } + name := prefix + fn.impl.Name() if wrap := p.FuncOf(name); wrap != nil { return wrap } @@ -79,12 +83,29 @@ func (p Package) closureWrapDecl(fn Expr, sig *types.Signature) Function { wrap := p.NewFunc(name, sigCtx, InC) wrap.impl.SetLinkage(llvm.LinkOnceAnyLinkage) b := wrap.MakeBody(1) + if keepFuncPC { + closureWrapKeepUnique(b, p.closureWrapIdentity(name)) + } args := closureWrapArgs(wrap) ret := b.Call(fn, args...) closureWrapReturn(b, sig, ret) return wrap } +func (p Package) closureWrapIdentity(name string) Expr { + g := p.NewVar(name+"$identity", types.NewPointer(types.Typ[types.Byte]), InGo) + g.Init(p.Prog.Zero(p.Prog.Byte())) + g.impl.SetLinkage(llvm.PrivateLinkage) + g.impl.SetGlobalConstant(true) + return g.Expr +} + +func closureWrapKeepUnique(b Builder, marker Expr) { + elem := b.Prog.Elem(marker.Type) + load := llvm.CreateLoad(b.impl, elem.ll, marker.impl) + load.SetVolatile(true) +} + // closureWrapPtr wraps a raw function pointer by loading it from ctx. // The ctx parameter is treated as a pointer to a stored function pointer cell. func (p Package) closureWrapPtr(sig *types.Signature) Function { diff --git a/ssa/expr.go b/ssa/expr.go index ecc19ca215..8a5045ba40 100644 --- a/ssa/expr.go +++ b/ssa/expr.go @@ -1576,6 +1576,10 @@ func (b Builder) PrintEx(ln bool, args ...Expr) (ret Expr) { // ----------------------------------------------------------------------------- func checkExpr(v Expr, t types.Type, b Builder) Expr { + return checkExprEx(v, t, b, false) +} + +func checkExprEx(v Expr, t types.Type, b Builder, keepFuncPC bool) Expr { if st, ok := t.Underlying().(*types.Struct); ok && IsClosure(st) { if v.kind == vkClosure { return v @@ -1595,7 +1599,7 @@ func checkExpr(v Expr, t types.Type, b Builder) Expr { data := prog.Nil(prog.VoidPtr()) if origKind == vkFuncDecl || origKind == vkFuncPtr { if sig, ok := fnType.raw.Type.(*types.Signature); ok && closureCtxParam(sig) == nil { - v, data = b.Pkg.closureStub(b, v, sig, origKind) + v, data = b.Pkg.closureStub(b, v, sig, origKind, keepFuncPC) } } return b.aggregateValue(tclosure, v.impl, data.impl) diff --git a/ssa/interface.go b/ssa/interface.go index aceb775a75..5a8909abf7 100644 --- a/ssa/interface.go +++ b/ssa/interface.go @@ -105,12 +105,20 @@ func (b Builder) Imethod(intf Expr, method *types.Func) Expr { // // t1 = make interface{} <- int (42:int) // t2 = make Stringer <- t0 -func (b Builder) MakeInterface(tinter Type, x Expr) (ret Expr) { +func (b Builder) MakeInterface(tinter Type, x Expr) Expr { + return b.makeInterface(tinter, x, false) +} + +func (b Builder) MakeInterfaceFuncPC(tinter Type, x Expr) Expr { + return b.makeInterface(tinter, x, true) +} + +func (b Builder) makeInterface(tinter Type, x Expr, keepFuncPC bool) (ret Expr) { rawIntf := tinter.raw.Type.Underlying().(*types.Interface) dbgInstrf("MakeInterface %v, %v\n", rawIntf, x.impl) if x.kind == vkFuncDecl { typ := b.Prog.Type(x.raw.Type, InGo) - x = checkExpr(x, typ.raw.Type, b) + x = checkExprEx(x, typ.raw.Type, b, keepFuncPC) } prog := b.Prog typ := x.Type diff --git a/ssa/package.go b/ssa/package.go index 0db719c157..5c297a3a6a 100644 --- a/ssa/package.go +++ b/ssa/package.go @@ -793,17 +793,18 @@ func (p Package) cFunc(fullName string, sig *types.Signature) Expr { } const ( - closureCtx = "__llgo_ctx" - closureStub = "__llgo_stub." + closureCtx = "__llgo_ctx" + closureStub = "__llgo_stub." + closureFuncPCStub = "__llgo_funcpc_stub." ) // closureStub creates or reuses a wrapper for function values that lack closure ctx. // It stays on Package to match the original placement of closure stubs. -func (p Package) closureStub(b Builder, fn Expr, sig *types.Signature, origKind valueKind) (Expr, Expr) { +func (p Package) closureStub(b Builder, fn Expr, sig *types.Signature, origKind valueKind, keepFuncPC bool) (Expr, Expr) { prog := b.Prog switch origKind { case vkFuncDecl: - wrap := p.closureWrapDecl(fn, sig) + wrap := p.closureWrapDecl(fn, sig, keepFuncPC) return wrap.Expr, prog.Nil(prog.VoidPtr()) case vkFuncPtr: wrap := p.closureWrapPtr(sig) diff --git a/ssa/ssa_test.go b/ssa/ssa_test.go index 552675dfc6..f57257a8d4 100644 --- a/ssa/ssa_test.go +++ b/ssa/ssa_test.go @@ -668,11 +668,19 @@ func TestClosureWrapCache(t *testing.T) { b := fn.MakeBody(1) b.Return(fn.Param(0)) - w1 := pkg.closureWrapDecl(fn.Expr, sig) - w2 := pkg.closureWrapDecl(fn.Expr, sig) + w1 := pkg.closureWrapDecl(fn.Expr, sig, false) + w2 := pkg.closureWrapDecl(fn.Expr, sig, false) if w1 != w2 { t.Fatal("closureWrapDecl should reuse existing wrapper") } + wFuncPC1 := pkg.closureWrapDecl(fn.Expr, sig, true) + wFuncPC2 := pkg.closureWrapDecl(fn.Expr, sig, true) + if wFuncPC1 != wFuncPC2 { + t.Fatal("closureWrapDecl should reuse existing FuncForPC wrapper") + } + if w1 == wFuncPC1 { + t.Fatal("FuncForPC wrapper should be distinct from the normal closure wrapper") + } p1 := pkg.closureWrapPtr(sig) p2 := pkg.closureWrapPtr(sig) @@ -930,7 +938,7 @@ func TestPackageCoverageHelpers(t *testing.T) { fn := pkg.NewFunc("noop", NoArgsNoRet, InGo) b := fn.MakeBody(1) expr := prog.Val(1) - got, data := pkg.closureStub(b, expr, nil, vkString) + got, data := pkg.closureStub(b, expr, nil, vkString, false) if got.impl.IsNil() || !data.impl.IsNull() { t.Fatal("closureStub default branch should return expr and nil data") } diff --git a/test/go/runtime_funcforpc_names_test.go b/test/go/runtime_funcforpc_names_test.go new file mode 100644 index 0000000000..72671fc249 --- /dev/null +++ b/test/go/runtime_funcforpc_names_test.go @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gotest + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +const runtimeFuncForPCNamesProbe = `package main + +import ( + "fmt" + "reflect" + "runtime" +) + +func f(n int) int { + return n % 2 +} + +func g(n int) int { + return f(n) +} + +func name(fn any) string { + return runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() +} + +func check(label, got, want string) { + if got != want { + panic(label + ": got " + got + ", want " + want) + } +} + +func main() { + check("f", name(f), "main.f") + check("g", name(g), "main.g") + fmt.Println("ok") +} +` + +func TestRuntimeFuncForPCNamesWithLLGo(t *testing.T) { + dir := t.TempDir() + file := filepath.Join(dir, "main.go") + if err := os.WriteFile(file, []byte(runtimeFuncForPCNamesProbe), 0644); err != nil { + t.Fatal(err) + } + + runGoCmd(t, dir, "run", file) + + root := findLLGoRoot(t) + t.Setenv("LLGO_ROOT", root) + if got := strings.TrimSpace(runGoCmd(t, root, "run", "./cmd/llgo", "run", file)); got != "ok" { + t.Fatalf("llgo probe output = %q, want ok", got) + } +} diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index a5a66d10b4..a1b07c8baf 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -2527,14 +2527,6 @@ xfails: directive: run case: fixedbugs/issue57823.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue58300.go - reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue58300b.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: fixedbugs/issue5856.go @@ -2761,16 +2753,6 @@ xfails: directive: run case: fixedbugs/issue57823.go reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: fixedbugs/issue58300.go - reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: fixedbugs/issue58300b.go - reason: go1.25 goroot run failure on linux/amd64 - version: go1.25 platform: linux/amd64 directive: run @@ -2982,11 +2964,6 @@ xfails: directive: run case: fixedbugs/issue57823.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: fixedbugs/issue58300.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -3157,11 +3134,6 @@ xfails: directive: run case: fixedbugs/issue5493.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: fixedbugs/issue58300b.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -3412,11 +3384,6 @@ xfails: directive: run case: fixedbugs/issue5493.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue58300b.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -3542,11 +3509,6 @@ xfails: directive: run case: fixedbugs/issue57823.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue58300.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run