From 526d45f5342c0187b7077759dd9326886ee54d17 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 24 May 2026 13:16:37 +0800 Subject: [PATCH] fix maymorestack gcflag hook instrumentation --- cl/compile.go | 87 +++++++++++++++++++++++++------- cmd/internal/base/base.go | 3 ++ cmd/internal/base/pass.go | 1 + cmd/internal/build/build.go | 4 ++ cmd/internal/flags/flags.go | 65 ++++++++++++++++++++++++ cmd/internal/flags/flags_test.go | 65 ++++++++++++++++++++++++ cmd/internal/install/install.go | 5 ++ cmd/internal/run/run.go | 4 ++ cmd/internal/test/test.go | 5 ++ internal/build/build.go | 2 + test/go/maymorestack_test.go | 84 ++++++++++++++++++++++++++++++ test/goroot/xfail.yaml | 30 ----------- 12 files changed, 308 insertions(+), 47 deletions(-) create mode 100644 test/go/maymorestack_test.go diff --git a/cl/compile.go b/cl/compile.go index 5b6d7c9fbe..66e630aa4a 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -56,6 +56,7 @@ var ( enableDbg bool enableDbgSyms bool disableInline bool + mayMoreStackHook string // enableExportRename enables //export to use different C symbol names than Go function names. // This is for TinyGo compatibility when using -target flag for embedded targets. @@ -121,6 +122,12 @@ func EnableTrace(b bool) { enableCallTracing = b } +// SetMayMoreStackHook sets the function called at ordinary Go function entry +// when -gcflags=-d=maymorestack=... is supplied. +func SetMayMoreStackHook(name string) { + mayMoreStackHook = name +} + // EnableExportRename enables or disables //export with different C symbol names. // This is enabled when using -target flag for TinyGo compatibility. func EnableExportRename(b bool) { @@ -152,23 +159,24 @@ type pkgInfo struct { type none = struct{} type context struct { - prog llssa.Program - pkg llssa.Package - fn llssa.Function - goFn *ssa.Function - fset *token.FileSet - goProg *ssa.Program - goTyps *types.Package - goPkg *ssa.Package - pyMod string - skips map[string]none - loaded map[*types.Package]*pkgInfo // loaded packages - bvals map[ssa.Value]llssa.Expr // block values - vargs map[*ssa.Alloc][]llssa.Expr // varargs - funcs map[*ssa.Function]llssa.Function - stackDefers map[*ssa.Function]bool - anonDefers map[*ssa.Function]bool - paramDIVars map[*types.Var]llssa.DIVar + prog llssa.Program + pkg llssa.Package + fn llssa.Function + goFn *ssa.Function + fset *token.FileSet + goProg *ssa.Program + goTyps *types.Package + goPkg *ssa.Package + pyMod string + skips map[string]none + loaded map[*types.Package]*pkgInfo // loaded packages + bvals map[ssa.Value]llssa.Expr // block values + vargs map[*ssa.Alloc][]llssa.Expr // varargs + funcs map[*ssa.Function]llssa.Function + stackDefers map[*ssa.Function]bool + anonDefers map[*ssa.Function]bool + maymorestack string + paramDIVars map[*types.Var]llssa.DIVar patches Patches blkInfos []blocks.Info @@ -562,6 +570,9 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do if block.Index == 0 && enableCallTracing && !strings.HasPrefix(fn.Name(), "github.com/goplus/llgo/runtime/internal/runtime.Print") { b.Printf("call " + fn.Name() + "\n\x00") } + if block.Index == 0 { + p.emitMayMoreStackHook(b) + } // place here to avoid wrong current-block if enableDbgSyms && block.Parent().Origin() == nil && block.Index == 0 { p.debugParams(b, block.Parent()) @@ -664,6 +675,48 @@ end: return ret } +func (p *context) emitMayMoreStackHook(b llssa.Builder) { + if p.goFn == nil || p.goFn.Synthetic != "" { + return + } + hook := p.mayMoreStackHookName() + if hook == "" || hook == p.fn.Name() { + return + } + fn := p.pkg.FuncOf(hook) + if fn == nil { + fn = p.pkg.NewFunc(hook, llssa.NoArgsNoRet, llssa.InGo) + } + b.Call(fn.Expr) +} + +func (p *context) mayMoreStackHookName() string { + if p.maymorestack != "" || mayMoreStackHook == "" || p.goTyps == nil { + return p.maymorestack + } + hook := strings.TrimSpace(mayMoreStackHook) + if hook == "" { + return "" + } + pkgPath := llssa.PathOf(p.goTyps) + pkgName := p.goTyps.Name() + if name, ok := strings.CutPrefix(hook, pkgName+"."); ok { + p.maymorestack = pkgPath + "." + name + return p.maymorestack + } + if pkgName == "main" { + if name, ok := strings.CutPrefix(hook, "main."); ok { + p.maymorestack = pkgPath + "." + name + return p.maymorestack + } + } + if strings.HasPrefix(hook, pkgPath+".") { + p.maymorestack = hook + return p.maymorestack + } + return "" +} + const ( RuntimeInit = llssa.PkgRuntime + ".init" ) diff --git a/cmd/internal/base/base.go b/cmd/internal/base/base.go index d073a77dba..8ddb7769ab 100644 --- a/cmd/internal/base/base.go +++ b/cmd/internal/base/base.go @@ -42,6 +42,9 @@ type Command struct { // Flag is a set of flags specific to this command. Flag flag.FlagSet + // PassArgs records build flags accepted for compatibility with cmd/go. + PassArgs *PassArgs + // Commands lists the available commands and help topics. // The order here is the order in which they are printed by 'gop help'. // Note that subcommands are in general best avoided. diff --git a/cmd/internal/base/pass.go b/cmd/internal/base/pass.go index 3b5c7d7d01..7771cc464e 100644 --- a/cmd/internal/base/pass.go +++ b/cmd/internal/base/pass.go @@ -71,6 +71,7 @@ func NewPassArgs(flag *flag.FlagSet) *PassArgs { func PassBuildFlags(cmd *Command) *PassArgs { p := NewPassArgs(&cmd.Flag) + cmd.PassArgs = p p.Bool("n") // Note: "a" flag removed - now handled by flags.AddBuildFlags() p.Bool("linkshared", "race", "msan", "asan", diff --git a/cmd/internal/build/build.go b/cmd/internal/build/build.go index e12629c7f9..5f8d9f46d3 100644 --- a/cmd/internal/build/build.go +++ b/cmd/internal/build/build.go @@ -56,6 +56,10 @@ func runCmd(cmd *base.Command, args []string) { fmt.Fprintln(os.Stderr, err) mockable.Exit(1) } + if err := flags.UpdatePassBuildConfig(conf, cmd.PassArgs.Args); err != nil { + fmt.Fprintln(os.Stderr, err) + mockable.Exit(1) + } args = cmd.Flag.Args() diff --git a/cmd/internal/flags/flags.go b/cmd/internal/flags/flags.go index bd707fbcd8..cae142d114 100644 --- a/cmd/internal/flags/flags.go +++ b/cmd/internal/flags/flags.go @@ -3,11 +3,13 @@ package flags import ( "flag" "fmt" + "strings" "github.com/goplus/llgo/cmd/internal/compilerhash" "github.com/goplus/llgo/internal/build" "github.com/goplus/llgo/internal/buildenv" "github.com/goplus/llgo/internal/optlevel" + "github.com/goplus/llgo/internal/shellparse" ) var OutputFile string @@ -272,3 +274,66 @@ func UpdateBuildConfig(conf *build.Config) error { return nil } + +func UpdatePassBuildConfig(conf *build.Config, args []string) error { + hook, err := mayMoreStackHookFromBuildFlags(args) + if err != nil { + return err + } + conf.MayMoreStack = hook + return nil +} + +func mayMoreStackHookFromBuildFlags(args []string) (string, error) { + var hook string + for i := 0; i < len(args); i++ { + arg := args[i] + var gcflags string + switch { + case arg == "-gcflags": + i++ + if i >= len(args) { + return "", fmt.Errorf("-gcflags requires an argument") + } + gcflags = args[i] + case strings.HasPrefix(arg, "-gcflags="): + gcflags = strings.TrimPrefix(arg, "-gcflags=") + default: + continue + } + got, err := mayMoreStackHookFromGCFlags(gcflags) + if err != nil { + return "", err + } + if got != "" { + hook = got + } + } + return hook, nil +} + +func mayMoreStackHookFromGCFlags(gcflags string) (string, error) { + fields, err := shellparse.Parse(gcflags) + if err != nil { + return "", fmt.Errorf("parse -gcflags: %w", err) + } + for _, field := range fields { + if pattern, rest, ok := strings.Cut(field, "="); ok && !strings.HasPrefix(pattern, "-") { + field = rest + } + if !strings.HasPrefix(field, "-d=") { + continue + } + for _, opt := range strings.Split(strings.TrimPrefix(field, "-d="), ",") { + hook, ok := strings.CutPrefix(opt, "maymorestack=") + if !ok { + continue + } + if hook == "" { + return "", fmt.Errorf("-d=maymorestack requires a function name") + } + return hook, nil + } + } + return "", nil +} diff --git a/cmd/internal/flags/flags_test.go b/cmd/internal/flags/flags_test.go index 129d5a881e..91025664aa 100644 --- a/cmd/internal/flags/flags_test.go +++ b/cmd/internal/flags/flags_test.go @@ -5,6 +5,7 @@ import ( "flag" "testing" + "github.com/goplus/llgo/internal/build" "github.com/goplus/llgo/internal/optlevel" ) @@ -69,3 +70,67 @@ func TestBuildOptimizationFlagsMutuallyExclusive(t *testing.T) { }) } } + +func TestUpdatePassBuildConfigMayMoreStack(t *testing.T) { + tests := []struct { + name string + args []string + want string + }{ + { + name: "direct gcflags", + args: []string{"-gcflags=-d=maymorestack=main.mayMoreStack"}, + want: "main.mayMoreStack", + }, + { + name: "pattern gcflags", + args: []string{"-gcflags=all=-d=maymorestack=main.mayMoreStack"}, + want: "main.mayMoreStack", + }, + { + name: "space separated gcflags", + args: []string{"-gcflags", "-N -d=maymorestack=main.mayMoreStack -l"}, + want: "main.mayMoreStack", + }, + { + name: "comma debug options", + args: []string{"-gcflags=-d=checkptr,maymorestack=main.mayMoreStack"}, + want: "main.mayMoreStack", + }, + { + name: "unrelated gcflags", + args: []string{"-gcflags=-N -l"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf := &build.Config{} + if err := UpdatePassBuildConfig(conf, tt.args); err != nil { + t.Fatalf("UpdatePassBuildConfig() error = %v", err) + } + if conf.MayMoreStack != tt.want { + t.Fatalf("MayMoreStack = %q, want %q", conf.MayMoreStack, tt.want) + } + }) + } +} + +func TestUpdatePassBuildConfigMayMoreStackErrors(t *testing.T) { + tests := []struct { + name string + args []string + }{ + {name: "missing gcflags value", args: []string{"-gcflags"}}, + {name: "empty hook", args: []string{"-gcflags=-d=maymorestack="}}, + {name: "bad quoting", args: []string{"-gcflags='-d=maymorestack=main.mayMoreStack"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := UpdatePassBuildConfig(&build.Config{}, tt.args); err == nil { + t.Fatal("UpdatePassBuildConfig() expected error") + } + }) + } +} diff --git a/cmd/internal/install/install.go b/cmd/internal/install/install.go index 13b1491485..e1a06c419e 100644 --- a/cmd/internal/install/install.go +++ b/cmd/internal/install/install.go @@ -35,6 +35,7 @@ var Cmd = &base.Command{ func init() { Cmd.Run = runCmd + base.PassBuildFlags(Cmd) flags.AddCommonFlags(&Cmd.Flag) flags.AddBuildFlags(&Cmd.Flag) flags.AddEmbeddedFlags(&Cmd.Flag) @@ -51,6 +52,10 @@ func runCmd(cmd *base.Command, args []string) { fmt.Fprintln(os.Stderr, err) mockable.Exit(1) } + if err := flags.UpdatePassBuildConfig(conf, cmd.PassArgs.Args); err != nil { + fmt.Fprintln(os.Stderr, err) + mockable.Exit(1) + } args = cmd.Flag.Args() _, err := build.Do(args, conf) diff --git a/cmd/internal/run/run.go b/cmd/internal/run/run.go index 07d01f5f08..c5a79d1b64 100644 --- a/cmd/internal/run/run.go +++ b/cmd/internal/run/run.go @@ -80,6 +80,10 @@ func runCmdEx(cmd *base.Command, args []string, mode build.Mode) { fmt.Fprintln(os.Stderr, err) mockable.Exit(1) } + if err := flags.UpdatePassBuildConfig(conf, cmd.PassArgs.Args); err != nil { + fmt.Fprintln(os.Stderr, err) + mockable.Exit(1) + } args = cmd.Flag.Args() args, runArgs, err := parseRunArgs(args) diff --git a/cmd/internal/test/test.go b/cmd/internal/test/test.go index a6e0022e3a..08d0f32311 100644 --- a/cmd/internal/test/test.go +++ b/cmd/internal/test/test.go @@ -21,6 +21,7 @@ var Cmd = &base.Command{ func init() { Cmd.Run = runCmd + base.PassBuildFlags(Cmd) flags.AddCommonFlags(&Cmd.Flag) flags.AddBuildFlags(&Cmd.Flag) flags.AddTestFlags(&Cmd.Flag) @@ -43,6 +44,10 @@ func runCmd(cmd *base.Command, args []string) { fmt.Fprintln(os.Stderr, err) mockable.Exit(1) } + if err := flags.UpdatePassBuildConfig(conf, cmd.PassArgs.Args); err != nil { + fmt.Fprintln(os.Stderr, err) + mockable.Exit(1) + } // Match `go test` behavior: set testing.Testing() to true by forcing the // stdlib testing package's testBinary marker to "1" in test binaries. diff --git a/internal/build/build.go b/internal/build/build.go index b0fce9f3f8..2dc9a2b9c0 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -151,6 +151,7 @@ type Config struct { SizeFormat string // size report format: text,json (default text) SizeLevel string // size aggregation level: full,module,package (default module) CompilerHash string // metadata hash for the running compiler (development builds only) + MayMoreStack string // function hook from -gcflags=-d=maymorestack=... // GlobalRewrites specifies compile-time overrides for global string variables. // Keys are fully qualified package paths (e.g. "main" or "github.com/user/pkg"). // Each Rewrites entry maps variable names to replacement string values. Only @@ -281,6 +282,7 @@ func Do(args []string, conf *Config) ([]Package, error) { cl.EnableDebug(IsDbgEnabled()) cl.EnableDbgSyms(IsDbgSymsEnabled()) cl.EnableTrace(IsTraceEnabled()) + cl.SetMayMoreStackHook(conf.MayMoreStack) llssa.Initialize(llssa.InitAll) target := &llssa.Target{ diff --git a/test/go/maymorestack_test.go b/test/go/maymorestack_test.go new file mode 100644 index 0000000000..f898749dd1 --- /dev/null +++ b/test/go/maymorestack_test.go @@ -0,0 +1,84 @@ +//go:build !llgo + +/* + * 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" + "os/exec" + "path/filepath" + "runtime" + "testing" +) + +func TestMayMoreStackGCFlag(t *testing.T) { + _, file, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("runtime.Caller failed") + } + repoRoot := filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..")) + tmp := t.TempDir() + src := filepath.Join(tmp, "main.go") + out := filepath.Join(tmp, "maymorestack") + if runtime.GOOS == "windows" { + out += ".exe" + } + if err := os.WriteFile(src, []byte(mayMoreStackProgram), 0644); err != nil { + t.Fatal(err) + } + + cmd := exec.Command("go", "run", "-tags=dev", "./cmd/llgo", "build", "-gcflags=-d=maymorestack=main.mayMoreStack", "-o", out, src) + cmd.Dir = repoRoot + if data, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("llgo build failed: %v\n%s", err, data) + } + + cmd = exec.Command(out) + if data, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("maymorestack program failed: %v\n%s", err, data) + } +} + +const mayMoreStackProgram = ` +package main + +var count int + +//go:nosplit +func mayMoreStack() { + count++ +} + +func main() { + const want = 8 + anotherFunc(want - 1) + if count != want { + println(count, "!=", want) + panic("wrong number of calls to mayMoreStack") + } +} + +//go:noinline +func anotherFunc(n int) { + var x [16]byte + if n > 1 { + anotherFunc(n - 1) + } + _ = x +} +` diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index a5a66d10b4..576ab315b9 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -2262,11 +2262,6 @@ xfails: directive: run case: inline_callers.go reason: go1.24 goroot ci-mode run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: maymorestack.go - reason: go1.24 goroot ci-mode run failure on linux/amd64 - version: go1.25 platform: linux/amd64 directive: run @@ -2277,11 +2272,6 @@ xfails: directive: run case: inline_callers.go reason: go1.25 goroot ci-mode run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: maymorestack.go - reason: go1.25 goroot ci-mode run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -2292,11 +2282,6 @@ xfails: directive: run case: inline_callers.go reason: go1.26 goroot ci-mode run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: maymorestack.go - reason: go1.26 goroot ci-mode run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: runoutput @@ -3182,11 +3167,6 @@ xfails: directive: run case: inline_callers.go reason: go1.24 goroot ci-mode run failure on darwin/arm64 - - version: go1.24 - platform: darwin/arm64 - directive: run - case: maymorestack.go - reason: go1.24 goroot ci-mode run failure on darwin/arm64 - version: go1.25 platform: darwin/arm64 directive: run @@ -3197,11 +3177,6 @@ xfails: directive: run case: inline_callers.go reason: go1.25 goroot ci-mode run failure on darwin/arm64 - - version: go1.25 - platform: darwin/arm64 - directive: run - case: maymorestack.go - reason: go1.25 goroot ci-mode run failure on darwin/arm64 - version: go1.26 platform: darwin/arm64 directive: run @@ -3212,11 +3187,6 @@ xfails: directive: run case: inline_callers.go reason: go1.26 goroot ci-mode run failure on darwin/arm64 - - version: go1.26 - platform: darwin/arm64 - directive: run - case: maymorestack.go - reason: go1.26 goroot ci-mode run failure on darwin/arm64 - version: go1.26 platform: darwin/arm64 directive: run