diff --git a/benchmark/runtime_funcinfo/.gitignore b/benchmark/runtime_funcinfo/.gitignore new file mode 100644 index 0000000000..d4632d51c8 --- /dev/null +++ b/benchmark/runtime_funcinfo/.gitignore @@ -0,0 +1,2 @@ +out/ +out-* diff --git a/benchmark/runtime_funcinfo/README.md b/benchmark/runtime_funcinfo/README.md new file mode 100644 index 0000000000..b473f94602 --- /dev/null +++ b/benchmark/runtime_funcinfo/README.md @@ -0,0 +1,62 @@ +# Runtime Funcinfo Benchmark + +This benchmark keeps runtime funcinfo measurements comparable across branches by +generating the same probe programs and rebuilding them with each compiler/root +pair in one run. + +It covers: + +- hot runtime metadata calls: `Caller`, `Callers`, `CallersFrames`, + `FuncForPC`, and `Func.FileLine`. +- deep stacks through direct calls, interface calls, and closures. +- many packages and methods, generated from configurable package/method counts. +- cold first-use runtime metadata paths, including lazy table initialization. +- a stdlib-heavy program with `encoding/json`, `text/template`, `regexp`, + `go/parser`, `go/token`, and `net/netip` imports. +- ordinary code (`plain`): pure-compute probes (recursive `fib`, JSON + round-trip, `sort.Ints`, map churn) with no runtime introspection at all, + measuring what the funcinfo machinery costs code that never asks for it. + +Generated modules use `example.com/llgo-bench/...` import paths. This is +intentional: LLGo does not enable caller-frame tracking for stdlib-shaped paths +without a dot, and that would benchmark the fallback path instead of normal +third-party package behavior. + +Example: + +```sh +go run ./benchmark/runtime_funcinfo \ + -runs=11 \ + -llgo-opt=2 \ + -variant go=go \ + -variant main=llgo,/path/to/llgo-main,/path/to/llgo-main-root \ + -variant 2002=llgo,/path/to/llgo-2002,/path/to/llgo-2002-root \ + -variant 2009=llgo,/path/to/llgo-2009,/path/to/llgo-2009-root \ + -variant 2010=llgo,/path/to/llgo-2010,/path/to/llgo-2010-root +``` + +Add `-include-lto` to build an additional `+lto` variant for every LLGo +compiler. LLGo builds use `-O2` by default; pass `-llgo-opt=` to omit the +optimization flag. Add `-scales=6x6,12x12,24x24` to generate separate +`multipkg_*` and `cold_*` scenarios for several package/function counts in one +run. Output is written to `benchmark/runtime_funcinfo/out` by default: + +- `summary.md`: markdown performance and size tables. +- `results.json`: raw build and run data. +- `work/`: generated probe modules. +- `bin/`: generated executables. + +Performance cells are `best/trimmed avg` from process-level runs. The trimmed +average drops one minimum and one maximum when at least three runs are present. +`-iters` is a base iteration count: `hot` uses the full count, `deep` uses a +quarter, `multipkg`/`stdlib` use one twentieth, and `plain` uses 1/2000 +because each operation does substantially more work. + +`multipkg.FuncForPCMany` and `multipkg.FileLineMany` are batch metrics over all +generated target functions (`-packages * -methods`, 144 targets with the default +settings), not single-lookup timings. + +`cold.First*` metrics are single measurements from a fresh process and include +lazy runtime initialization that has not already happened in that process. +`cold.WarmFuncForPCMany` and `cold.WarmFileLineMany` use the same batch target +count as `multipkg`. diff --git a/benchmark/runtime_funcinfo/main.go b/benchmark/runtime_funcinfo/main.go new file mode 100644 index 0000000000..618b78499a --- /dev/null +++ b/benchmark/runtime_funcinfo/main.go @@ -0,0 +1,1596 @@ +package main + +import ( + "encoding/json" + "errors" + "flag" + "fmt" + "math" + "os" + "os/exec" + "path/filepath" + "sort" + "strconv" + "strings" + "time" +) + +type variantFlag []string + +func (v *variantFlag) String() string { + return strings.Join(*v, ";") +} + +func (v *variantFlag) Set(s string) error { + *v = append(*v, s) + return nil +} + +type variant struct { + Name string `json:"name"` + Kind string `json:"kind"` + Tool string `json:"tool"` + Root string `json:"root,omitempty"` + LTO bool `json:"lto,omitempty"` +} + +type scenario struct { + Name string `json:"name"` + Kind string `json:"kind"` + Dir string `json:"-"` + PackageCount int `json:"package_count,omitempty"` + MethodCount int `json:"method_count,omitempty"` + TargetCount int `json:"target_count,omitempty"` + Scale scenarioSize `json:"scale,omitempty"` +} + +type scenarioSize struct { + Packages int `json:"packages"` + Methods int `json:"methods"` +} + +type buildResult struct { + Variant string `json:"variant"` + Scenario string `json:"scenario"` + Binary string `json:"binary"` + Size int64 `json:"size_bytes"` + BuildMS int64 `json:"build_ms"` + Error string `json:"error,omitempty"` +} + +type runResult struct { + Variant string `json:"variant"` + Scenario string `json:"scenario"` + Metrics map[string][]int64 `json:"metrics_ns"` + Error string `json:"error,omitempty"` + Output string `json:"output,omitempty"` + Env map[string]string `json:"env,omitempty"` +} + +type resultFile struct { + GeneratedAt time.Time `json:"generated_at"` + PackageCount int `json:"package_count"` + MethodCount int `json:"method_count"` + Variants []variant `json:"variants"` + Scenarios []string `json:"scenarios"` + ScenarioMeta []scenario `json:"scenario_meta,omitempty"` + Builds []buildResult `json:"builds"` + Runs []runResult `json:"runs"` +} + +func main() { + var variants variantFlag + outDir := flag.String("out", filepath.Join("benchmark", "runtime_funcinfo", "out"), "output directory") + runs := flag.Int("runs", 11, "process runs per executable") + iters := flag.Int("iters", 200000, "inner benchmark iterations") + llgoOpt := flag.String("llgo-opt", "2", "LLGo optimization level passed as -O; empty disables the flag") + scenarioList := flag.String("scenarios", "hot,deep,multipkg,cold,stdlib,plain", "comma-separated scenarios") + includeLTO := flag.Bool("include-lto", false, "also build full-LTO variants for LLGo compilers") + pkgCount := flag.Int("packages", 12, "generated package count for multipkg") + methodCount := flag.Int("methods", 12, "generated functions and methods per generated package") + scaleList := flag.String("scales", "", "optional comma-separated package x method scales for multipkg/cold, for example 6x6,12x12,24x24") + depthList := flag.String("depths", "", "optional comma-separated call depths for the deep scenario, for example 32,128,512") + bigList := flag.String("bigsizes", "", "optional comma-separated funcs x statements sizes for the bigfunc scenario, for example 32x200,16x2000") + flag.Var(&variants, "variant", "variant definition: name=go or name=llgo,/path/to/llgo,/path/to/root") + flag.Parse() + + if len(variants) == 0 { + variants = append(variants, "go=go") + } + parsed, err := parseVariants(variants, *includeLTO) + if err != nil { + fatal(err) + } + if *runs <= 0 { + fatal(errors.New("-runs must be positive")) + } + if *iters <= 0 { + fatal(errors.New("-iters must be positive")) + } + if *pkgCount <= 0 || *methodCount <= 0 { + fatal(errors.New("-packages and -methods must be positive")) + } + scales, err := parseScales(*scaleList) + if err != nil { + fatal(err) + } + depths, err := parseInts(*depthList) + if err != nil { + fatal(err) + } + bigSizes, err := parseScalePairs(*bigList) + if err != nil { + fatal(err) + } + + absOut, err := filepath.Abs(*outDir) + if err != nil { + fatal(err) + } + if err := os.RemoveAll(absOut); err != nil { + fatal(err) + } + for _, dir := range []string{"work", "bin"} { + if err := os.MkdirAll(filepath.Join(absOut, dir), 0755); err != nil { + fatal(err) + } + } + + scenarios, err := generateScenarios(filepath.Join(absOut, "work"), splitList(*scenarioList), *pkgCount, *methodCount, scales, depths, bigSizes) + if err != nil { + fatal(err) + } + + var builds []buildResult + var runsOut []runResult + for _, sc := range scenarios { + for _, v := range parsed { + br := buildScenario(absOut, sc, v, *llgoOpt) + builds = append(builds, br) + if br.Error != "" { + fmt.Fprintf(os.Stderr, "build failed: %s/%s: %s\n", v.Name, sc.Name, br.Error) + continue + } + rr := runScenario(sc, v, br.Binary, *runs, *iters) + runsOut = append(runsOut, rr) + if rr.Error != "" { + fmt.Fprintf(os.Stderr, "run failed: %s/%s: %s\n", v.Name, sc.Name, rr.Error) + } + } + } + + result := resultFile{ + GeneratedAt: time.Now(), + PackageCount: *pkgCount, + MethodCount: *methodCount, + Variants: parsed, + Scenarios: scenarioNames(scenarios), + ScenarioMeta: scenarios, + Builds: builds, + Runs: runsOut, + } + if err := writeJSON(filepath.Join(absOut, "results.json"), result); err != nil { + fatal(err) + } + summary := renderSummary(result) + if err := os.WriteFile(filepath.Join(absOut, "summary.md"), []byte(summary), 0644); err != nil { + fatal(err) + } + fmt.Print(summary) +} + +func parseVariants(values []string, includeLTO bool) ([]variant, error) { + var out []variant + seen := map[string]bool{} + for _, raw := range values { + name, spec, ok := strings.Cut(raw, "=") + if !ok || name == "" || spec == "" { + return nil, fmt.Errorf("bad -variant %q", raw) + } + if seen[name] { + return nil, fmt.Errorf("duplicate variant %q", name) + } + seen[name] = true + var v variant + v.Name = name + switch { + case spec == "go": + v.Kind = "go" + v.Tool = "go" + case strings.HasPrefix(spec, "go,"): + parts := strings.Split(spec, ",") + if len(parts) != 2 || parts[1] == "" { + return nil, fmt.Errorf("bad go variant %q", raw) + } + v.Kind = "go" + v.Tool = parts[1] + case strings.HasPrefix(spec, "llgo,"): + parts := strings.Split(spec, ",") + if len(parts) != 3 || parts[1] == "" || parts[2] == "" { + return nil, fmt.Errorf("bad llgo variant %q", raw) + } + v.Kind = "llgo" + v.Tool = parts[1] + v.Root = parts[2] + default: + return nil, fmt.Errorf("unknown variant kind in %q", raw) + } + out = append(out, v) + if includeLTO && v.Kind == "llgo" { + lto := v + lto.Name = v.Name + "+lto" + lto.LTO = true + out = append(out, lto) + } + } + return out, nil +} + +func generateScenarios(workDir string, names []string, pkgCount, methodCount int, scales []scenarioSize, depths []int, bigSizes []scenarioSize) ([]scenario, error) { + var out []scenario + for _, name := range names { + sizes := []scenarioSize{{Packages: pkgCount, Methods: methodCount}} + if len(scales) != 0 && (name == "multipkg" || name == "cold") { + sizes = scales + } + if name == "deep" { + sizes = []scenarioSize{{Packages: 32}} + if len(depths) != 0 { + sizes = sizes[:0] + for _, d := range depths { + sizes = append(sizes, scenarioSize{Packages: d}) + } + } + } + if name == "bigfunc" { + sizes = []scenarioSize{{Packages: 32, Methods: 200}} + if len(bigSizes) != 0 { + sizes = bigSizes + } + } + for _, size := range sizes { + scenarioName := name + if len(sizes) > 1 { + scenarioName = fmt.Sprintf("%s_%dx%d", name, size.Packages, size.Methods) + if name == "deep" { + scenarioName = fmt.Sprintf("%s_%d", name, size.Packages) + } + } + dir := filepath.Join(workDir, scenarioName) + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, err + } + var err error + switch name { + case "hot": + err = generateHot(dir) + case "deep": + err = generateDeep(dir, size.Packages) + case "multipkg": + err = generateMultipkg(dir, size.Packages, size.Methods) + case "cold": + err = generateCold(dir, size.Packages, size.Methods) + case "stdlib": + err = generateStdlib(dir) + case "plain": + err = generatePlain(dir) + case "bigfunc": + err = generateBigfunc(dir, size.Packages, size.Methods) + default: + return nil, fmt.Errorf("unknown scenario %q", name) + } + if err != nil { + return nil, err + } + sc := scenario{Name: scenarioName, Kind: name, Dir: dir} + if name == "multipkg" || name == "cold" || name == "bigfunc" { + sc.PackageCount = size.Packages + sc.MethodCount = size.Methods + sc.TargetCount = size.Packages * size.Methods + sc.Scale = size + } + out = append(out, sc) + } + } + return out, nil +} + +func parseInts(list string) ([]int, error) { + var out []int + for _, tok := range splitList(list) { + n, err := strconv.Atoi(tok) + if err != nil || n <= 0 { + return nil, fmt.Errorf("bad int %q", tok) + } + out = append(out, n) + } + return out, nil +} + +// parseScalePairs parses "AxB,CxD" lists that are not tied to the +// multipkg/cold flag defaults. +func parseScalePairs(list string) ([]scenarioSize, error) { + var out []scenarioSize + for _, tok := range splitList(list) { + a, b, ok := strings.Cut(tok, "x") + if !ok { + return nil, fmt.Errorf("bad size %q", tok) + } + f, err1 := strconv.Atoi(a) + st, err2 := strconv.Atoi(b) + if err1 != nil || err2 != nil || f <= 0 || st <= 0 { + return nil, fmt.Errorf("bad size %q", tok) + } + out = append(out, scenarioSize{Packages: f, Methods: st}) + } + return out, nil +} + +func writeModule(dir, module string) error { + return os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module "+module+"\n\ngo 1.24\n"), 0644) +} + +func generateHot(dir string) error { + if err := writeModule(dir, "example.com/llgo-bench/hot"); err != nil { + return err + } + return os.WriteFile(filepath.Join(dir, "main.go"), []byte(hotSource), 0644) +} + +func generateDeep(dir string, depth int) error { + if err := writeModule(dir, "example.com/llgo-bench/deep"); err != nil { + return err + } + var b strings.Builder + b.WriteString(deepPrefix) + for i := 0; i < depth; i++ { + fmt.Fprintf(&b, "//go:noinline\nfunc frame%d() { frame%d() }\n\n", i, i+1) + } + fmt.Fprintf(&b, `//go:noinline +func frame%d() { + pc, file, line, ok := runtime.Caller(%d) + if !ok || pc == 0 || file == "" || line == 0 { + panic("bad deep caller") + } + sinkPC = pc + sinkString = file + sinkInt += line +} + +`, depth, depth/2) + suffix := strings.ReplaceAll(deepSuffix, "[64]uintptr", fmt.Sprintf("[%d]uintptr", depth+64)) + for _, m := range []string{"Direct", "Interface", "Closure"} { + suffix = strings.ReplaceAll(suffix, "deep."+m+"32", fmt.Sprintf("deep.%s%d", m, depth)) + } + b.WriteString(suffix) + return os.WriteFile(filepath.Join(dir, "main.go"), []byte(b.String()), 0644) +} + +func generateMultipkg(dir string, pkgCount, methodCount int) error { + if err := writeModule(dir, "example.com/llgo-bench/multipkg"); err != nil { + return err + } + for i := 0; i < pkgCount; i++ { + pkgName := fmt.Sprintf("p%02d", i) + pkgDir := filepath.Join(dir, pkgName) + if err := os.MkdirAll(pkgDir, 0755); err != nil { + return err + } + var b strings.Builder + fmt.Fprintf(&b, "package %s\n\n", pkgName) + b.WriteString("import (\n\t\"reflect\"\n\t\"runtime\"\n") + if i+1 < pkgCount { + fmt.Fprintf(&b, "\tnext \"example.com/llgo-bench/multipkg/p%02d\"\n", i+1) + } + b.WriteString(")\n\n") + fmt.Fprintf(&b, "type T%02d struct { V int }\n", i) + b.WriteString("type Worker interface { M00(int) int }\n\n") + for j := 0; j < methodCount; j++ { + fmt.Fprintf(&b, "//go:noinline\nfunc F%02d_%02d(x int) int { return x + %d }\n\n", i, j, i*100+j) + fmt.Fprintf(&b, "//go:noinline\nfunc (t T%02d) M%02d(x int) int { return t.V + x + %d }\n\n", i, j, j) + } + b.WriteString("//go:noinline\nfunc Targets() []uintptr {\n\treturn []uintptr{\n") + for j := 0; j < methodCount; j++ { + fmt.Fprintf(&b, "\t\treflect.ValueOf(F%02d_%02d).Pointer(),\n", i, j) + } + b.WriteString("\t}\n}\n\n") + b.WriteString("//go:noinline\nfunc Run(x int) int {\n") + b.WriteString("\tpc, _, line, ok := runtime.Caller(0)\n\tif !ok || pc == 0 || line == 0 { panic(\"bad caller\") }\n") + fmt.Fprintf(&b, "\tvar w Worker = T%02d{V: x}\n", i) + b.WriteString("\ttotal := w.M00(x)\n") + for j := 0; j < methodCount; j++ { + fmt.Fprintf(&b, "\ttotal += F%02d_%02d(x)\n", i, j) + fmt.Fprintf(&b, "\ttotal += (T%02d{V: total}).M%02d(x)\n", i, j) + } + if i+1 < pkgCount { + b.WriteString("\ttotal += next.Run(x+1)\n") + } + b.WriteString("\treturn total + line\n}\n") + if err := os.WriteFile(filepath.Join(pkgDir, pkgName+".go"), []byte(b.String()), 0644); err != nil { + return err + } + } + + var main strings.Builder + main.WriteString("package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\t\"time\"\n") + for i := 0; i < pkgCount; i++ { + fmt.Fprintf(&main, "\tp%02d \"example.com/llgo-bench/multipkg/p%02d\"\n", i, i) + } + main.WriteString(")\n\nvar sinkInt int\nvar sinkString string\n\n") + main.WriteString(commonBenchHelpers) + main.WriteString("func main() {\n\titers := benchIters(10000)\n\tvar targets []uintptr\n") + for i := 0; i < pkgCount; i++ { + fmt.Fprintf(&main, "\ttargets = append(targets, p%02d.Targets()...)\n", i) + } + main.WriteString(` + if funcInfoReady(targets) { + measure("multipkg.FuncForPCMany", iters, func() { + total := 0 + for _, pc := range targets { + fn := runtime.FuncForPC(pc) + if fn == nil { + panic("missing func") + } + total += len(fn.Name()) + } + sinkInt += total + }) + measure("multipkg.FileLineMany", iters, func() { + total := 0 + for _, pc := range targets { + fn := runtime.FuncForPC(pc) + if fn == nil { + panic("missing func") + } + file, line := fn.FileLine(pc) + if file == "" || line == 0 { + panic("missing fileline") + } + total += line + len(file) + } + sinkInt += total + }) + } + measure("multipkg.DeepRun", iters, func() { + sinkInt += p00.Run(1) + }) + fmt.Println("sink=", sinkInt, sinkString) +} + +func funcInfoReady(targets []uintptr) bool { + for _, pc := range targets { + fn := runtime.FuncForPC(pc) + if fn == nil { + return false + } + if file, line := fn.FileLine(pc); file == "" || line == 0 { + return false + } + } + return len(targets) != 0 +} +`) + return os.WriteFile(filepath.Join(dir, "main.go"), []byte(main.String()), 0644) +} + +func generateCold(dir string, pkgCount, methodCount int) error { + if err := writeModule(dir, "example.com/llgo-bench/cold"); err != nil { + return err + } + for i := 0; i < pkgCount; i++ { + pkgName := fmt.Sprintf("p%02d", i) + pkgDir := filepath.Join(dir, pkgName) + if err := os.MkdirAll(pkgDir, 0755); err != nil { + return err + } + var b strings.Builder + fmt.Fprintf(&b, "package %s\n\n", pkgName) + b.WriteString("import \"reflect\"\n\n") + for j := 0; j < methodCount; j++ { + fmt.Fprintf(&b, "//go:noinline\nfunc F%02d_%02d(x int) int { return x + %d }\n\n", i, j, i*100+j) + } + b.WriteString("//go:noinline\nfunc Targets() []uintptr {\n\treturn []uintptr{\n") + for j := 0; j < methodCount; j++ { + fmt.Fprintf(&b, "\t\treflect.ValueOf(F%02d_%02d).Pointer(),\n", i, j) + } + b.WriteString("\t}\n}\n") + if err := os.WriteFile(filepath.Join(pkgDir, pkgName+".go"), []byte(b.String()), 0644); err != nil { + return err + } + } + + var main strings.Builder + main.WriteString("package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\t\"time\"\n") + for i := 0; i < pkgCount; i++ { + fmt.Fprintf(&main, "\tp%02d \"example.com/llgo-bench/cold/p%02d\"\n", i, i) + } + main.WriteString(")\n\nvar sinkInt int\nvar sinkString string\n\n") + main.WriteString(commonBenchHelpers) + main.WriteString("func main() {\n\titers := benchIters(10000)\n\tvar targets []uintptr\n") + for i := 0; i < pkgCount; i++ { + fmt.Fprintf(&main, "\ttargets = append(targets, p%02d.Targets()...)\n", i) + } + main.WriteString(` + if len(targets) == 0 { + panic("missing targets") + } + first := targets[len(targets)/2] + start := time.Now() + fn := runtime.FuncForPC(first) + if fn == nil || fn.Name() == "" { + panic("missing first func") + } + fmt.Printf("cold.FirstFuncForPC=%d\n", time.Since(start).Nanoseconds()) + sinkString = fn.Name() + + start = time.Now() + file, line := fn.FileLine(first) + if file == "" || line == 0 { + panic("missing first fileline") + } + fmt.Printf("cold.FirstFileLine=%d\n", time.Since(start).Nanoseconds()) + sinkString = file + sinkInt += line + + start = time.Now() + pc, file, line, ok := runtime.Caller(0) + if !ok || pc == 0 || file == "" || line == 0 { + panic("bad first caller") + } + fmt.Printf("cold.FirstCaller0=%d\n", time.Since(start).Nanoseconds()) + sinkString = file + sinkInt += line + + start = time.Now() + var pcs [16]uintptr + n := runtime.Callers(0, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + // Walk to the first fully symbolized frame: synthetic runtime frames + // (e.g. LLGo's runtime.Callers placeholder) carry no file/line. + for { + frame, more := frames.Next() + if frame.Function != "" && frame.File != "" && frame.Line != 0 { + fmt.Printf("cold.FirstCallersFrames=%d\n", time.Since(start).Nanoseconds()) + sinkString = frame.Function + sinkInt += frame.Line + break + } + if !more { + break + } + } + + measure("cold.WarmFuncForPCMany", iters, func() { + total := 0 + for _, pc := range targets { + fn := runtime.FuncForPC(pc) + if fn == nil { + panic("missing func") + } + total += len(fn.Name()) + } + sinkInt += total + }) + measure("cold.WarmFileLineMany", iters, func() { + total := 0 + for _, pc := range targets { + fn := runtime.FuncForPC(pc) + if fn == nil { + panic("missing func") + } + file, line := fn.FileLine(pc) + if file == "" || line == 0 { + panic("missing fileline") + } + total += len(file) + line + } + sinkInt += total + }) + fmt.Println("sink=", sinkInt, sinkString) +} +`) + return os.WriteFile(filepath.Join(dir, "main.go"), []byte(main.String()), 0644) +} + +func generateStdlib(dir string) error { + if err := writeModule(dir, "example.com/llgo-bench/stdlib"); err != nil { + return err + } + return os.WriteFile(filepath.Join(dir, "main.go"), []byte(stdlibSource), 0644) +} + +// generateBigfunc emits `funcs` functions of `stmts` call-site statements +// each: large bodies stress statement-level pcline density (many sites per +// findfunctab bucket), mid-function pc symbolization, first-use pcline table +// construction at scale, and ordinary-code performance of big method bodies. +func generateBigfunc(dir string, funcs, stmts int) error { + if err := writeModule(dir, "example.com/llgo-bench/bigfunc"); err != nil { + return err + } + var b strings.Builder + b.WriteString(bigfuncPrefix) + for i := 0; i < funcs; i++ { + fmt.Fprintf(&b, "//go:noinline\nfunc big%03d(x int) int {\n", i) + for j := 0; j < stmts; j++ { + b.WriteString("\tx = leaf(x)\n") + } + b.WriteString("\tif captureBig {\n\t\tpc, _, line, ok := runtime.Caller(0)\n\t\tif !ok || line == 0 {\n\t\t\tpanic(\"bad big caller\")\n\t\t}\n\t\tbigPCs = append(bigPCs, pc)\n\t}\n") + b.WriteString("\treturn x\n}\n\n") + } + b.WriteString("//go:noinline\nfunc runAll(x int) int {\n") + for i := 0; i < funcs; i++ { + fmt.Fprintf(&b, "\tx = big%03d(x)\n", i) + } + b.WriteString("\treturn x\n}\n\n") + b.WriteString(bigfuncMain) + return os.WriteFile(filepath.Join(dir, "main.go"), []byte(b.String()), 0644) +} + +func generatePlain(dir string) error { + if err := writeModule(dir, "example.com/llgo-bench/plain"); err != nil { + return err + } + return os.WriteFile(filepath.Join(dir, "main.go"), []byte(plainSource), 0644) +} + +func buildScenario(outDir string, sc scenario, v variant, llgoOpt string) buildResult { + bin := filepath.Join(outDir, "bin", safeName(v.Name)+"_"+sc.Name) + if v.LTO { + bin += "_lto" + } + if exeSuffix := executableSuffix(); exeSuffix != "" { + bin += exeSuffix + } + start := time.Now() + var cmd *exec.Cmd + switch v.Kind { + case "go": + cmd = exec.Command(v.Tool, "build", "-trimpath", "-o", bin, ".") + case "llgo": + args := []string{"build", "-trimpath", "-a", "-o", bin} + if llgoOpt != "" { + args = append(args, "-O"+llgoOpt) + } + if v.LTO { + args = append(args, "-lto=full") + } + args = append(args, ".") + cmd = exec.Command(v.Tool, args...) + default: + return buildResult{Variant: v.Name, Scenario: sc.Name, Binary: bin, Error: "unknown variant kind"} + } + cmd.Dir = sc.Dir + cmd.Env = os.Environ() + if v.Kind == "llgo" { + cmd.Env = append(cmd.Env, "LLGO_ROOT="+v.Root, "LLGO_FUNCINFO=1") + } + out, err := cmd.CombinedOutput() + br := buildResult{Variant: v.Name, Scenario: sc.Name, Binary: bin, BuildMS: time.Since(start).Milliseconds()} + if err != nil { + br.Error = strings.TrimSpace(string(out)) + if br.Error == "" { + br.Error = err.Error() + } + return br + } + info, err := os.Stat(bin) + if err != nil { + br.Error = err.Error() + return br + } + br.Size = info.Size() + return br +} + +func runScenario(sc scenario, v variant, bin string, runs, iters int) runResult { + scenarioIters := iterationsForScenario(sc.Kind, iters) + rr := runResult{ + Variant: v.Name, + Scenario: sc.Name, + Metrics: map[string][]int64{}, + Env: map[string]string{ + "BENCH_ITERS": strconv.Itoa(scenarioIters), + }, + } + for i := 0; i < runs; i++ { + cmd := exec.Command(bin) + cmd.Dir = sc.Dir + cmd.Env = append(os.Environ(), "BENCH_ITERS="+strconv.Itoa(scenarioIters)) + out, err := cmd.CombinedOutput() + if err != nil { + rr.Error = err.Error() + rr.Output = string(out) + return rr + } + metrics, err := parseMetrics(out) + if err != nil { + rr.Error = err.Error() + rr.Output = string(out) + return rr + } + for k, v := range metrics { + rr.Metrics[k] = append(rr.Metrics[k], v) + } + } + return rr +} + +func iterationsForScenario(name string, base int) int { + div := 1 + switch name { + case "deep": + div = 4 + case "multipkg", "cold", "stdlib": + div = 20 + case "plain": + div = 2000 + case "bigfunc": + div = 100 + } + n := base / div + if n < 1 { + return 1 + } + return n +} + +func parseMetrics(out []byte) (map[string]int64, error) { + metrics := map[string]int64{} + for _, raw := range strings.Split(string(out), "\n") { + line := strings.TrimSpace(raw) + if line == "" || !strings.Contains(line, "=") { + continue + } + name, value, _ := strings.Cut(line, "=") + if !strings.Contains(name, ".") { + continue + } + n, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64) + if err != nil { + return nil, fmt.Errorf("parse metric %q: %w", line, err) + } + metrics[strings.TrimSpace(name)] = n + } + return metrics, nil +} + +func renderSummary(result resultFile) string { + var b strings.Builder + fmt.Fprintf(&b, "# Runtime Funcinfo Benchmark\n\nGenerated: `%s`\n\n", result.GeneratedAt.Format(time.RFC3339)) + b.WriteString("Cells are `best/trimmed avg`. Runtime metrics use `ns/op`; sizes use MiB.\n\n") + for _, sc := range result.ScenarioMeta { + if sc.TargetCount == 0 { + continue + } + switch sc.Kind { + case "multipkg": + fmt.Fprintf(&b, "`%s` uses `multipkg.FuncForPCMany` and `multipkg.FileLineMany` batch metrics over %d target functions (%d packages x %d functions).\n\n", + sc.Name, sc.TargetCount, sc.PackageCount, sc.MethodCount) + case "cold": + fmt.Fprintf(&b, "`%s` uses `cold.WarmFuncForPCMany` and `cold.WarmFileLineMany` batch metrics over %d target functions (%d packages x %d functions). `cold.First*` metrics are one per process and include lazy runtime initialization that has not already happened in that process.\n\n", + sc.Name, sc.TargetCount, sc.PackageCount, sc.MethodCount) + } + } + for _, sc := range result.Scenarios { + metrics := metricsForScenario(result.Runs, sc) + if len(metrics) == 0 { + continue + } + fmt.Fprintf(&b, "## %s Performance\n\n", sc) + b.WriteString("| metric |") + for _, v := range result.Variants { + b.WriteString(" " + v.Name + " |") + } + b.WriteString("\n|---|") + for range result.Variants { + b.WriteString("---:|") + } + b.WriteString("\n") + for _, metric := range metrics { + b.WriteString("| " + metric + " |") + for _, v := range result.Variants { + rr, found := findRun(result.Runs, v.Name, sc) + cell := "FAIL" + if found && rr.Error == "" { + cell = "n/a" + } + if vals := rr.Metrics[metric]; len(vals) != 0 { + cell = formatPerf(vals) + } + b.WriteString(" " + cell + " |") + } + b.WriteString("\n") + } + b.WriteString("\n") + } + b.WriteString("## Binary Size\n\n| scenario |") + for _, v := range result.Variants { + b.WriteString(" " + v.Name + " |") + } + b.WriteString("\n|---|") + for range result.Variants { + b.WriteString("---:|") + } + b.WriteString("\n") + for _, sc := range result.Scenarios { + b.WriteString("| " + sc + " |") + for _, v := range result.Variants { + cell := "FAIL" + if br := findBuild(result.Builds, v.Name, sc); br.Error == "" && br.Size > 0 { + cell = formatMiB(br.Size) + } + b.WriteString(" " + cell + " |") + } + b.WriteString("\n") + } + b.WriteString("\n## Build Time\n\n| scenario |") + for _, v := range result.Variants { + b.WriteString(" " + v.Name + " |") + } + b.WriteString("\n|---|") + for range result.Variants { + b.WriteString("---:|") + } + b.WriteString("\n") + for _, sc := range result.Scenarios { + b.WriteString("| " + sc + " |") + for _, v := range result.Variants { + cell := "FAIL" + if br := findBuild(result.Builds, v.Name, sc); br.Error == "" { + cell = formatDurationMS(br.BuildMS) + } + b.WriteString(" " + cell + " |") + } + b.WriteString("\n") + } + return b.String() +} + +func metricsForScenario(runs []runResult, scenario string) []string { + set := map[string]bool{} + for _, rr := range runs { + if rr.Scenario != scenario || rr.Error != "" { + continue + } + for k := range rr.Metrics { + set[k] = true + } + } + out := make([]string, 0, len(set)) + for k := range set { + out = append(out, k) + } + sort.Strings(out) + return out +} + +func findRun(runs []runResult, variant, scenario string) (runResult, bool) { + for _, rr := range runs { + if rr.Variant == variant && rr.Scenario == scenario { + return rr, true + } + } + return runResult{Metrics: map[string][]int64{}}, false +} + +func findBuild(builds []buildResult, variant, scenario string) buildResult { + for _, br := range builds { + if br.Variant == variant && br.Scenario == scenario { + return br + } + } + return buildResult{Error: "missing"} +} + +func formatPerf(values []int64) string { + if len(values) == 0 { + return "n/a" + } + sorted := append([]int64(nil), values...) + sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] }) + best := sorted[0] + avgVals := sorted + if len(sorted) >= 3 { + avgVals = sorted[1 : len(sorted)-1] + } + var sum int64 + for _, v := range avgVals { + sum += v + } + avg := float64(sum) / float64(len(avgVals)) + return formatNS(float64(best)) + "/" + formatNS(avg) +} + +func formatNS(ns float64) string { + switch { + case ns >= 1e6: + return trimFloat(ns/1e6) + "ms" + case ns >= 1e3: + return trimFloat(ns/1e3) + "us" + default: + return trimFloat(ns) + "ns" + } +} + +func formatMiB(bytes int64) string { + return trimFloat(float64(bytes)/(1024*1024)) + " MiB" +} + +func formatDurationMS(ms int64) string { + if ms >= 1000 { + return trimFloat(float64(ms)/1000) + "s" + } + return strconv.FormatInt(ms, 10) + "ms" +} + +func trimFloat(v float64) string { + if math.Abs(v-math.Round(v)) < 0.05 { + return strconv.FormatInt(int64(math.Round(v)), 10) + } + return strconv.FormatFloat(v, 'f', 1, 64) +} + +func writeJSON(path string, data any) error { + raw, err := json.MarshalIndent(data, "", " ") + if err != nil { + return err + } + raw = append(raw, '\n') + return os.WriteFile(path, raw, 0644) +} + +func splitList(s string) []string { + var out []string + for _, part := range strings.Split(s, ",") { + part = strings.TrimSpace(part) + if part != "" { + out = append(out, part) + } + } + return out +} + +func parseScales(s string) ([]scenarioSize, error) { + var out []scenarioSize + for _, part := range splitList(s) { + left, right, ok := strings.Cut(part, "x") + if !ok { + left, right, ok = strings.Cut(part, "X") + } + if !ok { + return nil, fmt.Errorf("bad scale %q: want packages x methods, for example 12x12", part) + } + packages, err := strconv.Atoi(strings.TrimSpace(left)) + if err != nil || packages <= 0 { + return nil, fmt.Errorf("bad package count in scale %q", part) + } + methods, err := strconv.Atoi(strings.TrimSpace(right)) + if err != nil || methods <= 0 { + return nil, fmt.Errorf("bad method count in scale %q", part) + } + out = append(out, scenarioSize{Packages: packages, Methods: methods}) + } + return out, nil +} + +func scenarioNames(scenarios []scenario) []string { + out := make([]string, len(scenarios)) + for i, sc := range scenarios { + out[i] = sc.Name + } + return out +} + +func safeName(s string) string { + replacer := strings.NewReplacer("/", "_", "\\", "_", ":", "_", "+", "_") + return replacer.Replace(s) +} + +func executableSuffix() string { + if os.PathSeparator == '\\' { + return ".exe" + } + return "" +} + +func fatal(err error) { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) +} + +const commonBenchHelpers = ` +func benchIters(def int) int { + if s := getenv("BENCH_ITERS"); s != "" { + n, err := atoi(s) + if err == nil && n > 0 { + return n + } + } + return def +} + +func measure(name string, n int, fn func()) { + fn() + start := time.Now() + for i := 0; i < n; i++ { + fn() + } + elapsed := time.Since(start).Nanoseconds() + if n <= 0 { + panic("bad iterations") + } + fmt.Printf("%s=%d\n", name, elapsed/int64(n)) +} + +func getenv(k string) string { + for _, kv := range os.Environ() { + if len(kv) > len(k) && kv[:len(k)] == k && kv[len(k)] == '=' { + return kv[len(k)+1:] + } + } + return "" +} + +func atoi(s string) (int, error) { + n := 0 + for _, r := range s { + if r < '0' || r > '9' { + return 0, fmt.Errorf("bad int") + } + n = n*10 + int(r-'0') + } + return n, nil +} +` + +const hotSource = `package main + +import ( + "fmt" + "os" + "reflect" + "runtime" + "time" +) + +var sinkInt int +var sinkPC uintptr +var sinkString string + +` + commonBenchHelpers + ` + +//go:noinline +func entryTarget(x int) int { + return x + 7 +} + +//go:noinline +func caller0() { + pc, file, line, ok := runtime.Caller(0) + if !ok || pc == 0 || file == "" || line == 0 { + panic("bad caller0") + } + sinkPC = pc + sinkString = file + sinkInt += line +} + +//go:noinline +func caller1() { + caller1Helper() +} + +//go:noinline +func caller1Helper() { + pc, file, line, ok := runtime.Caller(1) + if !ok || pc == 0 || file == "" || line == 0 { + panic("bad caller1") + } + sinkPC = pc + sinkString = file + sinkInt += line +} + +//go:noinline +func returnPC() uintptr { + pc, _, _, ok := runtime.Caller(0) + if !ok || pc == 0 { + panic("bad return pc") + } + return pc +} + +//go:noinline +func callersOnly() { + var pcs [16]uintptr + n := runtime.Callers(0, pcs[:]) + if n == 0 || pcs[0] == 0 { + panic("bad callers") + } + sinkPC = pcs[0] + sinkInt += n +} + +//go:noinline +func callersFramesFirst() { + var pcs [16]uintptr + n := runtime.Callers(0, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + for { + frame, more := frames.Next() + if frame.Function != "" && frame.File != "" && frame.Line != 0 { + sinkString = frame.Function + sinkInt += frame.Line + return + } + if !more { + break + } + } + panic("bad frame") +} + +func callersFramesReady() (ok bool) { + defer func() { + if recover() != nil { + ok = false + } + }() + callersFramesFirst() + return true +} + +func main() { + iters := benchIters(200000) + entryPC := reflect.ValueOf(entryTarget).Pointer() + returnedPC := returnPC() + measure("hot.Caller0", iters, caller0) + measure("hot.Caller1", iters, caller1) + measure("hot.CallersOnly", iters, callersOnly) + if callersFramesReady() { + measure("hot.CallersFramesFirst", iters, callersFramesFirst) + } + if entryFn := runtime.FuncForPC(entryPC); entryFn != nil && entryFn.Name() != "" { + measure("hot.FuncForPCEntry", iters, func() { + fn := runtime.FuncForPC(entryPC) + if fn == nil { + panic("missing entry func") + } + sinkString = fn.Name() + }) + if file, line := entryFn.FileLine(entryPC); file != "" && line != 0 { + measure("hot.FuncFileLineEntry", iters, func() { + file, line := entryFn.FileLine(entryPC) + if file == "" || line == 0 { + panic("missing entry fileline") + } + sinkString = file + sinkInt += line + }) + } + } + if returnFn := runtime.FuncForPC(returnedPC); returnFn != nil && returnFn.Name() != "" { + measure("hot.FuncForPCReturnPC", iters, func() { + fn := runtime.FuncForPC(returnedPC) + if fn == nil { + panic("missing return func") + } + sinkString = fn.Name() + }) + if file, line := returnFn.FileLine(returnedPC); file != "" && line != 0 { + measure("hot.FuncFileLineReturnPC", iters, func() { + file, line := returnFn.FileLine(returnedPC) + if file == "" || line == 0 { + panic("missing return fileline") + } + sinkString = file + sinkInt += line + }) + } + } + fmt.Println("sink=", sinkInt, sinkPC, sinkString) +} +` + +const deepPrefix = `package main + +import ( + "fmt" + "os" + "runtime" + "time" +) + +var sinkInt int +var sinkPC uintptr +var sinkString string + +` + commonBenchHelpers + ` + +type callerIface interface { + call() +} + +type callerImpl struct{} + +//go:noinline +func (callerImpl) call() { + frame0() +} + +//go:noinline +func closureLayer(next func()) func() { + return func() { + next() + } +} + +//go:noinline +func callInterface(c callerIface) { + c.call() +} + +//go:noinline +func callClosure() { + closureLayer(closureLayer(frame0))() +} + +` + +const deepSuffix = `//go:noinline +func framesAll() { + frame0() + var pcs [64]uintptr + n := runtime.Callers(0, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + total := 0 + for { + frame, more := frames.Next() + if frame.Function != "" { + total += len(frame.Function) + frame.Line + } + if !more { + break + } + } + if total == 0 { + panic("bad frames") + } + sinkInt += total +} + +func deepReady(fn func()) (ok bool) { + defer func() { + if recover() != nil { + ok = false + } + }() + fn() + return true +} + +func main() { + iters := benchIters(50000) + if deepReady(frame0) { + measure("deep.Direct32", iters, frame0) + } + if deepReady(func() { callInterface(callerImpl{}) }) { + measure("deep.Interface32", iters, func() { callInterface(callerImpl{}) }) + } + if deepReady(callClosure) { + measure("deep.Closure32", iters, callClosure) + } + if deepReady(framesAll) { + measure("deep.CallersFramesAll", iters, framesAll) + } + fmt.Println("sink=", sinkInt, sinkPC, sinkString) +} +` + +const stdlibSource = `package main + +import ( + "bytes" + "encoding/json" + "fmt" + "go/parser" + "go/token" + "net/netip" + "os" + "reflect" + "regexp" + "runtime" + "strings" + "text/template" + "time" +) + +var sinkInt int +var sinkString string + +` + commonBenchHelpers + ` + +type payload struct { + Name string + Items []int + Addr string +} + +//go:noinline +func stdTarget(x int) int { + return x*3 + 1 +} + +//go:noinline +func stdWork() { + p := payload{Name: "llgo", Items: []int{1, 2, 3, 5, 8}, Addr: "127.0.0.1:8080"} + raw, err := json.Marshal(p) + if err != nil { + panic(err) + } + var out payload + if err := json.Unmarshal(raw, &out); err != nil { + panic(err) + } + tmpl := template.Must(template.New("x").Funcs(template.FuncMap{"join": strings.Join}).Parse("{{.Name}}:{{join .Words \",\"}}")) + var buf bytes.Buffer + if err := tmpl.Execute(&buf, map[string]any{"Name": out.Name, "Words": []string{"a", "b", "c"}}); err != nil { + panic(err) + } + re := regexp.MustCompile("[a-z]+") + matches := re.FindAllString(buf.String(), -1) + expr, err := parser.ParseExpr("1 + 2*3") + if err != nil || expr == nil { + panic("bad parser") + } + fs := token.NewFileSet() + file := fs.AddFile("bench.go", -1, 100) + file.AddLine(10) + addr := netip.MustParseAddrPort(out.Addr) + sinkInt += len(matches) + int(addr.Port()) + int(file.Line(token.Pos(11))) + sinkString = buf.String() +} + +//go:noinline +func stdCaller() { + pc, file, line, ok := runtime.Caller(0) + if !ok || pc == 0 || file == "" || line == 0 { + panic("bad caller") + } + sinkInt += line + sinkString = file +} + +//go:noinline +func stdFrames() { + var pcs [16]uintptr + n := runtime.Callers(0, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + for { + frame, more := frames.Next() + if frame.Function != "" && frame.File != "" && frame.Line != 0 { + sinkInt += frame.Line + sinkString = frame.Function + return + } + if !more { + break + } + } + panic("bad frame") +} + +func stdFramesReady() (ok bool) { + defer func() { + if recover() != nil { + ok = false + } + }() + stdFrames() + return true +} + +func main() { + iters := benchIters(50000) + entryPC := reflect.ValueOf(stdTarget).Pointer() + measure("stdlib.Work", iters/10, stdWork) + measure("stdlib.Caller0", iters, stdCaller) + if stdFramesReady() { + measure("stdlib.CallersFramesFirst", iters, stdFrames) + } + if fn := runtime.FuncForPC(entryPC); fn != nil && fn.Name() != "" { + measure("stdlib.FuncForPCEntry", iters, func() { + fn := runtime.FuncForPC(entryPC) + if fn == nil { + panic("missing func") + } + sinkString = fn.Name() + }) + if file, line := fn.FileLine(entryPC); file != "" && line != 0 { + measure("stdlib.FuncFileLineEntry", iters, func() { + file, line := fn.FileLine(entryPC) + if file == "" || line == 0 { + panic("missing fileline") + } + sinkInt += line + sinkString = file + }) + } + } + fmt.Println("sink=", sinkInt, sinkString) +} +` + +// plainSource is the ordinary-code scenario: pure compute with no runtime +// introspection at all. It exists to measure what the funcinfo machinery +// costs code that never asks for it (site-asm inline/layout perturbation is +// the only expected effect; the tables themselves are free until first use). +const plainSource = `package main + +import ( + "encoding/json" + "fmt" + "os" + "sort" + "time" +) + +var sinkInt int + +` + commonBenchHelpers + ` + +//go:noinline +func fib(n int) int { + if n < 2 { + return n + } + return fib(n-1) + fib(n-2) +} + +type item struct { + Name string + Value int + Tags []string +} + +func main() { + iters := benchIters(5) + + // Read the depth from the environment so full LTO cannot constant-fold + // the whole call away. + fibN := 30 + if s := getenv("PLAIN_FIB_N"); s != "" { + if n, err := atoi(s); err == nil && n > 0 { + fibN = n + } + } + measure("plain.fib30", iters, func() { + sinkInt += fib(fibN) + }) + + items := make([]item, 2000) + for i := range items { + items[i] = item{Name: fmt.Sprintf("item-%d", i), Value: i * 7, Tags: []string{"a", "b", "c"}} + } + measure("plain.json", iters, func() { + b, err := json.Marshal(items) + if err != nil { + panic(err) + } + var out []item + if err := json.Unmarshal(b, &out); err != nil { + panic(err) + } + sinkInt += len(out) + }) + + measure("plain.sort", iters, func() { + data := make([]int, 200000) + for i := range data { + data[i] = (i*2654435761 + 12345) % 1000003 + } + sort.Ints(data) + sinkInt += data[0] + }) + + measure("plain.map", iters, func() { + m := make(map[int]int, 16) + for i := 0; i < 200000; i++ { + m[(i*2654435761)%100003]++ + } + sinkInt += len(m) + }) + + if sinkInt == 0 { + os.Exit(1) + } +} +` + +const bigfuncPrefix = `package main + +import ( + "fmt" + "os" + "runtime" + "time" +) + +var sinkInt int +var captureBig bool +var bigPCs []uintptr + +` + commonBenchHelpers + ` + +//go:noinline +func leaf(x int) int { return x + 1 } + +` + +const bigfuncMain = `func main() { + iters := benchIters(100) + + // Capture one tail-of-body pc per big function (a statement-level pc + // deep inside a large body, not a function entry). + captureBig = true + sinkInt += runAll(1) + captureBig = false + if len(bigPCs) == 0 { + panic("no big pcs") + } + + // First statement-level FileLine in this process: includes any lazy + // line-table work over funcs*stmts call sites. + t := time.Now() + fn := runtime.FuncForPC(bigPCs[0]) + if fn == nil { + panic("no func") + } + file, line := fn.FileLine(bigPCs[0]) + if file == "" || line == 0 { + panic("bad first fileline") + } + fmt.Printf("bigfunc.FirstFileLineMid=%d\n", time.Since(t).Nanoseconds()) + + measure("bigfunc.FuncForPCMid", iters*100, func() { + for _, pc := range bigPCs { + if runtime.FuncForPC(pc) == nil { + panic("no func") + } + } + }) + + measure("bigfunc.FileLineMid", iters*100, func() { + for _, pc := range bigPCs { + f := runtime.FuncForPC(pc) + if f == nil { + panic("no func") + } + _, l := f.FileLine(pc) + sinkInt += l + } + }) + + measure("bigfunc.CallersFramesMid", iters*10, func() { + var pcs [16]uintptr + n := runtime.Callers(0, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + for { + frame, more := frames.Next() + sinkInt += frame.Line + if !more { + break + } + } + }) + + // Ordinary performance of the large bodies themselves. + measure("bigfunc.Work", iters, func() { + sinkInt += runAll(1) + }) + + if sinkInt == 0 { + os.Exit(1) + } +} +` diff --git a/chore/pclnpost/main.go b/chore/pclnpost/main.go new file mode 100644 index 0000000000..22a7efe2c5 --- /dev/null +++ b/chore/pclnpost/main.go @@ -0,0 +1,43 @@ +/* + * 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. + */ + +// Command pclnpost rewrites a linked LLGo binary's funcinfo entry section +// with the link-phase prebuilt ftab/findfunctab (see +// doc/design/pclntab-linkphase.md and internal/pclnpost). llgo build runs +// the same rewrite automatically; this command exists for manual inspection +// and re-processing. +package main + +import ( + "fmt" + "os" + + "github.com/goplus/llgo/internal/pclnpost" +) + +func main() { + if len(os.Args) != 2 { + fmt.Fprintln(os.Stderr, "usage: pclnpost ") + os.Exit(2) + } + st, err := pclnpost.Rewrite(os.Args[1]) + if err != nil { + fmt.Fprintln(os.Stderr, "pclnpost:", err) + os.Exit(1) + } + fmt.Printf("%s: entry=%d stub=%d kept=%d inlineCopies=%d noSymbol=%d -> ftab=%d buckets=%d\n", + st.Format, st.EntryRecords, st.StubRecords, st.Kept, st.InlineCopies, st.NoSymbol, st.FtabEntries, st.Buckets) +} diff --git a/cl/caller_frame_test.go b/cl/caller_frame_test.go new file mode 100644 index 0000000000..69d570d9ee --- /dev/null +++ b/cl/caller_frame_test.go @@ -0,0 +1,822 @@ +//go:build !llgo +// +build !llgo + +package cl + +import ( + "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" + "strings" + "testing" + + "github.com/goplus/gogen/packages" + llssa "github.com/goplus/llgo/ssa" + gossa "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" +) + +func parseCallerFrameFile(t *testing.T, src string) *ast.File { + t.Helper() + file, err := parser.ParseFile(token.NewFileSet(), "caller_frame.go", src, 0) + if err != nil { + t.Fatal(err) + } + return file +} + +func TestFilesUseRuntimeCaller(t *testing.T) { + tests := []struct { + name string + src string + want bool + }{ + { + name: "runtime selector", + src: `package foo +import "runtime" +func f() { runtime.Caller(0) } +`, + want: true, + }, + { + name: "runtime alias", + src: `package foo +import rt "runtime" +func f() { rt.Callers(0, nil) } +`, + want: true, + }, + { + name: "runtime debug stack", + src: `package foo +import dbg "runtime/debug" +func f() { _ = dbg.Stack() } +`, + want: true, + }, + { + name: "dot import", + src: `package foo +import . "runtime" +func f() { Caller(0) } +`, + want: true, + }, + { + name: "runtime FuncForPC only", + src: `package foo +import "runtime" +func f() { _ = runtime.FuncForPC(0) } +`, + want: false, + }, + { + name: "blank import", + src: `package foo +import _ "runtime" +func f() {} +`, + want: false, + }, + { + name: "non caller runtime selector", + src: `package foo +import "runtime" +func f() { _ = runtime.GOOS } +`, + want: false, + }, + { + name: "caller name without runtime import", + src: `package foo +func f() { Caller(0) } +`, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := filesUseRuntimeCaller([]*ast.File{parseCallerFrameFile(t, tt.src)}); got != tt.want { + t.Fatalf("filesUseRuntimeCaller() = %v, want %v", got, tt.want) + } + }) + } + + badImport := &ast.File{ + Imports: []*ast.ImportSpec{{ + Path: &ast.BasicLit{Kind: token.STRING, Value: "runtime"}, + }}, + } + if filesUseRuntimeCaller([]*ast.File{badImport}) { + t.Fatal("invalid import literal should not enable caller frame tracking") + } +} + +func buildCallerFrameSSAPackage(t *testing.T, pkgPath, src string) (*gossa.Package, []*ast.File) { + t.Helper() + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "caller_frame_compile.go", src, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + files := []*ast.File{file} + imp := packages.NewImporter(fset) + mode := gossa.SanityCheckFunctions | gossa.InstantiateGenerics + ssapkg, _, err := ssautil.BuildPackage( + &types.Config{Importer: imp}, + fset, + types.NewPackage(pkgPath, file.Name.Name), + files, + mode, + ) + if err != nil { + t.Fatal(err) + } + return ssapkg, files +} + +func newLLSSAProgForTarget(t *testing.T, target *llssa.Target) llssa.Program { + t.Helper() + prog := llssa.NewProgram(target) + prog.SetRuntime(func() *types.Package { + rt, err := importer.For("source", nil).Import(llssa.PkgRuntime) + if err != nil { + t.Fatal("load runtime failed:", err) + } + return rt + }) + if target != nil && target.GOARCH != "" { + prog.TypeSizes(types.SizesFor("gc", target.GOARCH)) + } + return prog +} + +func newRuntimeCallerAnalysis(pkg *gossa.Package) *runtimeCallerAnalysis { + funcs, trackable := collectRuntimeCallerFunctions(pkg) + return &runtimeCallerAnalysis{ + pkg: pkg, + funcs: funcs, + trackable: trackable, + callsites: collectRuntimeCallerCallsites(funcs), + memo: make(map[*gossa.Function]bool), + visiting: make(map[*gossa.Function]bool), + } +} + +func TestRuntimeCallerPackageDetection(t *testing.T) { + ssapkg, _ := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" +import "runtime/debug" + +type callerIface interface { Call() } +type callerImpl struct{} +type workerIface interface { Work() } +type workerImpl struct{} + +func direct() { runtime.Caller(0) } +func indirect() { direct() } +func dynamic(f func()) { f() } +func dynamicCaller() { dynamic(direct) } +func (callerImpl) Call() { direct() } +func interfaceDispatch(c callerIface) { c.Call() } +func interfaceCaller(c callerIface) { interfaceDispatch(c) } +func closureLayer(next func()) func() { return func() { next() } } +func closureCaller() { closureLayer(closureLayer(direct))() } +func stack() { _ = debug.Stack() } +func anonOnly() { func() { runtime.Caller(0) }() } +func funcForPCOnly() { _ = runtime.FuncForPC(0) } +func leaf() {} +func callFunc(f func()) { f() } +func callFuncHot() { callFunc(leaf) } +func (workerImpl) Work() {} +func callWorker(w workerIface) { w.Work() } +func workerHot() { var w workerIface = workerImpl{}; callWorker(w) } +func plain() {} +`) + if !packageUsesRuntimeCaller(ssapkg) { + t.Fatal("package should report runtime caller usage") + } + if !fnUsesRuntimeCaller(ssapkg.Func("direct")) { + t.Fatal("direct runtime.Caller use should be detected") + } + if !fnUsesRuntimeCaller(ssapkg.Func("indirect")) { + t.Fatal("transitive runtime.Caller use should be detected") + } + if !fnUsesRuntimeCaller(ssapkg.Func("stack")) { + t.Fatal("runtime/debug.Stack use should be detected") + } + if !fnUsesRuntimeCaller(ssapkg.Func("anonOnly")) { + t.Fatal("runtime caller use in anonymous functions should be detected") + } + if fnUsesRuntimeCaller(ssapkg.Func("plain")) { + t.Fatal("plain function should not report runtime caller usage") + } + runtimeCallerFuncs := runtimeCallerFuncSet(ssapkg) + for _, name := range []string{"dynamic", "dynamicCaller", "interfaceDispatch", "interfaceCaller", "closureLayer", "closureCaller"} { + if !runtimeCallerFuncs[ssapkg.Func(name)] { + t.Fatalf("%s should be tracked because dynamic calls may reach runtime stack APIs", name) + } + } + for _, name := range []string{"leaf", "callFunc", "callFuncHot", "callWorker", "workerHot"} { + if runtimeCallerFuncs[ssapkg.Func(name)] { + t.Fatalf("%s should not be tracked when resolved dynamic targets do not reach runtime stack APIs", name) + } + } + if runtimeCallerFuncs[ssapkg.Func("funcForPCOnly")] { + t.Fatal("FuncForPC-only function should not need caller frame tracking") + } + if runtimeCallerFuncs[ssapkg.Func("plain")] { + t.Fatal("plain function should not be tracked") + } + + for _, name := range []string{"Caller", "Callers", "CallersFrames", "FuncForPC", "Stack"} { + if !isRuntimeCallerName(name) { + t.Fatalf("%s should be a runtime caller metadata function", name) + } + } + if isRuntimeCallerFrameName("FuncForPC") { + t.Fatal("FuncForPC should not require caller frame tracking") + } + if !isRuntimeCallerFrameName("Caller") { + t.Fatal("Caller should require caller frame tracking") + } + if isRuntimeCallerName("Version") { + t.Fatal("Version should not be a runtime caller metadata function") + } + + rtpkg, _ := buildCallerFrameSSAPackage(t, "github.com/goplus/llgo/runtime/internal/lib/runtime", `package runtime +func Caller(skip int) (uintptr, string, int, bool) { return 0, "", 0, false } +func FuncForPC(pc uintptr) uintptr { return 0 } +`) + if !isRuntimeCallerFunc(rtpkg.Func("Caller")) || !isRuntimeCallerLookupFunc(rtpkg.Func("Caller")) { + t.Fatal("LLGo runtime lib Caller should be treated as runtime.Caller") + } + if !isRuntimeCallerFunc(rtpkg.Func("FuncForPC")) { + t.Fatal("LLGo runtime lib FuncForPC should be treated as runtime metadata use") + } + if isRuntimeCallerFrameFunc(rtpkg.Func("FuncForPC")) { + t.Fatal("FuncForPC should not require caller frame tracking") + } + if isRuntimeCallerLookupFunc(rtpkg.Func("FuncForPC")) { + t.Fatal("FuncForPC should not consume caller lookup tokens") + } +} + +func TestRuntimeCallerAnalysisEdgeCases(t *testing.T) { + if fnUsesRuntimeCaller(nil) { + t.Fatal("nil function should not use runtime caller metadata") + } + if fnUsesRuntimeCaller(&gossa.Function{}) { + t.Fatal("function without a package should not use runtime caller metadata") + } + if runtimeCallerFuncSet(nil) != nil { + t.Fatal("nil package should have no runtime caller set") + } + if fnHasDirectRuntimeCaller(nil) { + t.Fatal("nil function should not have direct runtime caller use") + } + if functionBelongsToPackage(nil, nil) { + t.Fatal("nil function/package should not belong to a package") + } + if typeBelongsToPackage(types.Typ[types.Int], nil) { + t.Fatal("types should not belong to a nil package") + } + if isRuntimeCallerLookupFunc(nil) { + t.Fatal("nil function should not be a runtime caller lookup") + } + called := false + forEachCall(nil, func(*gossa.CallCommon) { + called = true + }) + if called { + t.Fatal("forEachCall should ignore nil functions") + } + + ssapkg, _ := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +type I interface { Call() } +type J interface { Call() } +type T struct{} + +func target() { runtime.Caller(0) } +func plain() {} +func call(fn func()) { fn() } +func callRuntime() { call(target) } +func (T) Call() { runtime.Caller(0) } +func viaStatic() { var i I = T{}; i.Call() } +func viaChange(j J) { var i I = j; i.Call() } +func viaParam(i I) { i.Call() } +func passInterface() { var i I = T{}; viaParam(i) } +`) + analysis := newRuntimeCallerAnalysis(ssapkg) + if analysis.fnMayReachRuntimeCaller(nil) { + t.Fatal("nil function should not reach runtime caller metadata") + } + if targets, ok := analysis.functionValueTargets(ssapkg.Func("callRuntime"), ssapkg.Func("target")); !ok || !targets[ssapkg.Func("target")] { + t.Fatal("static function value should resolve to its target") + } + if _, ok := analysis.functionValueTargets(ssapkg.Func("target"), nil); ok { + t.Fatal("nil function value should be unresolved") + } + if _, ok := analysis.functionParamTargets(ssapkg.Func("call"), 99); ok { + t.Fatal("out-of-range function argument should be unresolved") + } + callFn := ssapkg.Func("call") + callParam := callFn.Params[0] + callParams := callFn.Params + callFn.Params = nil + if _, ok := analysis.functionValueTargets(callFn, callParam); ok { + t.Fatal("function parameter missing from Params should be unresolved") + } + callFn.Params = callParams + + iface := ssapkg.Pkg.Scope().Lookup("I").Type().Underlying().(*types.Interface) + method := iface.Method(0) + if !analysis.fnMayReachRuntimeCaller(ssapkg.Func("viaStatic")) { + t.Fatal("static interface dispatch should reach runtime caller metadata") + } + if !analysis.fnMayReachRuntimeCaller(ssapkg.Func("viaChange")) { + t.Fatal("changed interface dispatch should conservatively reach runtime caller metadata") + } + if targets, ok := analysis.interfaceMethodTargets(ssapkg.Func("viaParam"), ssapkg.Func("viaParam").Params[0], method); !ok || len(targets) == 0 { + t.Fatal("interface parameter callsites should resolve concrete method targets") + } + analysis.callsites[ssapkg.Func("viaParam")] = []*gossa.CallCommon{{}} + if _, ok := analysis.interfaceMethodTargets(ssapkg.Func("viaParam"), ssapkg.Func("viaParam").Params[0], method); ok { + t.Fatal("out-of-range interface argument should be unresolved") + } + if _, ok := analysis.interfaceMethodTargets(ssapkg.Func("viaStatic"), nil, method); ok { + t.Fatal("nil interface receiver should be unresolved") + } + if _, ok := analysis.staticInterfaceMethodTargets(&gossa.ChangeInterface{}, method); ok { + t.Fatal("empty interface conversion should be unresolved") + } + viaParam := ssapkg.Func("viaParam") + interfaceParam := viaParam.Params[0] + interfaceParams := viaParam.Params + viaParam.Params = nil + if _, ok := analysis.interfaceMethodTargets(viaParam, interfaceParam, method); ok { + t.Fatal("interface parameter missing from Params should be unresolved") + } + viaParam.Params = interfaceParams + if _, ok := analysis.methodTargetsForType(nil, nil); ok { + t.Fatal("nil method lookup should be unresolved") + } + other := types.NewFunc(token.NoPos, ssapkg.Pkg, "Other", nil) + if _, ok := analysis.methodTargetsForType(ssapkg.Type("T").Type(), other); ok { + t.Fatal("missing interface method should be unresolved") + } + if idx, ok := parameterIndex(ssapkg.Func("target"), nil); ok || idx != 0 { + t.Fatal("nil parameter should not be found") + } + + methodOnlyPkg, _ := buildCallerFrameSSAPackage(t, "example.com/methodonly", `package methodonly +import "runtime" + +type T struct{} +func (T) Call() { runtime.Caller(0) } +var _ = T{} +`) + if runtimeCallerFuncSet(methodOnlyPkg) != nil { + t.Fatal("method-only runtime caller use should not mark top-level functions") + } +} + +func TestCallerFrameTrackingEligibility(t *testing.T) { + if (&context{}).shouldTrackCallerFrames() { + t.Fatal("missing compiler state should not track caller frames") + } + var nilContext *context + if nilContext.shouldTrackCallerFrames() { + t.Fatal("nil context should not track caller frames") + } + + tests := []struct { + name string + pkgPath string + track bool + targetName string + goarch string + want bool + }{ + {name: "enabled user package", pkgPath: "example.com/foo", track: true, want: true}, + {name: "disabled flag", pkgPath: "example.com/foo", want: false}, + {name: "named target", pkgPath: "example.com/foo", track: true, targetName: "esp32", want: false}, + {name: "wasm", pkgPath: "example.com/foo", track: true, goarch: "wasm", want: false}, + {name: "stdlib", pkgPath: "fmt", track: true, want: false}, + {name: "runtime", pkgPath: "runtime", track: true, want: false}, + {name: "llgo runtime", pkgPath: llssa.PkgRuntime, track: true, want: false}, + {name: "llgo runtime internal", pkgPath: "github.com/goplus/llgo/runtime/internal/foo", track: true, want: false}, + {name: "command line package", pkgPath: "command-line-arguments", track: true, want: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ssapkg, _ := buildCallerFrameSSAPackage(t, tt.pkgPath, `package foo +import "runtime" +func f() { runtime.Caller(0) } +`) + prog := llssa.NewProgram(nil) + if tt.targetName != "" { + prog.Target().Target = tt.targetName + } + if tt.goarch != "" { + prog.Target().GOARCH = tt.goarch + } + pkg := prog.NewPackage("foo", tt.pkgPath) + fn := pkg.NewFunc("f", llssa.NoArgsNoRet, llssa.InGo) + goFn := ssapkg.Func("f") + ctx := &context{ + prog: prog, + pkg: pkg, + fn: fn, + goFn: goFn, + trackCallerFrames: tt.track, + runtimeCallerFuncs: runtimeCallerFuncSet(ssapkg), + } + if got := ctx.shouldTrackCallerFrames(); got != tt.want { + t.Fatalf("shouldTrackCallerFrames() = %v, want %v", got, tt.want) + } + }) + } + + if canTrackCallerFramesForPackage("net/http") { + t.Fatal("stdlib paths without dots should not track caller frames") + } +} + +func TestRuntimeFrameNameNormalization(t *testing.T) { + tests := []struct { + in string + want string + }{ + {in: "command-line-arguments.main", want: "main.main"}, + {in: "example.com/foo.f$1", want: "example.com/foo.f.func1"}, + {in: "example.com/foo.f", want: "example.com/foo.f"}, + {in: "example.com/foo.f$", want: "example.com/foo.f$"}, + {in: "example.com/foo.f$inner", want: "example.com/foo.f$inner"}, + } + for _, tt := range tests { + if got := runtimeFrameName(tt.in); got != tt.want { + t.Fatalf("runtimeFrameName(%q) = %q, want %q", tt.in, got, tt.want) + } + } + + if got := (*context)(nil).runtimeCallerFrameName(); got != "" { + t.Fatalf("nil context runtimeCallerFrameName() = %q, want empty", got) + } + if got := (&context{}).runtimeCallerFrameName(); got != "" { + t.Fatalf("empty context runtimeCallerFrameName() = %q, want empty", got) + } + prog := newLLSSAProg(t) + pkg := prog.NewPackage("main", "command-line-arguments") + sig := types.NewSignatureType(nil, nil, nil, nil, nil, false) + ctx := &context{fn: pkg.NewFuncEx("command-line-arguments.f$1", sig, llssa.InGo, false, false)} + if got, want := ctx.runtimeCallerFrameName(), "main.f.func1"; got != want { + t.Fatalf("fallback runtimeCallerFrameName() = %q, want %q", got, want) + } +} + +func TestCompileRuntimeCallerFrameInstrumentation(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime/debug" + +func f() { + _ = debug.Stack() +} +`) + prog := newLLSSAProg(t) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + for _, want := range []string{ + "RecordCallerLocation", + `c"example.com/foo.f`, + } { + if !strings.Contains(ir, want) { + t.Fatalf("compiled caller-frame IR missing %q:\n%s", want, ir) + } + } + for _, old := range []string{"PushCallerFrame", "SetCallerLine", "PopCallerFrame"} { + if strings.Contains(ir, old) { + t.Fatalf("compiled caller-frame IR still contains old %q instrumentation:\n%s", old, ir) + } + } +} + +func TestCompileRuntimeCallerPCLineMetadata(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func top() { + runtime.Caller(0) + leaf() +} + +func leaf() {} +`) + prog := newLLSSAProg(t) + prog.Target().GOOS = "linux" + prog.Target().GOARCH = "amd64" + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + for _, want := range []string{ + `!llgo.pcline = !{!`, + `!"example.com/foo.top"`, + `!"caller_frame_compile.go"`, + "__llgo_pcsite_", + "${:uid}", + `.pushsection llgo_pcline`, + `.quad __llgo_pcsite_`, + } { + if !strings.Contains(ir, want) { + t.Fatalf("missing pcline metadata %s:\n%s", want, ir) + } + } + for _, line := range strings.Split(ir, "\n") { + if strings.Contains(line, "!llgo.pcline") || strings.Contains(line, `!"example.com/foo.top"`) { + if strings.Contains(line, `ptr @`) { + t.Fatalf("pcline metadata should use symbol strings, not function pointers:\n%s", line) + } + } + } +} + +func TestCompileRuntimeCallerPCLineMetadata32Bit(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func top() { + runtime.Caller(0) +} +`) + prog := newLLSSAProgForTarget(t, &llssa.Target{GOOS: "linux", GOARCH: "386"}) + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + for _, want := range []string{ + `.p2align 2`, + `.long __llgo_pcsite_`, + } { + if !strings.Contains(ir, want) { + t.Fatalf("missing 32-bit pcline asm %q:\n%s", want, ir) + } + } +} + +func TestCompileRuntimeCallerPCLineEscapesDollarInInlineAsm(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func top() { + func() { + runtime.Caller(0) + }() +} +`) + prog := newLLSSAProg(t) + prog.Target().GOOS = "linux" + prog.Target().GOARCH = "amd64" + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + if !strings.Contains(ir, `!"example.com/foo.top$1"`) { + t.Fatalf("metadata should keep the original Go symbol name:\n%s", ir) + } + if !strings.Contains(ir, `example.com/foo.top$$1`) { + t.Fatalf("inline asm should escape $ in the associated symbol:\n%s", ir) + } + for _, line := range strings.Split(ir, "\n") { + if strings.Contains(line, `.pushsection llgo_pcline`) && strings.Contains(line, `example.com/foo.top$1`) && !strings.Contains(line, `example.com/foo.top$$1`) { + t.Fatalf("inline asm has an unescaped $ operand:\n%s", line) + } + } +} + +func TestRuntimeCallerInstrumentationEdgeCases(t *testing.T) { + ssapkg, _ := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func top() { + runtime.Caller(0) +} +`) + prog := newLLSSAProgForTarget(t, &llssa.Target{GOOS: "linux", GOARCH: "amd64"}) + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + pkg := prog.NewPackage("foo", "example.com/foo") + fn := pkg.NewFunc("example.com/foo.top", llssa.NoArgsNoRet, llssa.InGo) + ctx := &context{ + prog: prog, + pkg: pkg, + fn: fn, + goFn: ssapkg.Func("top"), + fset: token.NewFileSet(), + trackCallerFrames: true, + runtimeCallerFuncs: runtimeCallerFuncSet(ssapkg), + } + var b llssa.Builder + ctx.pushCallerLocationFrame(b, nil) + ctx.recordRuntimeLocation(b, token.NoPos, "RecordCallerLocation") + ctx.emitPCLineLabel(b, token.NoPos) + ctx.popCallerLocationFrame(b) + + if pos := (&context{}).funcInfoPosition(nil); pos.IsValid() { + t.Fatal("nil function should have no funcinfo position") + } + if canEmitPCLineLabelsForTarget(nil) { + t.Fatal("nil target should not emit pc-line labels") + } + if canEmitPCLineLabelsForTarget(&llssa.Target{GOOS: "linux", GOARCH: "wasm"}) { + t.Fatal("wasm target should not emit pc-line labels") + } + if canEmitPCLineLabelsForTarget(&llssa.Target{GOOS: "linux", GOARCH: "amd64", Target: "esp32"}) { + t.Fatal("named target should not emit pc-line labels") + } + if got, want := asmQuoteSymbol(`a\b"c$d`), `"a\\b\"c$$d"`; got != want { + t.Fatalf("asmQuoteSymbol() = %q, want %q", got, want) + } +} + +func TestCompileRuntimeCallerPCLineMetadataSitesDisabled(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func top() { + runtime.Caller(0) +} +`) + prog := newLLSSAProg(t) + prog.Target().GOOS = "linux" + prog.Target().GOARCH = "amd64" + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(false) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + // Funcinfo metadata still flows... + if !strings.Contains(ir, llssa.FuncInfoMetadataName) { + t.Fatalf("sites disabled should keep funcinfo metadata:\n%s", ir) + } + // ...but no pc-line site labels are emitted. + for _, bad := range []string{"__llgo_pcsite_", ".pushsection llgo_pcline", "!llgo.pcline"} { + if strings.Contains(ir, bad) { + t.Fatalf("sites disabled should not emit pc-line sites, found %q:\n%s", bad, ir) + } + } +} + +func TestCompileRuntimeCallerPCLineMetadataOnDarwin(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func top() { + runtime.Caller(0) +} +`) + prog := newLLSSAProg(t) + prog.Target().GOOS = "darwin" + prog.Target().GOARCH = "arm64" + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + for _, want := range []string{ + `!llgo.pcline`, + "__llgo_pcsite_", + `.pushsection __DATA,__llgo_pcl`, + } { + if !strings.Contains(ir, want) { + t.Fatalf("darwin should emit Mach-O pc-site labels, missing %q:\n%s", want, ir) + } + } + if strings.Contains(ir, `.pushsection llgo_pcline`) { + t.Fatalf("darwin must not use the ELF pcline section syntax:\n%s", ir) + } +} + +func TestCompileRuntimeCallerFrameUsesGoNameForLinkname(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "command-line-arguments", `package main +import "runtime" + +func renamedPC() uintptr { + pc, _, _, _ := runtime.Caller(0) + return pc +} +`) + prog := newLLSSAProg(t) + prog.SetLinkname("command-line-arguments.renamedPC", "main.renamedPCSymbol") + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + if !strings.Contains(ir, `c"main.renamedPC"`) { + t.Fatalf("compiled caller-frame IR missing source function name:\n%s", ir) + } + if strings.Contains(ir, `c"main.renamedPCSymbol"`) { + t.Fatalf("compiled caller-frame IR used linkname target as runtime function name:\n%s", ir) + } +} + +func TestCompileRuntimeCallerFrameInstrumentationSkipped(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func f() { + runtime.Caller(0) +} +`) + prog := newLLSSAProg(t) + prog.Target().Target = "esp32" + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + if ir := pkg.Module().String(); strings.Contains(ir, "RecordCallerLocation") || strings.Contains(ir, "RecordPanicLocation") { + t.Fatalf("target builds should not emit caller location tracking:\n%s", ir) + } + + ssapkg, files = buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +func f() {} +`) + prog = newLLSSAProg(t) + pkg, err = NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + if ir := pkg.Module().String(); strings.Contains(ir, "RecordCallerLocation") || strings.Contains(ir, "RecordPanicLocation") { + t.Fatalf("packages without runtime stack APIs should not emit caller location tracking:\n%s", ir) + } + + ssapkg, files = buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" +func f() { _ = runtime.FuncForPC(0) } +`) + prog = newLLSSAProg(t) + prog.Target().GOOS = "linux" + prog.Target().GOARCH = "amd64" + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + pkg, err = NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + for _, bad := range []string{"RecordCallerLocation", "RecordPanicLocation", "PushCallerLocationFrame", `!llgo.pcline`} { + if strings.Contains(ir, bad) { + t.Fatalf("FuncForPC-only packages should not emit caller frame tracking %q:\n%s", bad, ir) + } + } + if !strings.Contains(ir, `!llgo.funcinfo = !{!`) { + t.Fatalf("FuncForPC-only packages should still emit funcinfo metadata:\n%s", ir) + } +} + +func TestCompileRuntimeCallerLocationOnlyForRuntimePaths(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func helper() {} + +func f() { + helper() + runtime.Caller(0) +} +`) + prog := newLLSSAProg(t) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + if !strings.Contains(ir, "RecordCallerLocation") { + t.Fatalf("runtime.Caller should record caller location:\n%s", ir) + } + if strings.Contains(ir, "SetCallerLine") || strings.Contains(ir, "PushCallerFrame") { + t.Fatalf("caller location tracking should not emit old TLS instrumentation:\n%s", ir) + } +} diff --git a/cl/cltest/cltest.go b/cl/cltest/cltest.go index 51f199bf94..6a151f67ef 100644 --- a/cl/cltest/cltest.go +++ b/cl/cltest/cltest.go @@ -242,7 +242,10 @@ func testFrom(t *testing.T, pkgDir, sel string) { if spec.Mode == littest.ModeSkip { return } - v := llgen.GenFrom(pkgDir) + var v string + withFuncInfoDisabled(func() { + v = llgen.GenFrom(pkgDir) + }) if spec.Mode == littest.ModeFileCheck { if err := littest.Check(spec, v); err != nil { _ = os.WriteFile(pkgDir+"/result.txt", []byte(v), 0644) @@ -294,7 +297,14 @@ func testRunAndTestFrom(t *testing.T, pkgDir, relPkg, sel string, opts runOption } } - output, err := runWithConf(relPkg, pkgDir, conf) + var output []byte + if checkIR { + withFuncInfoDisabled(func() { + output, err = runWithConf(relPkg, pkgDir, conf) + }) + } else { + output, err = runWithConf(relPkg, pkgDir, conf) + } if err != nil { t.Logf("raw output:\n%s", string(output)) t.Fatalf("run failed: %v\noutput: %s", err, string(output)) @@ -509,6 +519,20 @@ func readIRSpec(pkgDir string) (littest.Spec, bool, error) { return spec, true, nil } +func withFuncInfoDisabled(fn func()) { + const key = "LLGO_FUNCINFO" + old, ok := os.LookupEnv(key) + _ = os.Setenv(key, "0") + defer func() { + if ok { + _ = os.Setenv(key, old) + } else { + _ = os.Unsetenv(key) + } + }() + fn() +} + func filterRunOutput(in []byte) []byte { // Tests compare output with expect.txt. Some toolchain/environment warnings are // inherently machine-specific and should not be part of the golden output. @@ -540,8 +564,15 @@ func filterRunOutput(in []byte) []byte { return out.Bytes() } -func TestCompileEx(t *testing.T, src any, fname, expected string, dbg bool) { +func CompileIREx(t *testing.T, src any, fname string, dbg bool, configure func(llssa.Program)) string { t.Helper() + // Build.Do configures cl debug globals for full-package builds. Keep the + // single-file compiler assertions independent from any prior build test. + cl.EnableDebug(dbg) + cl.EnableDbgSyms(dbg) + defer cl.EnableDebug(false) + defer cl.EnableDbgSyms(false) + fset := token.NewFileSet() f, err := parser.ParseFile(fset, fname, src, parser.ParseComments) if err != nil { @@ -563,13 +594,21 @@ func TestCompileEx(t *testing.T, src any, fname, expected string, dbg bool) { foo.WriteTo(os.Stderr) prog := ssatest.NewProgramEx(t, nil, imp) prog.TypeSizes(types.SizesFor("gc", runtime.GOARCH)) + if configure != nil { + configure(prog) + } ret, err := cl.NewPackage(prog, foo, files) if err != nil { t.Fatal("cl.NewPackage failed:", err) } + return ret.String() +} - if v := ret.String(); llssa.StripModuleTarget(v) != expected && expected != ";" { // expected == ";" means skipping out.ll +func TestCompileEx(t *testing.T, src any, fname, expected string, dbg bool) { + t.Helper() + v := CompileIREx(t, src, fname, dbg, nil) + if llssa.StripModuleTarget(v) != expected && expected != ";" { // expected == ";" means skipping out.ll t.Fatalf("\n==> got:\n%s\n==> expected:\n%s\n", v, expected) } } diff --git a/cl/compile.go b/cl/compile.go index 434a25d773..4243c98ba5 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -175,7 +175,15 @@ type context struct { linkOnceFns map[*ssa.Function]none stackDefers map[*ssa.Function]bool anonDefers map[*ssa.Function]bool + stackClears map[ssa.Instruction][]*ssa.Alloc + entryClears map[*ssa.BasicBlock][]*ssa.Alloc + loadClears map[ssa.Instruction]bool + callClobbers map[ssa.Instruction]bool + paramClobbers map[ssa.Instruction]bool + paramScans map[ssa.Instruction][]*ssa.Parameter paramDIVars map[*types.Var]llssa.DIVar + runtimeCallerFuncs map[*ssa.Function]bool + pcLineSeq uint64 patches Patches blkInfos []blocks.Info @@ -199,6 +207,9 @@ type context struct { rewrites map[string]string embedMap goembed.VarMap embedInits []embedInit + + trackCallerFrames bool + callerFrameMark llssa.Expr } func (p *context) rewriteValue(name string) (string, bool) { @@ -214,6 +225,79 @@ func (p *context) rewriteValue(name string) (string, bool) { return val, ok } +func filesUseRuntimeCaller(files []*ast.File) bool { + for _, file := range files { + imports := make(map[string]string) + dotImports := make(map[string]bool) + for _, imp := range file.Imports { + path, err := strconv.Unquote(imp.Path.Value) + if err != nil { + continue + } + switch path { + case "runtime", "runtime/debug": + default: + continue + } + name := path[strings.LastIndex(path, "/")+1:] + if imp.Name != nil { + switch imp.Name.Name { + case ".": + dotImports[path] = true + continue + case "_": + continue + default: + name = imp.Name.Name + } + } + imports[name] = path + } + if len(imports) == 0 && len(dotImports) == 0 { + continue + } + found := false + ast.Inspect(file, func(n ast.Node) bool { + if found { + return false + } + switch n := n.(type) { + case *ast.SelectorExpr: + ident, ok := n.X.(*ast.Ident) + if !ok { + return true + } + if runtimeCallerSelector(imports[ident.Name], n.Sel.Name) { + found = true + return false + } + case *ast.Ident: + if (dotImports["runtime"] && isRuntimeCallerFrameName(n.Name)) || + (dotImports["runtime/debug"] && n.Name == "Stack") { + found = true + return false + } + } + return true + }) + if found { + return true + } + } + return false +} + +func runtimeCallerSelector(path, name string) bool { + switch path { + case "runtime": + return isRuntimeCallerFrameName(name) + case "runtime/debug": + return name == "Stack" + default: + return false + } +} + // isStringPtrType checks if typ is a pointer to the basic string type (*string). // This is used to validate that -ldflags -X can only rewrite variables of type *string, // not derived string types like "type T string". @@ -469,13 +553,27 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun } if fn == nil { fn = pkg.NewFuncEx(name, sig, llssa.Background(ftype), hasCtx, p.needsLinkOnce(f)) - if disableInline { - fn.Inline(llssa.NoInline) - } + } + noInlineDirective := hasNoInlineDirective(f) + runtimeStackNoInline := needsRuntimeStackNoInline(pkgTypes, f) + pcLineNoInline := p.needsPCLineNoInline(f) + if disableInline || noInlineDirective || runtimeStackNoInline || pcLineNoInline { + fn.Inline(llssa.NoInline) + } + if noInlineDirective || runtimeStackNoInline || pcLineNoInline { + fn.DisableTailCalls() } p.funcs[f] = fn isCgo := isCgoExternSymbol(f) if nblk := len(f.Blocks); nblk > 0 { + if p.prog.FuncInfoMetadataEnabled() { + goName := fn.Name() + if pkgTypes != nil { + goName = funcName(pkgTypes, f, false) + } + pos := p.funcInfoPosition(f) + pkg.EmitFuncInfo(fn.Name(), goName, pos.Filename, pos.Line, pos.Column) + } var childInits []func() if len(f.AnonFuncs) > 0 { parentInits := p.inits @@ -500,12 +598,13 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun dbgEnabled := enableDbg && (f == nil || f.Origin() == nil) dbgSymsEnabled := enableDbgSyms && (f == nil || f.Origin() == nil) p.inits = append(p.inits, func() { - oldFn, oldGoFn, oldMethodNilDerefChecks := p.fn, p.goFn, p.methodNilDerefChecks + oldFn, oldGoFn, oldMethodNilDerefChecks, oldCallerFrameMark := p.fn, p.goFn, p.methodNilDerefChecks, p.callerFrameMark p.fn = fn p.goFn = f + p.callerFrameMark = llssa.Nil p.state = state // restore pkgState when compiling funcBody defer func() { - p.fn, p.goFn, p.methodNilDerefChecks = oldFn, oldGoFn, oldMethodNilDerefChecks + p.fn, p.goFn, p.methodNilDerefChecks, p.callerFrameMark = oldFn, oldGoFn, oldMethodNilDerefChecks, oldCallerFrameMark }() p.phis = nil if dbgSymsEnabled { @@ -523,6 +622,21 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun } p.bvals = make(map[ssa.Value]llssa.Expr) p.methodNilDerefChecks = collectMethodNilDerefChecks(f) + if p.enableConservativeLivenessClears(f) { + p.stackClears = p.collectStackClearPlans(f) + p.entryClears = p.collectEntryClearPlans(f) + p.loadClears = make(map[ssa.Instruction]bool) + p.callClobbers = p.collectCallClobberPlans(f) + p.paramClobbers = p.collectParamClobberPlans(f) + p.paramScans = p.collectParamScanPlans(f) + } else { + p.stackClears = nil + p.entryClears = nil + p.loadClears = nil + p.callClobbers = nil + p.paramClobbers = nil + p.paramScans = nil + } off := make([]int, len(f.Blocks)) if isCgo { p.cgoArgs = make([]llssa.Expr, len(f.Params)) @@ -560,6 +674,45 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun return fn, nil, goFunc } +func hasNoInlineDirective(f *ssa.Function) bool { + decl, _ := f.Syntax().(*ast.FuncDecl) + if decl == nil || decl.Doc == nil { + return false + } + for _, c := range decl.Doc.List { + if c.Text == "//go:noinline" { + return true + } + } + return false +} + +func needsRuntimeStackNoInline(pkg *types.Package, f *ssa.Function) bool { + if pkg == nil || f == nil || f.Signature.Recv() != nil { + return false + } + switch pkg.Path() { + case "runtime", "github.com/goplus/llgo/runtime/internal/lib/runtime": + switch f.Name() { + case "Caller", "Callers", "callers": + return true + } + case "github.com/goplus/llgo/runtime/internal/clite/debug": + return f.Name() == "StackTrace" + } + return false +} + +func (p *context) needsPCLineNoInline(f *ssa.Function) bool { + if p == nil || f == nil || !p.prog.FuncInfoSitesEnabled() || !p.trackCallerFrames || !p.runtimeCallerFuncs[f] { + return false + } + if !canEmitPCLineLabelsForTarget(p.prog.Target()) { + return false + } + return p.pkg != nil && canTrackCallerFramesForPackage(p.pkg.Path()) +} + func (p *context) getFuncBodyPos(f *ssa.Function) token.Position { if f.Object() != nil { if fn, ok := f.Object().(*types.Func); ok && fn.Scope() != nil { @@ -569,6 +722,23 @@ func (p *context) getFuncBodyPos(f *ssa.Function) token.Position { return p.goProg.Fset.Position(f.Pos()) } +func (p *context) funcInfoPosition(f *ssa.Function) token.Position { + if f != nil { + switch syntax := f.Syntax().(type) { + case *ast.FuncDecl: + if syntax.Body != nil && len(syntax.Body.List) != 0 { + return p.goProg.Fset.Position(syntax.Body.List[0].Pos()) + } + case *ast.FuncLit: + if syntax.Body != nil && len(syntax.Body.List) != 0 { + return p.goProg.Fset.Position(syntax.Body.List[0].Pos()) + } + } + return p.goProg.Fset.Position(f.Pos()) + } + return token.Position{} +} + func isGlobal(v *types.Var) bool { // TODO(lijie): better implementation return strings.HasPrefix(v.Parent().String(), "package ") @@ -627,6 +797,10 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do var instrs = block.Instrs[n:] var ret = fn.Block(block.Index) b.SetBlock(ret) + p.clearEntryAllocs(b, block) + if block.Index == 0 && p.shouldTrackCallerFrames() { + p.pushCallerLocationFrame(b, block.Parent()) + } if block.Index == 0 && enableCallTracing && !strings.HasPrefix(fn.Name(), "github.com/goplus/llgo/runtime/internal/runtime.Print") { b.Printf("call " + fn.Name() + "\n\x00") } @@ -661,6 +835,9 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do fnOld := pkg.NewFunc(initFnNameOld, llssa.NoArgsNoRet, llssa.InC) b.Call(fnOld.Expr) } + if !(isCgoCfunc || isCgoC2 || isCgoCmacro) && p.shouldSkipLateSetFinalizerValue(instr) { + continue + } if isCgoCfunc || isCgoC2 || isCgoCmacro { switch instr := instr.(type) { case *ssa.Alloc: @@ -699,6 +876,17 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do } else { p.compileInstr(b, instr) } + if isTerminatingInstruction(instr) { + continue + } + p.clearDeadAllocs(b, instr) + if p.callClobbers[instr] { + p.clobberPointerRegs(b) + } + p.scanParamPointers(b, instr) + if p.paramClobbers[instr] { + p.clobberPointerRegs(b) + } } // is cgo cfunc but not return yet, some funcs has multiple blocks if (isCgoCfunc || isCgoC2 || isCgoCmacro) && !cgoReturned { @@ -928,6 +1116,623 @@ func isAllocVargs(ctx *context, v *ssa.Alloc) bool { return false } +func (p *context) enableConservativeLivenessClears(fn *ssa.Function) bool { + if fn == nil || fn.Pkg == nil || fn.Pkg.Pkg == nil { + return false + } + path := fn.Pkg.Pkg.Path() + if path == "command-line-arguments" { + return p.packageUsesRuntimeSetFinalizer(fn.Pkg) + } + return false +} + +func (p *context) packageUsesRuntimeSetFinalizer(pkg *ssa.Package) bool { + for _, member := range pkg.Members { + fn, ok := member.(*ssa.Function) + if ok && p.functionUsesRuntimeSetFinalizer(fn, map[*ssa.Function]bool{}) { + return true + } + } + return false +} + +func (p *context) functionUsesRuntimeSetFinalizer(fn *ssa.Function, seen map[*ssa.Function]bool) bool { + if fn == nil || seen[fn] { + return false + } + seen[fn] = true + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + switch instr := instr.(type) { + case *ssa.Call: + if p.isRuntimeSetFinalizerCall(&instr.Call) { + return true + } + case *ssa.Defer: + if p.isRuntimeSetFinalizerCall(&instr.Call) { + return true + } + case *ssa.Go: + if p.isRuntimeSetFinalizerCall(&instr.Call) { + return true + } + } + } + } + for _, anon := range fn.AnonFuncs { + if p.functionUsesRuntimeSetFinalizer(anon, seen) { + return true + } + } + return false +} + +func hasConservativeGCPointers(t types.Type, seen map[types.Type]bool) bool { + if t == nil { + return false + } + t = types.Unalias(t) + if seen[t] { + return false + } + seen[t] = true + switch t := t.Underlying().(type) { + case *types.Pointer, *types.Slice, *types.Map, *types.Chan, *types.Signature, *types.Interface: + return true + case *types.Basic: + return t.Kind() == types.String || t.Kind() == types.UnsafePointer + case *types.Array: + return hasConservativeGCPointers(t.Elem(), seen) + case *types.Struct: + for i := 0; i < t.NumFields(); i++ { + if hasConservativeGCPointers(t.Field(i).Type(), seen) { + return true + } + } + } + return false +} + +func (p *context) shouldClearAlloc(v *ssa.Alloc) bool { + if v == nil || v.Comment == "varargs" || v.Comment == "makeslice" { + return false + } + ptr, ok := v.Type().Underlying().(*types.Pointer) + return ok && hasConservativeGCPointers(ptr.Elem(), map[types.Type]bool{}) +} + +func blockCanReach(from, to *ssa.BasicBlock, seen map[*ssa.BasicBlock]bool) bool { + if from == nil || to == nil { + return false + } + if from == to { + return true + } + if seen[from] { + return false + } + seen[from] = true + for _, succ := range from.Succs { + if blockCanReach(succ, to, seen) { + return true + } + } + return false +} + +func refBlock(ref ssa.Instruction) *ssa.BasicBlock { + if ref == nil { + return nil + } + return ref.Block() +} + +func instructionUsesValue(instr ssa.Instruction, v ssa.Value) bool { + if instr == nil || v == nil { + return false + } + for _, operand := range instr.Operands(nil) { + if operand != nil && *operand == v { + return true + } + } + return false +} + +func isCallLikeInstruction(instr ssa.Instruction) bool { + switch instr.(type) { + case *ssa.Call, *ssa.Defer, *ssa.Go: + return true + } + return false +} + +func isTerminatingInstruction(instr ssa.Instruction) bool { + switch instr.(type) { + case *ssa.Jump, *ssa.Return, *ssa.If, *ssa.Panic: + return true + } + return false +} + +func (p *context) isRuntimeSetFinalizerCall(call *ssa.CallCommon) bool { + if call == nil { + return false + } + fn, ok := call.Value.(*ssa.Function) + if !ok || fn == nil || fn.Pkg == nil || fn.Pkg.Pkg == nil { + return false + } + return fn.Name() == "SetFinalizer" && + fn.Pkg.Pkg.Path() == "github.com/goplus/llgo/runtime/internal/lib/runtime" +} + +func (p *context) isOnlyRuntimeSetFinalizerArg(v ssa.Value) bool { + refs := v.Referrers() + if refs == nil || len(*refs) != 1 { + return false + } + call, ok := (*refs)[0].(*ssa.Call) + return ok && p.isRuntimeSetFinalizerCall(&call.Call) +} + +func (p *context) shouldSkipLateSetFinalizerValue(instr ssa.Instruction) bool { + switch instr := instr.(type) { + case *ssa.MakeInterface: + return p.isOnlyRuntimeSetFinalizerArg(instr) + case *ssa.UnOp: + if instr.Op != token.MUL { + return false + } + refs := instr.Referrers() + if refs == nil || len(*refs) != 1 { + return false + } + mi, ok := (*refs)[0].(*ssa.MakeInterface) + return ok && p.isOnlyRuntimeSetFinalizerArg(mi) + } + return false +} + +func (p *context) collectValueUseBlocks(v ssa.Value, blocks map[*ssa.BasicBlock]bool, seen map[ssa.Value]bool, followPhi bool) bool { + if v == nil || seen[v] { + return true + } + seen[v] = true + refs := v.Referrers() + if refs == nil { + return true + } + for _, ref := range *refs { + switch ref := ref.(type) { + case *ssa.DebugRef: + continue + case *ssa.FieldAddr, *ssa.IndexAddr, *ssa.ChangeType, *ssa.Convert, *ssa.MakeInterface: + refVal, ok := ref.(ssa.Value) + if !ok { + return false + } + if !p.collectValueUseBlocks(refVal, blocks, seen, followPhi) { + return false + } + case *ssa.UnOp: + if ref.Op != token.MUL || ref.X != v { + blk := refBlock(ref) + if blk == nil { + return false + } + blocks[blk] = true + continue + } + blk := refBlock(ref) + if blk == nil { + return false + } + blocks[blk] = true + if !p.collectValueUseBlocks(ref, blocks, seen, followPhi) { + return false + } + case *ssa.Phi: + if followPhi { + if !p.collectValueUseBlocks(ref, blocks, seen, followPhi) { + return false + } + continue + } + for i, edge := range ref.Edges { + if edge == v && i < len(ref.Block().Preds) { + blocks[ref.Block().Preds[i]] = true + } + } + default: + instr, ok := ref.(ssa.Instruction) + if !ok || !instructionUsesValue(instr, v) { + return false + } + blk := refBlock(instr) + if blk == nil { + return false + } + blocks[blk] = true + } + } + return true +} + +func (p *context) valueLastUseBlock(v ssa.Value) (*ssa.BasicBlock, bool) { + blocks := make(map[*ssa.BasicBlock]bool) + if !p.collectValueUseBlocks(v, blocks, map[ssa.Value]bool{}, true) { + return nil, false + } + if len(blocks) == 0 { + return nil, true + } + for candidate := range blocks { + ok := true + for blk := range blocks { + if blk != candidate && !blockCanReach(blk, candidate, map[*ssa.BasicBlock]bool{}) { + ok = false + break + } + } + if ok { + return candidate, true + } + } + return nil, false +} + +func (p *context) lastUseInBlock(v ssa.Value, blk *ssa.BasicBlock, order map[ssa.Instruction]int, seen map[ssa.Value]bool) (ssa.Instruction, bool) { + if v == nil || seen[v] { + return nil, true + } + seen[v] = true + refs := v.Referrers() + if refs == nil { + return nil, true + } + var last ssa.Instruction + updateLast := func(instr ssa.Instruction) { + if instr == nil { + return + } + if last == nil || order[instr] > order[last] { + last = instr + } + } + refBeforeBlock := func(refBlk *ssa.BasicBlock) bool { + return refBlk != nil && blk != nil && refBlk != blk && blockCanReach(refBlk, blk, map[*ssa.BasicBlock]bool{}) + } + for _, ref := range *refs { + switch ref := ref.(type) { + case *ssa.DebugRef: + continue + case *ssa.FieldAddr, *ssa.IndexAddr, *ssa.ChangeType, *ssa.Convert, *ssa.MakeInterface: + refVal := ref.(ssa.Value) + refInstr := ref.(ssa.Instruction) + if refInstr.Block() != blk { + if refBeforeBlock(refInstr.Block()) { + continue + } + return nil, false + } + use, ok := p.lastUseInBlock(refVal, blk, order, seen) + if !ok { + return nil, false + } + updateLast(use) + case *ssa.UnOp: + if ref.Op != token.MUL || ref.X != v { + if ref.Block() != blk { + if refBeforeBlock(ref.Block()) { + continue + } + return nil, false + } + updateLast(ref) + continue + } + if ref.Block() != blk { + if refBeforeBlock(ref.Block()) { + continue + } + return nil, false + } + use, ok := p.lastUseInBlock(ref, blk, order, seen) + if !ok { + return nil, false + } + if use != nil { + if isCallLikeInstruction(use) { + updateLast(ref) + continue + } + updateLast(use) + } else { + updateLast(ref) + } + case *ssa.Phi: + use, ok := p.lastUseInBlock(ref, blk, order, seen) + if !ok { + return nil, false + } + updateLast(use) + default: + instr, ok := ref.(ssa.Instruction) + if !ok || !instructionUsesValue(instr, v) { + return nil, false + } + if instr.Block() != blk { + if refBeforeBlock(instr.Block()) { + continue + } + return nil, false + } + updateLast(instr) + } + } + return last, true +} + +func (p *context) collectStackClearPlans(fn *ssa.Function) map[ssa.Instruction][]*ssa.Alloc { + plans := make(map[ssa.Instruction][]*ssa.Alloc) + for _, blk := range fn.Blocks { + for _, instr := range blk.Instrs { + alloc, ok := instr.(*ssa.Alloc) + if !ok || !p.shouldClearAlloc(alloc) { + continue + } + useBlk, ok := p.valueLastUseBlock(alloc) + if !ok || useBlk == nil { + continue + } + if useBlk != alloc.Block() && alloc.Block().Index != 0 { + continue + } + order := make(map[ssa.Instruction]int, len(useBlk.Instrs)) + for i, useInstr := range useBlk.Instrs { + order[useInstr] = i + } + last, ok := p.lastUseInBlock(alloc, useBlk, order, map[ssa.Value]bool{}) + if ok && last != nil { + plans[last] = append(plans[last], alloc) + } + } + } + return plans +} + +func (p *context) collectEntryClearPlans(fn *ssa.Function) map[*ssa.BasicBlock][]*ssa.Alloc { + plans := make(map[*ssa.BasicBlock][]*ssa.Alloc) + for _, blk := range fn.Blocks { + if blk == nil || len(blk.Succs) < 2 { + continue + } + for _, instr := range blk.Instrs { + alloc, ok := instr.(*ssa.Alloc) + if !ok || !p.shouldClearAlloc(alloc) { + continue + } + useBlocks := make(map[*ssa.BasicBlock]bool) + if !p.collectValueUseBlocks(alloc, useBlocks, map[ssa.Value]bool{}, false) { + continue + } + liveSucc := make(map[*ssa.BasicBlock]bool, len(blk.Succs)) + for _, succ := range blk.Succs { + for useBlk := range useBlocks { + if useBlk == nil { + continue + } + if succ == useBlk || blockCanReach(succ, useBlk, map[*ssa.BasicBlock]bool{}) { + liveSucc[succ] = true + break + } + } + } + if len(liveSucc) == 0 || len(liveSucc) == len(blk.Succs) { + continue + } + for _, succ := range blk.Succs { + if !liveSucc[succ] && len(succ.Preds) == 1 { + plans[succ] = append(plans[succ], alloc) + } + } + } + } + return plans +} + +func (p *context) collectParamClobberPlans(fn *ssa.Function) map[ssa.Instruction]bool { + plans := make(map[ssa.Instruction]bool) + for _, param := range fn.Params { + if !hasConservativeGCPointers(param.Type(), map[types.Type]bool{}) { + continue + } + useBlk, ok := p.valueLastUseBlock(param) + if !ok || useBlk == nil { + continue + } + order := make(map[ssa.Instruction]int, len(useBlk.Instrs)) + for i, useInstr := range useBlk.Instrs { + order[useInstr] = i + } + last, ok := p.lastUseInBlock(param, useBlk, order, map[ssa.Value]bool{}) + if ok && last != nil { + plans[last] = true + } + } + return plans +} + +func (p *context) collectParamScanPlans(fn *ssa.Function) map[ssa.Instruction][]*ssa.Parameter { + plans := make(map[ssa.Instruction][]*ssa.Parameter) + for _, param := range fn.Params { + if !hasConservativeGCPointers(param.Type(), map[types.Type]bool{}) { + continue + } + useBlk, ok := p.valueLastUseBlock(param) + if !ok || useBlk == nil { + continue + } + order := make(map[ssa.Instruction]int, len(useBlk.Instrs)) + for i, useInstr := range useBlk.Instrs { + order[useInstr] = i + } + last, ok := p.lastUseInBlock(param, useBlk, order, map[ssa.Value]bool{}) + if ok && last != nil { + plans[last] = append(plans[last], param) + } + } + return plans +} + +func (p *context) collectCallClobberPlans(fn *ssa.Function) map[ssa.Instruction]bool { + plans := make(map[ssa.Instruction]bool) + for _, blk := range fn.Blocks { + for _, instr := range blk.Instrs { + call, ok := instr.(*ssa.Call) + if !ok { + continue + } + for _, arg := range call.Common().Args { + if hasConservativeGCPointers(arg.Type(), map[types.Type]bool{}) { + plans[instr] = true + break + } + } + } + } + return plans +} + +func (p *context) compileLateValue(b llssa.Builder, v ssa.Value) llssa.Expr { + switch v := v.(type) { + case *ssa.MakeInterface: + t := p.type_(v.Type(), llssa.InGo) + x := p.compileLateValue(b, v.X) + return b.MakeInterface(t, x) + case *ssa.UnOp: + if v.Op != token.MUL { + return p.compileValue(b, v) + } + x := p.compileLateValue(b, v.X) + return b.UnOp(v.Op, x) + case *ssa.FieldAddr: + x := p.compileLateValue(b, v.X) + return b.FieldAddr(x, v.Field) + case *ssa.IndexAddr: + x := p.compileLateValue(b, v.X) + idx := p.compileLateValue(b, v.Index) + return b.IndexAddr(x, idx) + case *ssa.ChangeType: + t := p.type_(v.Type(), llssa.InGo) + x := p.compileLateValue(b, v.X) + return b.ChangeType(t, x) + case *ssa.Convert: + t := p.type_(v.Type(), llssa.InGo) + x := p.compileLateValue(b, v.X) + return b.Convert(t, x) + } + return p.compileValue(b, v) +} + +func (p *context) scanStackPointer(b llssa.Builder, val llssa.Expr) { + b.Pkg.NeedRuntime = true + t := p.type_(types.Typ[types.Uintptr], llssa.InGo) + if !types.Identical(val.RawType(), t.RawType()) { + val = b.Convert(t, val) + } + fn := b.Pkg.NewFunc("llgo_clear_stack_ptr", + types.NewSignatureType(nil, nil, nil, types.NewTuple(types.NewParam(token.NoPos, nil, "target", types.Typ[types.Uintptr])), nil, false), llssa.InC) + b.Call(fn.Expr, val) +} + +func (p *context) scanPointerExpr(b llssa.Builder, val llssa.Expr) { + switch t := types.Unalias(val.RawType()).Underlying().(type) { + case *types.Pointer: + p.scanStackPointer(b, val) + case *types.Struct: + if t.NumFields() == 1 { + if _, ok := types.Unalias(t.Field(0).Type()).Underlying().(*types.Pointer); ok { + p.scanStackPointer(b, b.Field(val, 0)) + } + } + } +} + +func (p *context) scanAllocPointer(b llssa.Builder, ptr llssa.Expr) { + elem := b.Prog.Elem(ptr.Type) + switch t := types.Unalias(elem.RawType()).Underlying().(type) { + case *types.Pointer: + p.scanStackPointer(b, b.Load(ptr)) + case *types.Struct: + if t.NumFields() == 1 { + if _, ok := types.Unalias(t.Field(0).Type()).Underlying().(*types.Pointer); ok { + p.scanStackPointer(b, b.Load(b.FieldAddr(ptr, 0))) + } + } + } +} + +func (p *context) scanParamPointers(b llssa.Builder, instr ssa.Instruction) { + params := p.paramScans[instr] + for _, param := range params { + p.scanPointerExpr(b, p.compileValue(b, param)) + } +} + +func (p *context) clearAlloc(b llssa.Builder, alloc *ssa.Alloc) { + ptr := p.compileValue(b, alloc) + b.IfThen(b.BinOp(token.NEQ, ptr, p.prog.Zero(ptr.Type)), func() { + p.scanAllocPointer(b, ptr) + elem := b.Prog.Elem(ptr.Type) + b.Store(ptr, p.prog.Zero(elem)) + }) +} + +func (p *context) clearDeadAllocs(b llssa.Builder, instr ssa.Instruction) { + if p.loadClears[instr] { + return + } + allocs := p.stackClears[instr] + if len(allocs) == 0 { + return + } + for _, alloc := range allocs { + p.clearAlloc(b, alloc) + } + if unop, ok := instr.(*ssa.UnOp); ok && unop.Op == token.MUL { + return + } + p.clobberPointerRegs(b) +} + +func (p *context) clearEntryAllocs(b llssa.Builder, block *ssa.BasicBlock) { + allocs := p.entryClears[block] + if len(allocs) == 0 { + return + } + for _, alloc := range allocs { + p.clearAlloc(b, alloc) + } + p.clobberPointerRegs(b) +} + +func (p *context) clobberPointerRegs(b llssa.Builder) { + b.Pkg.NeedRuntime = true + uintptrParam := func(name string) *types.Var { + return types.NewParam(token.NoPos, nil, name, types.Typ[types.Uintptr]) + } + fn := b.Pkg.NewFunc("llgo_clobber_pointer_regs", + types.NewSignatureType(nil, nil, nil, types.NewTuple( + uintptrParam("a0"), uintptrParam("a1"), uintptrParam("a2"), uintptrParam("a3"), + uintptrParam("a4"), uintptrParam("a5"), uintptrParam("a6"), uintptrParam("a7"), + ), nil, false), llssa.InC) + zero := b.Prog.IntVal(0, b.Prog.Uintptr()) + b.Call(fn.Expr, zero, zero, zero, zero, zero, zero, zero, zero) +} + func isPhi(i ssa.Instruction) bool { _, ok := i.(*ssa.Phi) return ok @@ -1015,6 +1820,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue if t := p.type_(v.Type(), llssa.InGo); t.RawType() != nil { if p.isLargeNonPointerValue(t) { x := p.compileValue(b, v.X) + p.recordPanicLocation(b, v.Pos()) p.assertNilDerefBase(b, v.X) b.AssertNilDeref(x) return @@ -1028,6 +1834,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue // Zero-length slice-to-array conversions can leave only // an unused slice deref; preserve its required nil check. x := p.compileValue(b, v.X) + p.recordPanicLocation(b, v.Pos()) p.assertNilDerefBase(b, v.X) b.AssertNilDeref(x) return @@ -1056,8 +1863,19 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue return ret } } + if len(p.stackClears[v]) > 0 { + x := p.compileValue(b, v.X) + if ret, ok := b.LoadAndClearSinglePointer(x); ok { + p.loadClears[v] = true + p.bvals[iv] = ret + return ret + } + } } x := p.compileValue(b, v.X) + if v.Op != token.ARROW { + p.recordPanicLocation(b, v.Pos()) + } if shouldAssertDirectNilDeref(v) { b.AssertNilDeref(x) } @@ -1093,6 +1911,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue ret = b.Convert(p.type_(t, llssa.InGo), x) case *ssa.FieldAddr: x := p.compileValue(b, v.X) + p.recordPanicLocation(b, v.Pos()) if p.isAddressOfFieldAddr(v) { b.AssertNilDeref(x) } @@ -1114,10 +1933,12 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue } x := p.compileValue(b, vx) idx := p.compileValue(b, v.Index) + p.recordPanicLocation(b, v.Pos()) ret = b.IndexAddr(x, idx) case *ssa.Index: x := p.compileValue(b, v.X) idx := p.compileValue(b, v.Index) + p.recordPanicLocation(b, v.Pos()) ret = b.Index(x, idx, func() (addr llssa.Expr, zero bool) { switch n := v.X.(type) { case *ssa.Const: @@ -1151,6 +1972,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue if v.Max != nil { max = p.compileValue(b, v.Max) } + p.recordPanicLocation(b, v.Pos()) ret = b.Slice(x, low, high, max) ret.Type = p.type_(v.Type(), llssa.InGo) case *ssa.MakeInterface: @@ -1207,6 +2029,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue case *ssa.TypeAssert: x := p.compileValue(b, v.X) t := p.type_(v.AssertedType, llssa.InGo) + p.recordPanicLocation(b, v.Pos()) ret = b.TypeAssert(x, t, v.CommaOk) case *ssa.Extract: x := p.compileValue(b, v.Tuple) @@ -1247,6 +2070,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue case *ssa.SliceToArrayPointer: t := p.type_(v.Type(), llssa.InGo) x := p.compileValue(b, v.X) + p.recordPanicLocation(b, v.Pos()) ret = b.SliceToArrayPointer(x, t) default: panic(fmt.Sprintf("compileInstrAndValue: unknown instr - %T\n", iv)) @@ -1381,8 +2205,12 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { } } if p.returnNeedsImplicitRunDefers(v) { + p.recordPanicLocation(b, v.Pos()) b.RunDefers() } + if p.shouldTrackCallerFrames() { + p.popCallerLocationFrame(b) + } b.Return(results...) case *ssa.If: fn := p.fn @@ -1395,6 +2223,7 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { m := p.compileValue(b, v.Map) key := p.compileValue(b, v.Key) val := p.compileValue(b, v.Value) + p.recordPanicLocation(b, v.Pos()) b.MapUpdate(m, key, val) case *ssa.Defer: if v.DeferStack != nil { @@ -1405,13 +2234,16 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { case *ssa.Go: p.call(b, llssa.Go, &v.Call) case *ssa.RunDefers: + p.recordPanicLocation(b, v.Pos()) b.RunDefers() case *ssa.Panic: arg := p.compileValue(b, v.X) + p.recordPanicLocation(b, v.Pos()) b.Panic(arg) case *ssa.Send: ch := p.compileValue(b, v.Chan) x := p.compileValue(b, v.X) + p.recordPanicLocation(b, v.Pos()) b.Send(ch, x) case *ssa.DebugRef: if enableDbgSyms && v.Parent().Origin() == nil { @@ -1727,6 +2559,9 @@ func newPackageEx(prog llssa.Program, patches Patches, rewrites map[string]strin }, cgoSymbols: make([]string, 0, 128), rewrites: rewrites, + + trackCallerFrames: filesUseRuntimeCaller(files) || packageUsesRuntimeCaller(pkg), + runtimeCallerFuncs: runtimeCallerFuncSet(pkg), } if embedMap != nil { ctx.embedMap = *embedMap diff --git a/cl/funcinfo_metadata_test.go b/cl/funcinfo_metadata_test.go new file mode 100644 index 0000000000..5af5800613 --- /dev/null +++ b/cl/funcinfo_metadata_test.go @@ -0,0 +1,169 @@ +//go:build !llgo +// +build !llgo + +/* + * Copyright (c) 2024 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 cl_test + +import ( + "regexp" + "strconv" + "strings" + "testing" + + "github.com/goplus/llgo/cl/cltest" + llssa "github.com/goplus/llgo/ssa" +) + +type funcInfoRecord struct { + symbol string + name string + file string + line int + column int +} + +func TestFuncInfoMetadataEmission(t *testing.T) { + const src = `package foo + +type T struct{} + +func top() { + _ = func() int { return leaf() }() +} + +func leaf() int { return 1 } + +func (T) method() {} +` + ir := cltest.CompileIREx(t, src, "foo.go", false, func(prog llssa.Program) { + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + }) + + for _, want := range []string{ + `!llgo.funcinfo = !{!`, + `!"foo.top"`, + `!"foo.top$1"`, + `!"foo.T.method"`, + `!"foo.go"`, + } { + if !strings.Contains(ir, want) { + t.Fatalf("missing funcinfo metadata %s:\n%s", want, ir) + } + } + if strings.Contains(ir, "llvm.compiler.used") { + t.Fatalf("funcinfo metadata should not add llvm.compiler.used:\n%s", ir) + } + if strings.Contains(ir, `ptr @"foo.top"`) || strings.Contains(ir, `ptr @foo.top`) { + t.Fatalf("funcinfo metadata should use symbol strings, not function pointers:\n%s", ir) + } + + records := parseFuncInfoRecords(t, ir) + stackSymbols := []string{"foo.leaf", "foo.top$1", "foo.top"} + for _, symbol := range stackSymbols { + record, ok := records[symbol] + if !ok { + t.Fatalf("stack symbol %q not found in funcinfo metadata: %#v", symbol, records) + } + if record.name == "" || record.file != "foo.go" || record.line <= 0 || record.column <= 0 { + t.Fatalf("bad funcinfo for stack symbol %q: %#v", symbol, record) + } + } + if got := records["foo.leaf"].name; got != "foo.leaf" { + t.Fatalf("leaf stack frame name = %q, want foo.leaf", got) + } + if got := records["foo.top$1"].name; got != "foo.top$1" { + t.Fatalf("closure stack frame name = %q, want foo.top$1", got) + } + if got := records["foo.top"].name; got != "foo.top" { + t.Fatalf("caller stack frame name = %q, want foo.top", got) + } + if got := records["foo.top"].line; got != 6 { + t.Fatalf("top funcinfo line = %d, want first body statement line 6", got) + } + if got := records["foo.leaf"].line; got != 9 { + t.Fatalf("leaf funcinfo line = %d, want line 9", got) + } + if got := records["foo.T.method"].line; got != 11 { + t.Fatalf("empty method funcinfo line = %d, want declaration line 11", got) + } +} + +func TestNoInlineDirectiveDisablesTailCalls(t *testing.T) { + const src = `package foo + +func caller() { callee() } + +//go:noinline +func callee() {} +` + ir := cltest.CompileIREx(t, src, "foo.go", false, nil) + if !strings.Contains(ir, `define void @foo.callee()`) { + t.Fatalf("missing callee function:\n%s", ir) + } + if !strings.Contains(ir, `noinline`) || !strings.Contains(ir, `"disable-tail-calls"="true"`) { + t.Fatalf("callee should disable inlining and tail calls:\n%s", ir) + } +} + +func parseFuncInfoRecords(t *testing.T, ir string) map[string]funcInfoRecord { + t.Helper() + + listRE := regexp.MustCompile(`!llgo\.funcinfo = !\{([^}]*)\}`) + listMatch := listRE.FindStringSubmatch(ir) + if listMatch == nil { + t.Fatalf("missing funcinfo metadata list:\n%s", ir) + } + refRE := regexp.MustCompile(`!(\d+)`) + refs := refRE.FindAllStringSubmatch(listMatch[1], -1) + if len(refs) == 0 { + t.Fatalf("empty funcinfo metadata list:\n%s", ir) + } + wantRefs := make(map[string]bool, len(refs)) + for _, ref := range refs { + wantRefs[ref[1]] = true + } + + rowRE := regexp.MustCompile(`^!(\d+) = !\{i32 1, !"([^"]+)", !"([^"]+)", !"([^"]*)", i32 ([0-9]+), i32 ([0-9]+)\}$`) + records := make(map[string]funcInfoRecord) + for _, line := range strings.Split(ir, "\n") { + row := rowRE.FindStringSubmatch(line) + if row == nil || !wantRefs[row[1]] { + continue + } + lineNo, err := strconv.Atoi(row[5]) + if err != nil { + t.Fatalf("bad funcinfo line in %q: %v", line, err) + } + column, err := strconv.Atoi(row[6]) + if err != nil { + t.Fatalf("bad funcinfo column in %q: %v", line, err) + } + records[row[2]] = funcInfoRecord{ + symbol: row[2], + name: row[3], + file: row[4], + line: lineNo, + column: column, + } + } + if len(records) != len(wantRefs) { + t.Fatalf("parsed %d funcinfo records, want %d:\n%s", len(records), len(wantRefs), ir) + } + return records +} diff --git a/cl/instr.go b/cl/instr.go index b7fc52abd3..389d369bc2 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -853,6 +853,745 @@ func (p *context) sourceLine(filename string, line int) (string, bool) { return lines[line-1], true } +func (p *context) shouldTrackCallerFrames() bool { + if p == nil || p.pkg == nil || p.fn == nil || p.goFn == nil || !p.trackCallerFrames { + return false + } + if !p.runtimeCallerFuncs[p.goFn] { + return false + } + if target := p.prog.Target(); target != nil && (target.Target != "" || target.GOARCH == "wasm") { + return false + } + return canTrackCallerFramesForPackage(p.pkg.Path()) +} + +func canTrackCallerFramesForPackage(pkgPath string) bool { + return pkgPath != llssa.PkgRuntime && + pkgPath != "runtime" && + !isStandardLibraryPackage(pkgPath) && + !strings.HasPrefix(pkgPath, "github.com/goplus/llgo/runtime/internal/") +} + +func isStandardLibraryPackage(pkgPath string) bool { + return pkgPath != "command-line-arguments" && !strings.Contains(pkgPath, ".") +} + +func packageUsesRuntimeCaller(pkg *ssa.Package) bool { + return len(runtimeCallerFuncSet(pkg)) != 0 +} + +func fnUsesRuntimeCaller(fn *ssa.Function) bool { + if fn == nil { + return false + } + if fn.Pkg == nil { + return fnHasDirectRuntimeCaller(fn) + } + return runtimeCallerFuncSet(fn.Pkg)[fn] +} + +func runtimeCallerFuncSet(pkg *ssa.Package) map[*ssa.Function]bool { + if pkg == nil { + return nil + } + funcs, trackable := collectRuntimeCallerFunctions(pkg) + analysis := &runtimeCallerAnalysis{ + pkg: pkg, + funcs: funcs, + trackable: trackable, + callsites: collectRuntimeCallerCallsites(funcs), + memo: make(map[*ssa.Function]bool), + visiting: make(map[*ssa.Function]bool), + } + if !analysis.packageHasRuntimeCaller() { + return nil + } + out := make(map[*ssa.Function]bool) + for { + ntrack := len(trackable) + for fn := range trackable { + if analysis.fnMayReachRuntimeCaller(fn) { + out[fn] = true + } + } + if len(trackable) == ntrack { + break + } + } + if len(out) == 0 { + return nil + } + return out +} + +type runtimeCallerAnalysis struct { + pkg *ssa.Package + funcs map[*ssa.Function]bool + trackable map[*ssa.Function]bool + callsites map[*ssa.Function][]*ssa.CallCommon + memo map[*ssa.Function]bool + visiting map[*ssa.Function]bool +} + +func collectRuntimeCallerFunctions(pkg *ssa.Package) (funcs, trackable map[*ssa.Function]bool) { + funcs = make(map[*ssa.Function]bool) + trackable = make(map[*ssa.Function]bool) + var add func(*ssa.Function, bool) bool + add = func(fn *ssa.Function, track bool) bool { + if fn == nil || !functionBelongsToPackage(pkg, fn) { + return false + } + if track { + trackable[fn] = true + } + if funcs[fn] { + return false + } + funcs[fn] = true + for _, anon := range fn.AnonFuncs { + add(anon, false) + } + return true + } + for _, member := range pkg.Members { + if fn, ok := member.(*ssa.Function); ok { + add(fn, true) + } + } + if pkg.Prog != nil && pkg.Pkg != nil { + for _, typ := range pkg.Prog.RuntimeTypes() { + if !typeBelongsToPackage(typ, pkg.Pkg) { + continue + } + methods := pkg.Prog.MethodSets.MethodSet(typ) + for i := 0; i < methods.Len(); i++ { + add(pkg.Prog.MethodValue(methods.At(i)), false) + } + } + } + for changed := true; changed; { + changed = false + for fn := range funcs { + forEachCall(fn, func(call *ssa.CallCommon) { + if add(call.StaticCallee(), trackable[fn]) { + changed = true + } + }) + } + } + return funcs, trackable +} + +func collectRuntimeCallerCallsites(funcs map[*ssa.Function]bool) map[*ssa.Function][]*ssa.CallCommon { + callsites := make(map[*ssa.Function][]*ssa.CallCommon) + for fn := range funcs { + forEachCall(fn, func(call *ssa.CallCommon) { + callee := call.StaticCallee() + if funcs[callee] { + callsites[callee] = append(callsites[callee], call) + } + }) + } + return callsites +} + +func functionBelongsToPackage(pkg *ssa.Package, fn *ssa.Function) bool { + if pkg == nil || fn == nil { + return false + } + if fn.Pkg == pkg { + return true + } + return fn.Pkg == nil && fn.Parent() != nil && functionBelongsToPackage(pkg, fn.Parent()) +} + +func typeBelongsToPackage(typ types.Type, pkg *types.Package) bool { + if pkg == nil { + return false + } + for { + if ptr, ok := types.Unalias(typ).(*types.Pointer); ok { + typ = ptr.Elem() + continue + } + break + } + named, ok := types.Unalias(typ).(*types.Named) + return ok && named.Obj() != nil && named.Obj().Pkg() == pkg +} + +func (a *runtimeCallerAnalysis) packageHasRuntimeCaller() bool { + for fn := range a.funcs { + if fnHasDirectRuntimeCaller(fn) { + return true + } + } + return false +} + +func fnHasDirectRuntimeCaller(fn *ssa.Function) bool { + if fn == nil { + return false + } + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + call, ok := instr.(ssa.CallInstruction) + if !ok { + continue + } + if isRuntimeCallerFrameFunc(call.Common().StaticCallee()) { + return true + } + } + } + for _, anon := range fn.AnonFuncs { + if fnHasDirectRuntimeCaller(anon) { + return true + } + } + return false +} + +func (a *runtimeCallerAnalysis) fnMayReachRuntimeCaller(fn *ssa.Function) bool { + if fn == nil { + return false + } + if isRuntimeCallerFrameFunc(fn) { + return true + } + if !a.funcs[fn] { + return false + } + if ok, done := a.memo[fn]; done { + return ok + } + if a.visiting[fn] { + return false + } + a.visiting[fn] = true + defer delete(a.visiting, fn) + reaches := false + forEachCall(fn, func(call *ssa.CallCommon) { + if reaches { + return + } + callee := call.StaticCallee() + switch { + case isRuntimeCallerFrameFunc(callee): + reaches = true + case callee != nil: + reaches = a.fnMayReachRuntimeCaller(callee) + case call.Method != nil: + reaches = a.interfaceInvokeMayReachRuntimeCaller(fn, call) + default: + reaches = a.functionValueCallMayReachRuntimeCaller(fn, call.Value) + } + }) + if !reaches { + for _, anon := range fn.AnonFuncs { + if a.fnMayReachRuntimeCaller(anon) { + if a.trackable[fn] { + a.trackable[anon] = true + } + reaches = true + break + } + } + } + a.memo[fn] = reaches + return reaches +} + +func (a *runtimeCallerAnalysis) functionValueCallMayReachRuntimeCaller(fn *ssa.Function, value ssa.Value) bool { + targets, ok := a.functionValueTargets(fn, value) + if !ok { + return true + } + for target := range targets { + if a.fnMayReachRuntimeCaller(target) { + return true + } + } + return false +} + +func (a *runtimeCallerAnalysis) functionValueTargets(fn *ssa.Function, value ssa.Value) (map[*ssa.Function]bool, bool) { + if targets, ok := staticFunctionTargets(value); ok { + return targets, true + } + param, ok := value.(*ssa.Parameter) + if !ok || param.Parent() != fn { + return nil, false + } + idx, ok := parameterIndex(fn, param) + if !ok { + return nil, false + } + return a.functionParamTargets(fn, idx) +} + +func (a *runtimeCallerAnalysis) functionParamTargets(fn *ssa.Function, idx int) (map[*ssa.Function]bool, bool) { + callsites := a.callsites[fn] + if len(callsites) == 0 { + return nil, false + } + targets := make(map[*ssa.Function]bool) + for _, call := range callsites { + args := call.Args + if idx >= len(args) { + return nil, false + } + argTargets, ok := staticFunctionTargets(args[idx]) + if !ok { + return nil, false + } + for target := range argTargets { + targets[target] = true + } + } + return targets, true +} + +func staticFunctionTargets(value ssa.Value) (map[*ssa.Function]bool, bool) { + switch v := value.(type) { + case *ssa.Function: + return map[*ssa.Function]bool{v: true}, true + case *ssa.MakeClosure: + if fn, ok := v.Fn.(*ssa.Function); ok { + return map[*ssa.Function]bool{fn: true}, true + } + } + return nil, false +} + +func (a *runtimeCallerAnalysis) interfaceInvokeMayReachRuntimeCaller(fn *ssa.Function, call *ssa.CallCommon) bool { + targets, ok := a.interfaceMethodTargets(fn, call.Value, call.Method) + if !ok { + return true + } + for target := range targets { + if a.fnMayReachRuntimeCaller(target) { + return true + } + } + return false +} + +func (a *runtimeCallerAnalysis) interfaceMethodTargets(fn *ssa.Function, value ssa.Value, method *types.Func) (map[*ssa.Function]bool, bool) { + if targets, ok := a.staticInterfaceMethodTargets(value, method); ok { + return targets, true + } + param, ok := value.(*ssa.Parameter) + if !ok || param.Parent() != fn { + return nil, false + } + idx, ok := parameterIndex(fn, param) + if !ok { + return nil, false + } + callsites := a.callsites[fn] + if len(callsites) == 0 { + return nil, false + } + targets := make(map[*ssa.Function]bool) + for _, call := range callsites { + args := call.Args + if idx >= len(args) { + return nil, false + } + argTargets, ok := a.staticInterfaceMethodTargets(args[idx], method) + if !ok { + return nil, false + } + for target := range argTargets { + targets[target] = true + } + } + return targets, true +} + +func (a *runtimeCallerAnalysis) staticInterfaceMethodTargets(value ssa.Value, method *types.Func) (map[*ssa.Function]bool, bool) { + switch v := value.(type) { + case *ssa.MakeInterface: + return a.methodTargetsForType(v.X.Type(), method) + case *ssa.ChangeInterface: + return a.staticInterfaceMethodTargets(v.X, method) + } + return nil, false +} + +func (a *runtimeCallerAnalysis) methodTargetsForType(typ types.Type, method *types.Func) (map[*ssa.Function]bool, bool) { + if a.pkg == nil || a.pkg.Prog == nil || method == nil { + return nil, false + } + methods := a.pkg.Prog.MethodSets.MethodSet(typ) + for i := 0; i < methods.Len(); i++ { + sel := methods.At(i) + if sel.Obj().Name() != method.Name() { + continue + } + fn := a.pkg.Prog.MethodValue(sel) + if fn == nil { + return nil, false + } + return map[*ssa.Function]bool{fn: true}, true + } + return nil, false +} + +func parameterIndex(fn *ssa.Function, param *ssa.Parameter) (int, bool) { + for i, candidate := range fn.Params { + if candidate == param { + return i, true + } + } + return 0, false +} + +func forEachCall(fn *ssa.Function, do func(*ssa.CallCommon)) { + if fn == nil { + return + } + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + if call, ok := instr.(ssa.CallInstruction); ok { + do(call.Common()) + } + } + } +} + +func isRuntimeCallerFunc(fn *ssa.Function) bool { + if fn == nil || fn.Pkg == nil || fn.Pkg.Pkg == nil { + return false + } + switch fn.Pkg.Pkg.Path() { + case "runtime", "github.com/goplus/llgo/runtime/internal/lib/runtime": + return isRuntimeCallerName(fn.Name()) + case "runtime/debug": + return fn.Name() == "Stack" + default: + return false + } +} + +func isRuntimeCallerFrameFunc(fn *ssa.Function) bool { + if fn == nil || fn.Pkg == nil || fn.Pkg.Pkg == nil { + return false + } + switch fn.Pkg.Pkg.Path() { + case "runtime", "github.com/goplus/llgo/runtime/internal/lib/runtime": + return isRuntimeCallerFrameName(fn.Name()) + case "runtime/debug": + return fn.Name() == "Stack" + default: + return false + } +} + +func isRuntimeCallerLookupFunc(fn *ssa.Function) bool { + if fn == nil || fn.Pkg == nil || fn.Pkg.Pkg == nil { + return false + } + switch fn.Pkg.Pkg.Path() { + case "runtime", "github.com/goplus/llgo/runtime/internal/lib/runtime": + switch fn.Name() { + case "Caller", "Callers", "Stack": + return true + } + case "runtime/debug": + return fn.Name() == "Stack" + } + return false +} + +func isRuntimeCallerName(name string) bool { + switch name { + case "Caller", "Callers", "CallersFrames", "FuncForPC", "Stack": + return true + default: + return false + } +} + +func isRuntimeCallerFrameName(name string) bool { + switch name { + case "Caller", "Callers", "CallersFrames", "Stack": + return true + default: + return false + } +} + +func (p *context) runtimeCallerFrameName() string { + if p == nil { + return "" + } + if p.goFn != nil && p.goFn.Pkg != nil && p.goFn.Pkg.Pkg != nil { + return runtimeFrameName(funcName(p.goFn.Pkg.Pkg, p.goFn, false)) + } + if p.fn != nil { + return runtimeFrameName(p.fn.Name()) + } + return "" +} + +func (p *context) pushCallerLocationFrame(b llssa.Builder, fn *ssa.Function) { + if fn == nil { + return + } + pos := p.fset.Position(fn.Pos()) + entry := b.Convert(p.prog.Uintptr(), p.fn.Expr) + p.callerFrameMark = b.Call( + p.runtimeFunc("PushCallerLocationFrame", pushCallerLocationFrameSig()), + entry, + b.Str(p.runtimeCallerFrameName()), + b.Str(pos.Filename), + p.prog.IntVal(uint64(pos.Line), p.prog.Int()), + ) +} + +func (p *context) recordCallerLocation(b llssa.Builder, pos token.Pos) { + p.recordRuntimeLocation(b, pos, "RecordCallerLocation") +} + +func (p *context) recordPanicLocation(b llssa.Builder, pos token.Pos) { + p.recordRuntimeLocation(b, pos, "RecordPanicLocation") +} + +func (p *context) recordRuntimeLocation(b llssa.Builder, pos token.Pos, fn string) { + if !p.shouldTrackCallerFrames() { + return + } + position := p.fset.Position(pos) + if position.Line <= 0 || position.Filename == "" { + return + } + b.Call( + p.runtimeFunc(fn, recordRuntimeLocationSig()), + b.Convert(p.prog.Uintptr(), p.fn.Expr), + b.Str(p.runtimeCallerFrameName()), + b.Str(position.Filename), + p.prog.IntVal(uint64(position.Line), p.prog.Int()), + ) +} + +func (p *context) recordCallerLocationForCall(b llssa.Builder, call *ssa.CallCommon) { + if !p.shouldTrackCallerFrames() { + return + } + callee := call.StaticCallee() + if isRuntimeCallerLookupFunc(callee) { + p.recordCallerLocation(b, call.Pos()) + return + } + p.recordPanicLocation(b, call.Pos()) +} + +func (p *context) emitPCLineLabel(b llssa.Builder, pos token.Pos) { + if p == nil || p.pkg == nil || p.fn == nil || !p.prog.FuncInfoSitesEnabled() || !p.shouldTrackCallerFrames() { + return + } + target := p.prog.Target() + if !canEmitPCLineLabelsForTarget(target) { + return + } + position := p.fset.Position(pos) + if position.Line <= 0 || position.Filename == "" { + return + } + p.pcLineSeq++ + id := pcLineID(p.fn.Name(), p.pcLineSeq) + label := pcLineLabelName(id) + if target.GOOS == "darwin" { + // Mach-O subsections-via-symbols treats every non-local symbol as an + // atom boundary; a visible label in the middle of a function body + // lets the linker split and reorder the function. The "L" prefix + // keeps the label assembler-local so the function stays one atom. + label = "L" + label + } + asmLabel := label + "_${:uid}" + ptrDirective := ".quad" + align := "3" + if p.prog.PointerSize() == 4 { + ptrDirective = ".long" + align = "2" + } + // Keep section names in sync with internal/build/funcinfo_table.go + // (pcLineSiteSectionInfo). ELF ties the record to the function via + // SHF_LINK_ORDER (honored by --gc-sections); Mach-O uses a live_support + // section plus one linker-private atom symbol per record so -dead_strip + // keeps a record exactly when the function containing its label is live. + pushSection := ".pushsection llgo_pcline,\"ao\",@progbits," + asmQuoteSymbol(p.fn.Name()) + recordSymbol := "" + if target.GOOS == "darwin" { + pushSection = ".pushsection __DATA,__llgo_pcl,regular,live_support" + recordSymbol = "l_llgo_pcline_rec_${:uid}:\n" + } + b.InlineAsm( + asmLabel + ":\n" + + pushSection + "\n" + + ".p2align " + align + "\n" + + recordSymbol + + ptrDirective + " " + asmLabel + "\n" + + ".quad " + uint64Hex(id) + "\n" + + ".popsection", + ) + p.pkg.EmitPCLineInfo(id, p.fn.Name(), position.Filename, position.Line, position.Column) +} + +func canEmitPCLineLabelsForTarget(target *llssa.Target) bool { + if target == nil { + return false + } + if target.Target != "" || target.GOARCH == "wasm" { + return false + } + // ELF uses SHF_LINK_ORDER associated sections; Mach-O uses plain + // __DATA,__llgo_pcl sections (safe because LLGo's global DCE runs at the + // IR level). Other object formats need separate support. + switch target.GOOS { + case "linux", "darwin": + return true + } + return false +} + +func pcLineID(symbol string, seq uint64) uint64 { + const ( + offset = uint64(14695981039346656037) + prime = uint64(1099511628211) + ) + h := offset + for i := 0; i < len(symbol); i++ { + h ^= uint64(symbol[i]) + h *= prime + } + for i := 0; i < 8; i++ { + h ^= byteOfUint64(seq, uint(i*8)) + h *= prime + } + if h == 0 { + return 1 + } + return h +} + +func byteOfUint64(v uint64, shift uint) uint64 { + return (v >> shift) & 0xff +} + +func pcLineLabelName(id uint64) string { + const hexdigits = "0123456789abcdef" + var buf [16]byte + for i := len(buf) - 1; i >= 0; i-- { + buf[i] = hexdigits[id&0xf] + id >>= 4 + } + return "__llgo_pcsite_" + string(buf[:]) +} + +func uint64Hex(v uint64) string { + const hexdigits = "0123456789abcdef" + var buf [18]byte + buf[0] = '0' + buf[1] = 'x' + for i := len(buf) - 1; i >= 2; i-- { + buf[i] = hexdigits[v&0xf] + v >>= 4 + } + return string(buf[:]) +} + +func asmQuoteSymbol(symbol string) string { + var b strings.Builder + b.Grow(len(symbol) + 2) + b.WriteByte('"') + for i := 0; i < len(symbol); i++ { + switch symbol[i] { + case '\\', '"': + b.WriteByte('\\') + case '$': + b.WriteByte('$') + } + b.WriteByte(symbol[i]) + } + b.WriteByte('"') + return b.String() +} + +func (p *context) popCallerLocationFrame(b llssa.Builder) { + if p.callerFrameMark.IsNil() { + return + } + b.Call(p.runtimeFunc("PopCallerLocationFrame", popCallerLocationFrameSig()), p.callerFrameMark) +} + +func (p *context) runtimeFunc(name string, sig *types.Signature) llssa.Expr { + p.pkg.NeedRuntime = true + fullName := llssa.PkgRuntime + "." + name + if fn := p.pkg.FuncOf(fullName); fn != nil { + return fn.Expr + } + return p.pkg.NewFuncEx(fullName, sig, llssa.InGo, false, false).Expr +} + +func pushCallerLocationFrameSig() *types.Signature { + return types.NewSignatureType(nil, nil, nil, + types.NewTuple( + types.NewVar(token.NoPos, nil, "entry", types.Typ[types.Uintptr]), + types.NewVar(token.NoPos, nil, "name", types.Typ[types.String]), + types.NewVar(token.NoPos, nil, "file", types.Typ[types.String]), + types.NewVar(token.NoPos, nil, "startLine", types.Typ[types.Int]), + ), + types.NewTuple(types.NewVar(token.NoPos, nil, "", types.Typ[types.Int])), + false, + ) +} + +func recordRuntimeLocationSig() *types.Signature { + return types.NewSignatureType(nil, nil, nil, + types.NewTuple( + types.NewVar(token.NoPos, nil, "entry", types.Typ[types.Uintptr]), + types.NewVar(token.NoPos, nil, "name", types.Typ[types.String]), + types.NewVar(token.NoPos, nil, "file", types.Typ[types.String]), + types.NewVar(token.NoPos, nil, "line", types.Typ[types.Int]), + ), + nil, + false, + ) +} + +func popCallerLocationFrameSig() *types.Signature { + return types.NewSignatureType(nil, nil, nil, + types.NewTuple(types.NewVar(token.NoPos, nil, "mark", types.Typ[types.Int])), + nil, + false, + ) +} + +func runtimeFrameName(name string) string { + const commandLineArguments = "command-line-arguments." + if strings.HasPrefix(name, commandLineArguments) { + name = "main." + name[len(commandLineArguments):] + } + return normalizeRuntimeAnonFuncName(name) +} + +func normalizeRuntimeAnonFuncName(name string) string { + dollar := strings.LastIndexByte(name, '$') + if dollar < 0 || dollar == len(name)-1 { + return name + } + for i := dollar + 1; i < len(name); i++ { + if name[i] < '0' || name[i] > '9' { + return name + } + } + return name[:dollar] + ".func" + name[dollar+1:] +} + // ----------------------------------------------------------------------------- type explicitDeferStack struct { @@ -1049,6 +1788,8 @@ func collectMethodNilDerefChecks(fn *ssa.Function) map[*ssa.UnOp]none { } func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallCommon, ds *explicitDeferStack) (ret llssa.Expr) { + p.recordCallerLocationForCall(b, call) + p.emitPCLineLabel(b, call.Pos()) cv := call.Value if mthd := call.Method; mthd != nil { reflectCheck := p.reflectTypeMethodCheck(call, mthd) @@ -1093,6 +1834,12 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm ret = p.emitDo(b, act, ds, llssa.Builtin(fn), llssa.Builder.Call, args...) case *ssa.Function: aFn, pyFn, ftype := p.compileFunction(cv) + if p.isRuntimeSetFinalizerCall(call) && len(args) == 2 && act == llssa.Call && ds == nil { + finalizer := p.compileLateValue(b, args[1]) + obj := p.compileLateValue(b, args[0]) + ret = p.emitDo(b, act, nil, aFn.Expr, llssa.Builder.Call, obj, finalizer) + return + } // TODO(xsw): check ca != llssa.Call switch ftype { case cFunc: diff --git a/cl/liveness_internal_test.go b/cl/liveness_internal_test.go new file mode 100644 index 0000000000..9e142e0d25 --- /dev/null +++ b/cl/liveness_internal_test.go @@ -0,0 +1,860 @@ +//go:build !llgo +// +build !llgo + +package cl + +import ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "strings" + "testing" + + "github.com/goplus/gogen/packages" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" +) + +func buildSSAPackageWithPath(t *testing.T, pkgPath, pkgName, src string) *ssa.Package { + t.Helper() + ssapkg, _ := buildSSAPackageWithPathAndFiles(t, pkgPath, pkgName, src) + return ssapkg +} + +func buildSSAPackageWithPathAndFiles(t *testing.T, pkgPath, pkgName, src string) (*ssa.Package, []*ast.File) { + t.Helper() + return buildSSAPackageWithPathAndFilesMode(t, pkgPath, pkgName, src, ssa.SanityCheckFunctions|ssa.InstantiateGenerics) +} + +func buildSSAPackageWithPathAndFilesMode(t *testing.T, pkgPath, pkgName, src string, mode ssa.BuilderMode) (*ssa.Package, []*ast.File) { + t.Helper() + + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "p.go", src, 0) + if err != nil { + t.Fatal(err) + } + files := []*ast.File{f} + pkg := types.NewPackage(pkgPath, pkgName) + imp := packages.NewImporter(fset) + ssapkg, _, err := ssautil.BuildPackage(&types.Config{Importer: imp}, fset, pkg, files, mode) + if err != nil { + t.Fatal(err) + } + return ssapkg, files +} + +func TestConservativeGCPointerTypeAnalysis(t *testing.T) { + if hasConservativeGCPointers(nil, map[types.Type]bool{}) { + t.Fatal("nil type should not report conservative pointers") + } + if hasConservativeGCPointers(types.Typ[types.Int], map[types.Type]bool{}) { + t.Fatal("int should not report conservative pointers") + } + if hasConservativeGCPointers(types.Typ[types.String], map[types.Type]bool{types.Typ[types.String]: true}) { + t.Fatal("seen type should short-circuit") + } + for _, typ := range []types.Type{ + types.Typ[types.String], + types.Typ[types.UnsafePointer], + types.NewPointer(types.Typ[types.Int]), + types.NewSlice(types.Typ[types.Int]), + types.NewMap(types.Typ[types.String], types.Typ[types.Int]), + types.NewChan(types.SendRecv, types.Typ[types.Int]), + types.NewSignatureType(nil, nil, nil, nil, nil, false), + types.NewInterfaceType(nil, nil), + types.NewArray(types.NewPointer(types.Typ[types.Int]), 2), + types.NewStruct([]*types.Var{types.NewField(token.NoPos, nil, "p", types.NewPointer(types.Typ[types.Int]), false)}, nil), + } { + if !hasConservativeGCPointers(typ, map[types.Type]bool{}) { + t.Fatalf("%v should report conservative pointers", typ) + } + } + if hasConservativeGCPointers(types.NewStruct([]*types.Var{ + types.NewField(token.NoPos, nil, "i", types.Typ[types.Int], false), + }, nil), map[types.Type]bool{}) { + t.Fatal("struct without pointer fields should not report conservative pointers") + } + if hasConservativeGCPointers(types.NewArray(types.Typ[types.Int], 2), map[types.Type]bool{}) { + t.Fatal("array without pointer elements should not report conservative pointers") + } + if !hasConservativeGCPointers(types.NewStruct([]*types.Var{ + types.NewField(token.NoPos, nil, "i", types.Typ[types.Int], false), + types.NewField(token.NoPos, nil, "p", types.NewPointer(types.Typ[types.Int]), false), + }, nil), map[types.Type]bool{}) { + t.Fatal("struct with later pointer field should report conservative pointers") + } +} + +func TestShouldClearAlloc(t *testing.T) { + ssapkg := buildSSAPackageWithPath(t, "example.com/live", "live", `package live + +type Box struct{ p *int } + +var Sink any + +func allocs(p *int) { + var box Box + var i int + box.p = p + Sink = &box + Sink = &i +} + `) + fn := ssapkg.Func("allocs") + ctx := &context{} + if ctx.shouldClearAlloc(nil) { + t.Fatal("nil alloc should not be cleared") + } + + var boxAlloc, intAlloc *ssa.Alloc + for _, local := range functionAllocs(fn) { + ptr := local.Type().Underlying().(*types.Pointer) + if _, ok := ptr.Elem().Underlying().(*types.Struct); ok { + boxAlloc = local + } + if ptr.Elem() == types.Typ[types.Int] { + intAlloc = local + } + } + if boxAlloc == nil || intAlloc == nil { + var dump strings.Builder + fn.WriteTo(&dump) + t.Fatalf("missing expected allocs: %v\n%s", functionAllocs(fn), dump.String()) + } + if !ctx.shouldClearAlloc(boxAlloc) { + t.Fatal("struct containing a pointer should be cleared") + } + if ctx.shouldClearAlloc(intAlloc) { + t.Fatal("int alloc should not be cleared") + } + + boxAlloc.Comment = "varargs" + if ctx.shouldClearAlloc(boxAlloc) { + t.Fatal("varargs alloc should not be cleared") + } + boxAlloc.Comment = "makeslice" + if ctx.shouldClearAlloc(boxAlloc) { + t.Fatal("synthetic makeslice alloc should not be cleared") + } +} + +func functionAllocs(fn *ssa.Function) []*ssa.Alloc { + seen := make(map[*ssa.Alloc]bool) + var allocs []*ssa.Alloc + add := func(alloc *ssa.Alloc) { + if alloc != nil && !seen[alloc] { + seen[alloc] = true + allocs = append(allocs, alloc) + } + } + for _, local := range fn.Locals { + add(local) + } + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + if alloc, ok := instr.(*ssa.Alloc); ok { + add(alloc) + } + } + } + return allocs +} + +func TestRuntimeSetFinalizerDetection(t *testing.T) { + ssapkg := buildSSAPackageWithPath(t, "github.com/goplus/llgo/runtime/livetest", "livetest", `package livetest + +import rt "github.com/goplus/llgo/runtime/internal/lib/runtime" + +func direct(p *int) { + rt.SetFinalizer(p, func(*int) {}) +} + +func deferred(p *int) { + defer rt.SetFinalizer(p, nil) +} + +func goroutine(p *int) { + go rt.SetFinalizer(p, nil) +} + +func nested(p *int) { + func() { + rt.SetFinalizer(p, nil) + }() +} + +func none(p *int) {} +`) + ctx := &context{} + if ctx.enableConservativeLivenessClears(nil) { + t.Fatal("nil function should not enable conservative clears") + } + for _, name := range []string{"direct", "deferred", "goroutine", "nested"} { + if !ctx.functionUsesRuntimeSetFinalizer(ssapkg.Func(name), map[*ssa.Function]bool{}) { + t.Fatalf("%s should be detected as SetFinalizer user", name) + } + } + if ctx.functionUsesRuntimeSetFinalizer(nil, map[*ssa.Function]bool{}) { + t.Fatal("nil function should not use SetFinalizer") + } + direct := ssapkg.Func("direct") + if ctx.functionUsesRuntimeSetFinalizer(direct, map[*ssa.Function]bool{direct: true}) { + t.Fatal("seen function should short-circuit") + } + if ctx.functionUsesRuntimeSetFinalizer(ssapkg.Func("none"), map[*ssa.Function]bool{}) { + t.Fatal("none should not use SetFinalizer") + } + if ctx.packageUsesRuntimeSetFinalizer(&ssa.Package{Members: map[string]ssa.Member{"none": ssapkg.Func("none")}}) { + t.Fatal("package without SetFinalizer should not report use") + } + if !ctx.packageUsesRuntimeSetFinalizer(ssapkg) { + t.Fatal("package should report SetFinalizer use") + } + if ctx.enableConservativeLivenessClears(direct) { + t.Fatal("non command-line-arguments package should not enable conservative clears") + } + ssapkg.Pkg = types.NewPackage("command-line-arguments", "main") + if !ctx.enableConservativeLivenessClears(direct) { + t.Fatal("command-line-arguments package with SetFinalizer should enable conservative clears") + } +} + +func TestRuntimeSetFinalizerLateValueSkips(t *testing.T) { + ssapkg := buildSSAPackageWithPath(t, "github.com/goplus/llgo/runtime/livetest", "livetest", `package livetest + +import rt "github.com/goplus/llgo/runtime/internal/lib/runtime" + +func direct(p *int) { + rt.SetFinalizer(p, func(*int) {}) +} +`) + ctx := &context{} + fn := ssapkg.Func("direct") + var makeIface *ssa.MakeInterface + var deref *ssa.UnOp + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + switch instr := instr.(type) { + case *ssa.MakeInterface: + makeIface = instr + case *ssa.UnOp: + if instr.Op == token.MUL { + deref = instr + } + } + } + } + if makeIface == nil { + t.Fatal("missing MakeInterface for SetFinalizer argument") + } + if ctx.isRuntimeSetFinalizerCall(nil) { + t.Fatal("nil call should not be SetFinalizer") + } + if !ctx.shouldSkipLateSetFinalizerValue(makeIface) { + t.Fatal("SetFinalizer-only MakeInterface should be skipped") + } + if deref != nil && !ctx.shouldSkipLateSetFinalizerValue(deref) { + t.Fatal("SetFinalizer-only deref should be skipped") + } + if ctx.shouldSkipLateSetFinalizerValue(&ssa.Return{}) { + t.Fatal("unrelated instruction should not be skipped") + } + if ctx.shouldSkipLateSetFinalizerValue(&ssa.UnOp{Op: token.SUB}) { + t.Fatal("non-deref unary op should not be skipped") + } +} + +func TestConservativeLivenessPlanCollectors(t *testing.T) { + ssapkg := buildSSAPackageWithPath(t, "example.com/live", "live", `package live + +type Box struct{ p *int } + +var Sink any + +func linear(p *int) { + var box Box + box.p = p + Sink = box.p + Sink = 1 +} + +func branch(p *int, cond bool) { + var box Box + box.p = p + if cond { + Sink = box.p + } else { + Sink = 0 + } + Sink = 1 +} + +func branchBoth(p *int, cond bool) { + var box Box + box.p = p + if cond { + Sink = box.p + } else { + Sink = box.p + } + Sink = 1 +} + +func paramUse(p *int) { + Sink = p + Sink = 1 +} + +func splitParam(p *int, cond bool) { + if cond { + Sink = p + } else { + Sink = p + } + Sink = 1 +} + +func takes(*int) {} + +func callWithPointer(p *int) { + takes(p) + Sink = 1 +} + +func callWithInt(i int) { + Sink = i +} +`) + ctx := &context{} + linear := ssapkg.Func("linear") + stackPlans := ctx.collectStackClearPlans(linear) + if len(stackPlans) == 0 { + t.Fatal("linear should produce stack clear plans") + } + for instr := range stackPlans { + if isTerminatingInstruction(instr) { + t.Fatalf("stack clear should not be scheduled after terminator %T", instr) + } + } + + entryPlans := ctx.collectEntryClearPlans(ssapkg.Func("branch")) + if len(entryPlans) == 0 { + t.Fatal("branch should produce entry clear plans for dead successor") + } + if got := ctx.collectEntryClearPlans(ssapkg.Func("branchBoth")); len(got) != 0 { + t.Fatalf("branchBoth should not clear values live in both successors: %v", got) + } + + paramFn := ssapkg.Func("paramUse") + if len(ctx.collectParamClobberPlans(paramFn)) == 0 { + t.Fatal("paramUse should produce param clobber plans") + } + if len(ctx.collectParamScanPlans(paramFn)) == 0 { + t.Fatal("paramUse should produce param scan plans") + } + splitParam := ssapkg.Func("splitParam") + if got := ctx.collectParamClobberPlans(splitParam); len(got) != 0 { + t.Fatalf("splitParam has no single last-use block, got clobbers: %v", got) + } + if got := ctx.collectParamScanPlans(splitParam); len(got) != 0 { + t.Fatalf("splitParam has no single last-use block, got scans: %v", got) + } + if len(ctx.collectCallClobberPlans(ssapkg.Func("callWithPointer"))) == 0 { + t.Fatal("pointer call should clobber pointer regs") + } + if len(ctx.collectCallClobberPlans(ssapkg.Func("callWithInt"))) != 0 { + t.Fatal("int-only call should not clobber pointer regs") + } +} + +func TestConservativeLivenessGraphHelpers(t *testing.T) { + ssapkg := buildSSAPackageWithPath(t, "example.com/live", "live", `package live + +import "unsafe" + +var Sink any + +type Box struct{ p *int } + +func flow(p *int, cond bool) { + if cond { + Sink = p + } else { + Sink = 0 + } +} + +func split(p *int, cond bool) { + if cond { + Sink = p + } else { + Sink = p + } +} + +func target(*int) {} + +func withCall(p *int) { + target(p) +} + +func refs(p *int, arr *[2]*int, box *Box, cond bool) *int { + var q *int + if cond { + q = p + } else { + q = box.p + } + Sink = arr[0] + Sink = q + return q +} + +func converted(p *int) unsafe.Pointer { + return unsafe.Pointer(p) +} + `) + fn := ssapkg.Func("flow") + if blockCanReach(nil, fn.Blocks[0], map[*ssa.BasicBlock]bool{}) { + t.Fatal("nil block should not reach anything") + } + if !blockCanReach(fn.Blocks[0], fn.Blocks[0], map[*ssa.BasicBlock]bool{}) { + t.Fatal("block should reach itself") + } + if instructionUsesValue(nil, fn.Params[0]) { + t.Fatal("nil instruction should not use values") + } + if instructionUsesValue(fn.Blocks[0].Instrs[0], nil) { + t.Fatal("nil value should not be used") + } + if isCallLikeInstruction(fn.Blocks[0].Instrs[0]) { + t.Fatal("if instruction should not be call-like") + } + if !isTerminatingInstruction(fn.Blocks[0].Instrs[len(fn.Blocks[0].Instrs)-1]) { + t.Fatal("entry block should end with a terminator") + } + + ctx := &context{} + if blk := refBlock(nil); blk != nil { + t.Fatalf("refBlock(nil) = %v", blk) + } + blocks := make(map[*ssa.BasicBlock]bool) + if !ctx.collectValueUseBlocks(nil, blocks, map[ssa.Value]bool{}, false) { + t.Fatal("nil collectValueUseBlocks should succeed") + } + if !ctx.collectValueUseBlocks(fn.Params[0], blocks, map[ssa.Value]bool{fn.Params[0]: true}, false) { + t.Fatal("seen collectValueUseBlocks should succeed") + } + if !ctx.collectValueUseBlocks(fn.Params[0], blocks, map[ssa.Value]bool{}, false) { + t.Fatal("collectValueUseBlocks failed") + } + if len(blocks) == 0 { + t.Fatal("expected use blocks for parameter") + } + if blk, ok := ctx.valueLastUseBlock(fn.Params[0]); !ok || blk == nil { + t.Fatalf("valueLastUseBlock = %v, %v", blk, ok) + } + if blk, ok := ctx.valueLastUseBlock(nil); !ok || blk != nil { + t.Fatalf("valueLastUseBlock(nil) = %v, %v", blk, ok) + } + if last, ok := ctx.lastUseInBlock(nil, fn.Blocks[0], map[ssa.Instruction]int{}, map[ssa.Value]bool{}); !ok || last != nil { + t.Fatalf("lastUseInBlock(nil) = %v, %v", last, ok) + } + split := ssapkg.Func("split") + if blk, ok := ctx.valueLastUseBlock(split.Params[0]); ok || blk != nil { + t.Fatalf("valueLastUseBlock(split param) = %v, %v; want no single block", blk, ok) + } + entryOrder := make(map[ssa.Instruction]int, len(split.Blocks[0].Instrs)) + for i, instr := range split.Blocks[0].Instrs { + entryOrder[instr] = i + } + if last, ok := ctx.lastUseInBlock(split.Params[0], split.Blocks[0], entryOrder, map[ssa.Value]bool{}); ok || last != nil { + t.Fatalf("lastUseInBlock(split param in entry) = %v, %v; want failure outside block", last, ok) + } + + var callLike int + for _, block := range ssapkg.Func("withCall").Blocks { + for _, instr := range block.Instrs { + if isCallLikeInstruction(instr) { + callLike++ + } + } + } + if callLike == 0 { + t.Fatal("flow should include at least one call-like instruction") + } + + refs := ssapkg.Func("refs") + var lastUseCount int + for _, param := range refs.Params { + blocks := make(map[*ssa.BasicBlock]bool) + if !ctx.collectValueUseBlocks(param, blocks, map[ssa.Value]bool{}, true) { + t.Fatalf("collectValueUseBlocks failed for %s", param.Name()) + } + if len(blocks) == 0 { + t.Fatalf("expected use blocks for %s", param.Name()) + } + blk, ok := ctx.valueLastUseBlock(param) + if !ok || blk == nil { + t.Fatalf("valueLastUseBlock(%s) = %v, %v", param.Name(), blk, ok) + } + order := make(map[ssa.Instruction]int, len(blk.Instrs)) + for i, instr := range blk.Instrs { + order[instr] = i + } + if last, ok := ctx.lastUseInBlock(param, blk, order, map[ssa.Value]bool{}); !ok { + t.Fatalf("lastUseInBlock(%s) = %v, %v", param.Name(), last, ok) + } else if last != nil { + lastUseCount++ + } + } + if lastUseCount == 0 { + t.Fatal("expected at least one parameter with a concrete last use") + } + phiBlocks := make(map[*ssa.BasicBlock]bool) + if !ctx.collectValueUseBlocks(refs.Params[0], phiBlocks, map[ssa.Value]bool{}, false) { + t.Fatal("non-following phi use collection failed") + } + if len(phiBlocks) == 0 { + t.Fatal("non-following phi use collection should record predecessor blocks") + } + converted := ssapkg.Func("converted") + if !ctx.collectValueUseBlocks(converted.Params[0], make(map[*ssa.BasicBlock]bool), map[ssa.Value]bool{}, true) { + t.Fatal("conversion use collection failed") + } +} + +func TestConservativeLivenessHelperFallbacks(t *testing.T) { + ssapkg := buildSSAPackageWithPath(t, "example.com/live", "live", `package live + +var Sink any + +func branch(cond bool) { + if cond { + Sink = 1 + } else { + Sink = 2 + } +} + +func useOne(p, q *int) { + Sink = p +} + +func neg(i int) int { + return -i +} + +func callDeref(f *func()) { + (*f)() +} + +func derefOnly(p **int) { + _ = *p +} + `) + ctx := &context{} + + branch := ssapkg.Func("branch") + if len(branch.Blocks) < 2 { + t.Fatalf("branch should have successors:\n%s", branch.String()) + } + if blockCanReach(branch.Blocks[0], branch.Blocks[1], map[*ssa.BasicBlock]bool{branch.Blocks[0]: true}) { + t.Fatal("seen entry block should stop reachability recursion") + } + + useOne := ssapkg.Func("useOne") + var useP ssa.Instruction + for _, block := range useOne.Blocks { + for _, instr := range block.Instrs { + if instructionUsesValue(instr, useOne.Params[0]) { + useP = instr + break + } + } + if useP != nil { + break + } + } + if useP == nil { + t.Fatalf("missing instruction that uses p:\n%s", useOne.String()) + } + if instructionUsesValue(useP, useOne.Params[1]) { + t.Fatal("instruction using p should not report use of q") + } + if ctx.isOnlyRuntimeSetFinalizerArg(useOne.Params[1]) { + t.Fatal("unused parameter should not be treated as a SetFinalizer-only argument") + } + if ctx.shouldSkipLateSetFinalizerValue(&ssa.UnOp{Op: token.MUL}) { + t.Fatal("deref without a single MakeInterface referrer should not be skipped") + } + + global := ssapkg.Members["Sink"].(*ssa.Global) + if !ctx.collectValueUseBlocks(global, make(map[*ssa.BasicBlock]bool), map[ssa.Value]bool{}, false) { + t.Fatal("global without referrers should be a valid value-use query") + } + if last, ok := ctx.lastUseInBlock(global, useOne.Blocks[0], map[ssa.Instruction]int{}, map[ssa.Value]bool{}); !ok || last != nil { + t.Fatalf("lastUseInBlock(global) = %v, %v", last, ok) + } + + neg := ssapkg.Func("neg") + var negInstr *ssa.UnOp + for _, block := range neg.Blocks { + for _, instr := range block.Instrs { + if unop, ok := instr.(*ssa.UnOp); ok && unop.Op == token.SUB { + negInstr = unop + break + } + } + if negInstr != nil { + break + } + } + if negInstr == nil { + t.Fatalf("missing unary negation:\n%s", neg.String()) + } + blocks := make(map[*ssa.BasicBlock]bool) + if !ctx.collectValueUseBlocks(neg.Params[0], blocks, map[ssa.Value]bool{}, false) { + t.Fatal("non-deref unary use collection failed") + } + if !blocks[negInstr.Block()] { + t.Fatal("non-deref unary use should record its block") + } + order := make(map[ssa.Instruction]int, len(negInstr.Block().Instrs)) + for i, instr := range negInstr.Block().Instrs { + order[instr] = i + } + if last, ok := ctx.lastUseInBlock(neg.Params[0], negInstr.Block(), order, map[ssa.Value]bool{}); !ok || last != negInstr { + t.Fatalf("lastUseInBlock(neg param) = %v, %v; want unary op", last, ok) + } + + callDeref := ssapkg.Func("callDeref") + var deref *ssa.UnOp + for _, block := range callDeref.Blocks { + for _, instr := range block.Instrs { + if unop, ok := instr.(*ssa.UnOp); ok && unop.Op == token.MUL { + deref = unop + break + } + } + if deref != nil { + break + } + } + if deref == nil { + t.Fatalf("missing call dereference:\n%s", callDeref.String()) + } + order = make(map[ssa.Instruction]int, len(deref.Block().Instrs)) + for i, instr := range deref.Block().Instrs { + order[instr] = i + } + if last, ok := ctx.lastUseInBlock(callDeref.Params[0], deref.Block(), order, map[ssa.Value]bool{}); !ok || last != deref { + t.Fatalf("lastUseInBlock(call deref param) = %v, %v; want deref", last, ok) + } + + derefOnly := ssapkg.Func("derefOnly") + var loneDeref *ssa.UnOp + for _, block := range derefOnly.Blocks { + for _, instr := range block.Instrs { + if unop, ok := instr.(*ssa.UnOp); ok && unop.Op == token.MUL { + loneDeref = unop + break + } + } + if loneDeref != nil { + break + } + } + if loneDeref == nil { + t.Fatalf("missing lone dereference:\n%s", derefOnly.String()) + } + if !ctx.collectValueUseBlocks(derefOnly.Params[0], make(map[*ssa.BasicBlock]bool), map[ssa.Value]bool{}, false) { + t.Fatal("lone deref use collection failed") + } + order = make(map[ssa.Instruction]int, len(loneDeref.Block().Instrs)) + for i, instr := range loneDeref.Block().Instrs { + order[instr] = i + } + if last, ok := ctx.lastUseInBlock(derefOnly.Params[0], loneDeref.Block(), order, map[ssa.Value]bool{}); !ok || last != loneDeref { + t.Fatalf("lastUseInBlock(lone deref param) = %v, %v; want deref", last, ok) + } +} + +func TestConservativeLivenessDebugRefs(t *testing.T) { + ssapkg, _ := buildSSAPackageWithPathAndFilesMode(t, "example.com/live", "live", `package live + +var Sink any + +func use(p *int) { + Sink = p +} + `, ssa.SanityCheckFunctions|ssa.InstantiateGenerics|ssa.GlobalDebug) + + fn := ssapkg.Func("use") + var debugRefs int + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + if _, ok := instr.(*ssa.DebugRef); ok { + debugRefs++ + } + } + } + if debugRefs == 0 { + t.Fatalf("debug SSA package did not contain DebugRef instructions:\n%s", fn.String()) + } + + ctx := &context{} + if !ctx.collectValueUseBlocks(fn.Params[0], make(map[*ssa.BasicBlock]bool), map[ssa.Value]bool{}, false) { + t.Fatal("DebugRef should be ignored while collecting use blocks") + } + order := make(map[ssa.Instruction]int, len(fn.Blocks[0].Instrs)) + for i, instr := range fn.Blocks[0].Instrs { + order[instr] = i + } + if last, ok := ctx.lastUseInBlock(fn.Params[0], fn.Blocks[0], order, map[ssa.Value]bool{}); !ok || last == nil { + t.Fatalf("lastUseInBlock with DebugRef = %v, %v", last, ok) + } +} + +func TestConservativeLivenessScanAllocPointerSlot(t *testing.T) { + prog := newLLSSAProg(t) + pkg := prog.NewPackage("live", "live") + ptrToInt := types.NewPointer(types.Typ[types.Int]) + slotType := types.NewPointer(ptrToInt) + sig := types.NewSignatureType(nil, nil, nil, + types.NewTuple(types.NewParam(token.NoPos, nil, "slot", slotType)), nil, false) + fn := pkg.NewFunc("scanPointerSlot", sig, llssa.InGo) + b := fn.MakeBody(1) + (&context{prog: prog}).scanAllocPointer(b, fn.Param(0)) + b.Return() + b.EndBuild() + + ir := pkg.String() + if !strings.Contains(ir, "llgo_clear_stack_ptr") { + t.Fatalf("pointer slot scan should emit stack clear helper:\n%s", ir) + } +} + +func TestCompileWithoutConservativeLivenessClears(t *testing.T) { + ssapkg, files := buildSSAPackageWithPathAndFiles(t, "command-line-arguments", "main", `package main + +func main() { + x := 1 + _ = &x +} +`) + + prog := newLLSSAProg(t) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + if strings.Contains(pkg.String(), "llgo_clear_stack_ptr") { + t.Fatalf("package without SetFinalizer should not emit liveness clear helpers:\n%s", pkg.String()) + } +} + +func TestCompileConservativeLivenessClears(t *testing.T) { + ssapkg, files := buildSSAPackageWithPathAndFiles(t, "github.com/goplus/llgo/runtime/livetest", "main", `package main + +import rt "github.com/goplus/llgo/runtime/internal/lib/runtime" + +type Box struct{ p *int } + +var Sink any + +func main() { + x := 1 + var box Box + box.p = &x + Sink = box.p + rt.SetFinalizer(&box, func(*Box) {}) + Sink = 1 +} +`) + ssapkg.Pkg = types.NewPackage("command-line-arguments", "main") + + prog := newLLSSAProg(t) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.String() + for _, want := range []string{"llgo_clear_stack_ptr", "llgo_clobber_pointer_regs"} { + if !strings.Contains(ir, want) { + t.Fatalf("compiled liveness module missing %s:\n%s", want, ir) + } + } + if !pkg.NeedRuntime { + t.Fatal("liveness clear helpers should mark runtime as needed") + } +} + +func TestCompileConservativeLivenessStructParamScans(t *testing.T) { + ssapkg, files := buildSSAPackageWithPathAndFiles(t, "github.com/goplus/llgo/runtime/livetest", "main", `package main + +import rt "github.com/goplus/llgo/runtime/internal/lib/runtime" +import "unsafe" + +type Cell struct{ p *int } +type Ptr *int + +var Sink any + +func consume(cell Cell) { + Sink = cell.p + Sink = 1 +} + +func consumePtr(p *int) { + Sink = p + Sink = 1 +} + +func branch(cell Cell, cond bool) { + if cond { + Sink = cell.p + } else { + Sink = 0 + } + Sink = 1 +} + +func main() { + x := 1 + y := 2 + arr := [2]*int{&x, &y} + cell := Cell{p: &x} + p := &x + pp := &p + ptr := Ptr(&x) + rt.SetFinalizer(&cell, func(*Cell) {}) + rt.SetFinalizer(&p, func(**int) {}) + rt.SetFinalizer(*pp, nil) + rt.SetFinalizer(&cell.p, func(**int) {}) + rt.SetFinalizer(&arr[0], func(**int) {}) + rt.SetFinalizer(unsafe.Pointer(&x), nil) + rt.SetFinalizer(ptr, nil) + consume(cell) + consumePtr(p) + branch(cell, x == y) +} + `) + ssapkg.Pkg = types.NewPackage("command-line-arguments", "main") + + prog := newLLSSAProg(t) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.String() + if strings.Count(ir, "llgo_clear_stack_ptr") < 2 { + t.Fatalf("expected stack pointer scans for struct param and local:\n%s", ir) + } + if !strings.Contains(ir, "llgo_clobber_pointer_regs") { + t.Fatalf("compiled liveness module missing clobber helper:\n%s", ir) + } +} diff --git a/doc/design/pclntab-linkphase.md b/doc/design/pclntab-linkphase.md new file mode 100644 index 0000000000..cbbbaf776e --- /dev/null +++ b/doc/design/pclntab-linkphase.md @@ -0,0 +1,132 @@ +# Link-phase ftab/findfunctab generation + +Status: design + staged plan. Depends on #2012 (runtime funcinfo find index) +and benefits from #2015 (nanosecond monotonic clock, for honest benchmarks). + +## Problem + +#2012 builds the sorted function-entry table and the Go-style findfunctab at +**first use in the running process**, because LLVM IR generation does not know +final linked text order. This leaves four measured gaps against Go 1.26: + +1. `cold.FirstFuncForPC`: 36µs on macOS / 12µs on Linux vs Go's 2.4µs / 375ns. + The cold fast path (bounded linear scan of raw entry sections, then dladdr) + is a transitional mechanism; Go needs none of it because the linker ships a + sorted table. +2. LTO inlining duplicates the body-embedded entry-site inline asm into every + inline site: `llgo_funcinfo_entry` grew ~4x on the multipkg benchmark and + host-function PCs get registered under the inlinee's symbol ID. IR-level + fixes were tried and ruled out (see Facts below); dedup must happen after + final code generation. +3. The runtime keeps ~300 lines of transitional complexity: cold lookup + budget, section scans, first-use sort, entry-PC slack matching. +4. pcvalue-style instruction-level line tables (the next alignment step with + Go) need a per-function table keyed by final text order. + +## Approach: post-link table generation + +Insert a post-link step into `internal/build` after the final clang/lld link: + +``` +link -> post-link tool: parse binary -> sort/dedup -> build buckets -> write back +``` + +A separate linker plugin was considered and rejected: llgo drives stock +clang/lld and a plugin would need to be maintained per linker flavor +(ld64.lld, ld.lld) and per LTO mode. Editing the linked artifact is +linker-agnostic. + +### Data flow + +1. **Parse** the linked binary's metadata sections (`debug/elf`, + `debug/macho` from the Go stdlib — the tool runs on the host): + - `llgo_funcinfo_entry` / `__DATA,__llgo_fie`: `{pc, symbolID}` records. + - `llgo_funcinfo_stubsite` / `__DATA,__llgo_stub`: same layout. + - Zero records are skipped, as in the runtime today. +2. **Dedup by symbolID**: LTO inline copies register the same symbolID at + several PCs. The true entry is the record whose PC lies inside the text + range of the symbol that owns the symbolID; resolve via the binary's + symbol table (`.symtab` / `nlist`). Records that fall inside a different + function's range are inline copies — drop them. This is the fix for gap 2 + that IR-level metadata could not express. +3. **Sort** by PC; append a sentinel entry (end of text) so the runtime can + use Go's forward-scan lookup shape (`internal/pclntab.LookupFuncIndex`). +4. **Build buckets** with `internal/pclntab.BuildFindFuncBuckets` — the + faithful port of `cmd/link`'s algorithm that has been sitting unwired + since #2012. Delta overflow is a hard error here, mirroring Go's linker; + if it ever fires, fall back to leaving the prebuilt table absent. +5. **Write back** into a reserved section: + - The main module already emits `__llgo_funcinfo_*` globals; add a + `__llgo_pclntab_prebuilt` global sized from the collected package data + (entry-record count is known at main-module emission time; LTO can only + shrink it after dedup) plus a header {magic, version, count, anchorOff}. + - The tool rewrites the section contents in place (same size or smaller; + unused tail is zeroed) and flips the header magic to "valid". + +### ASLR + +Stored PCs must survive load-time slide. Store **offsets relative to an +anchor symbol** (`__llgo_pclntab_anchor`, placed in the same section). At +startup the runtime computes `slide = &anchor_runtime - anchorOff_stored` +and adds it during lookup (one add on the hot path, same as Go's +`datap.text` bias). Note the entry-site records themselves are already +rebased by the loader (they hold absolute pointers with relocations); the +prebuilt table deliberately holds offsets so the tool does not need to +emit relocations. + +### Runtime integration + +`initRuntimeFuncPCFramesOnce` gains a fast path: if the prebuilt header is +valid, adopt the table directly (no section scan, no sort, no bucket build) +— `FirstFuncForPC` becomes bucket-lookup cost, matching Go's shape. The +existing first-use construction remains as the fallback whenever the header +is invalid (older compilers, exotic formats, overflow bail-out), so the +change is strictly additive and safe to land incrementally. + +## Staging + +- **P1** `chore/pclnpost`: standalone tool, parse + dedup + sort + bucket + build + stats printing; golden tests against binaries produced by the + existing test programs. No behavior change. +- **P2** Reserve the section in `internal/build`, run the tool as a post-link + step, wire the runtime fast path. Benchmarks: cold.FirstFuncForPC on both + platforms; assert `llgo funcinfo: ... entries= prebuilt` via + LLGO_FUNCINFO_DEBUG. +- **P3** (done) Mach-O bind-record resolution: pointer slots naming exported + functions — every `__llgo_stub.*` and any exported Go function — are + chained-fixup BIND nodes, not rebases; without decoding them through the + imports table, all stub records miss the prebuilt ftab and function-value + `FuncForPC` silently pays a dladdr per fresh pc (~6µs). Also: the prebuilt + header's base slot is spliced back into the fixup chain as a live rebase + node, so the runtime reads a dyld-slid runtime PC directly (no slide + arithmetic). Transitional cold budget/scan stays as the fallback for + non-rewritten binaries. +- **P4** pcvalue-style line tables keyed by the prebuilt function order + (replaces the call-site pcline records; gives instruction-level FileLine). + +## Established facts (verified in #2012 work; do not re-derive) + +- Mach-O metadata sections need `live_support` + one lowercase-`l` + linker-private symbol per record; ld64/lld `-dead_strip` then drops records + exactly with their function. Verified with lld 19.1.7, including LTO. +- Boundary symbols: ELF `__start_/__stop_`; Mach-O `section$start$SEG$SECT` + referenced from IR needs the `\x01` verbatim-name prefix or LLVM prepends + an underscore and the linker stops recognizing it. +- Visible (non-`L`) labels inside Mach-O function bodies split the function + into atoms that the linker may reorder — assembler-local labels only. +- `!associated` affects only linker GC; IR-level GlobalDCE deletes such + globals regardless, and `llvm.compiler.used` pins dead functions through + the records' initializers. This is why records stay body-embedded inline + asm and dedup happens post-link. +- Mach-O chained fixups encode anchors to exported symbols as BIND nodes + (import ordinal + addend), even when the target is defined in the same + image; only local-symbol anchors are rebases. Decode both. +- Adding fixup nodes does not pre-touch pages at load on modern macOS: + dyld uses page-in linking (the kernel applies fixups lazily at first + touch), so "sacrificial fixups to warm the table's pages" is not a + viable optimization — measured no effect on first-lookup latency. +- `internal/pclntab` is a faithful port of Go 1.26's findfunctab generation + and lookup (uint8 deltas, overflow error, forward scan, sentinel); the + runtime's in-process variant deliberately uses uint16 deltas because LLGo + lacks Go's MINFUNC guarantee. The post-link table can use the faithful + uint8 layout since dedup restores the one-record-per-function invariant. diff --git a/internal/build/build.go b/internal/build/build.go index 149411815f..db02fd67f3 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -52,6 +52,7 @@ import ( "github.com/goplus/llgo/internal/monitor" "github.com/goplus/llgo/internal/optlevel" "github.com/goplus/llgo/internal/packages" + "github.com/goplus/llgo/internal/pclnpost" "github.com/goplus/llgo/internal/typepatch" "github.com/goplus/llgo/ssa/abi" xenv "github.com/goplus/llgo/xtool/env" @@ -318,6 +319,16 @@ func Do(args []string, conf *Config) ([]Package, error) { prog := llssa.NewProgram(target) prog.EnableGoGlobalDCE(conf.goGlobalDCEEnabled()) + funcInfo := conf.Mode != ModeGen && IsFuncInfoEnabled() + prog.EnableFuncInfoMetadata(funcInfo) + // Site records are inline-asm fragments inside function bodies; their + // anchors shift instruction/scope layout enough to confuse debuggers + // (LLDB reported variables from an inner lexical block as in scope before + // the block began). Debug builds keep the metadata tables — FuncForPC + // name/FileLine fidelity survives via the dlsym path — but drop the + // sites. Caller-frame instrumentation is independent of both switches, + // so runtime.Caller keeps working in debug builds. + prog.EnableFuncInfoSites(funcInfo && !IsDbgEnabled() && IsFuncInfoSitesEnabled()) sizes := func(sizes types.Sizes, compiler, arch string) types.Sizes { if arch == "wasm" { sizes = &types.StdSizes{WordSize: 4, MaxAlign: 4} @@ -467,6 +478,7 @@ func Do(args []string, conf *Config) ([]Package, error) { if err != nil { return nil, err } + rewritePrebuiltFuncTab(ctx, outFmts.Out, verbose) if conf.Mode == ModeBuild && conf.SizeReport { if err := reportBinarySize(outFmts.Out, conf.SizeFormat, conf.SizeLevel, allPkgs); err != nil { fmt.Fprintf(os.Stderr, "Warning: size report failed: %v\n", err) @@ -960,6 +972,35 @@ func compileExtraFiles(ctx *context, verbose bool) ([]string, error) { return objFiles, nil } +// rewritePrebuiltFuncTab runs the link-phase prebuilt-table rewrite on the +// linked executable: it deduplicates LTO inline copies of the funcinfo entry +// records against the symbol table and replaces the entry section with a +// sorted ftab plus findfunctab that the runtime adopts zero-copy (see +// internal/pclnpost and doc/design/pclntab-linkphase.md). Any failure leaves +// the binary fully functional on the first-use construction fallback. +func rewritePrebuiltFuncTab(ctx *context, out string, verbose bool) { + if ctx == nil || ctx.prog == nil || !ctx.prog.FuncInfoSitesEnabled() || !shouldEmitRuntimeSites(ctx) { + return + } + if ctx.buildConf.BuildMode != BuildModeExe { + return + } + if os.Getenv("LLGO_PCLNPOST") == "0" { // escape hatch: keep first-use construction + return + } + st, err := pclnpost.Rewrite(out) + if err != nil { + if verbose { + fmt.Fprintf(os.Stderr, "llgo: prebuilt functab rewrite skipped: %v\n", err) + } + return + } + if verbose { + fmt.Fprintf(os.Stderr, "llgo: prebuilt functab: %d entries (%d LTO inline copies removed), %d buckets\n", + st.FtabEntries, st.InlineCopies, st.Buckets) + } +} + func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPath string, verbose bool) error { needRuntime := false needPyInit := false @@ -1043,6 +1084,9 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa // Generate main module file (needed for global variables even in library modes) // This is compiled directly to .o and added to linkInputs (not cached) // Use a stable synthetic name to avoid confusing it with the real main package in traces/logs. + funcInfo := prepareFuncInfoTableRecords(collectFuncInfo(linkedOrder), nil) + pcLineInfo := collectPCLineInfo(linkedOrder) + funcInfoStubs := collectFuncInfoStubRecords(linkedOrder, funcInfo) entryPkg := genMainModule(ctx, llssa.PkgRuntime, pkg, &genConfig{ rtInit: needRuntime, pyInit: needPyInit, @@ -1050,6 +1094,9 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa methodByIndex: methodByIndex, methodByName: methodByName, abiSymbols: linkedModuleGlobals(linkedOrder), + funcInfo: funcInfo, + pcLineInfo: pcLineInfo, + funcInfoStubs: funcInfoStubs, }) entryObjFile, err := exportObject(ctx, "entry_main", entryPkg.ExportFile, entryPkg.LPkg) if err != nil { @@ -1100,6 +1147,9 @@ func linkedModuleGlobals(pkgs []Package) map[string]none { continue } for g := pkg.LPkg.Module().FirstGlobal(); !g.IsNil(); g = gllvm.NextGlobal(g) { + if g.IsDeclaration() { + continue + } seen[g.Name()] = none{} } } @@ -1130,9 +1180,9 @@ func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose if needsLinuxNoPIE(ctx, linkArgs) { buildArgs = append(buildArgs, "-no-pie") } + buildArgs = append(buildArgs, linuxExportDynamicArgs(ctx)...) } - // Add common linker arguments based on target OS and architecture if IsDbgSymsEnabled() { buildArgs = append(buildArgs, "-gdwarf-4") } @@ -1178,6 +1228,20 @@ func needsLinuxNoPIE(ctx *context, linkArgs []string) bool { return true } +func needsLinuxExportDynamic(ctx *context) bool { + return ctx.buildConf.Target == "" && ctx.buildConf.Goos == "linux" && IsFuncInfoEnabled() +} + +func linuxExportDynamicArgs(ctx *context) []string { + if !needsLinuxExportDynamic(ctx) { + return nil + } + return []string{ + "-Wl,--export-dynamic-symbol=main.*", + "-Wl,--export-dynamic-symbol=command-line-arguments.*", + } +} + // archiver returns the archiving tool to use for the current context. // For wasm targets and LTO builds, it prefers llvm-ar because linkers need // LLVM-aware archive indexes for wasm objects and bitcode members. @@ -1324,6 +1388,8 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) error { return fmt.Errorf("run LLVM passes failed for %v: %v", pkgPath, err) } } + emitFuncInfoEntrySites(ctx, ret) + emitFuncInfoStubSites(ctx, ret) printCmds := ctx.shouldPrintCommands(verbose) cgoLLFiles, cgoLdflags, err := buildCgo(ctx, aPkg, aPkg.Package.Syntax, externs, printCmds) @@ -1796,6 +1862,8 @@ var ( const llgoDebug = "LLGO_DEBUG" const llgoDbgSyms = "LLGO_DEBUG_SYMBOLS" +const llgoFuncInfo = "LLGO_FUNCINFO" +const llgoFuncInfoSites = "LLGO_FUNCINFO_SITES" const llgoTrace = "LLGO_TRACE" const llgoOptimize = "LLGO_OPTIMIZE" const llgoWasmRuntime = "LLGO_WASM_RUNTIME" @@ -1843,6 +1911,18 @@ func IsDbgEnabled() bool { return isEnvOn(llgoDebug, false) || isEnvOn(llgoDbgSyms, false) } +func IsFuncInfoEnabled() bool { + return isEnvOn(llgoFuncInfo, true) +} + +// IsFuncInfoSitesEnabled controls the body-embedded site records +// independently of the funcinfo tables (LLGO_FUNCINFO_SITES=0 keeps the +// metadata but drops entry/stub/pc-line inline-asm sites). Useful for +// isolating codegen perturbation caused by the in-body asm anchors. +func IsFuncInfoSitesEnabled() bool { + return isEnvOn(llgoFuncInfoSites, true) +} + func IsDbgSymsEnabled() bool { return isEnvOn(llgoDbgSyms, false) } diff --git a/internal/build/build_test.go b/internal/build/build_test.go index 51401c2131..c8ce08e9e6 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -22,6 +22,7 @@ import ( "github.com/goplus/llgo/internal/mockable" "github.com/goplus/llgo/internal/packages" llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" ) func TestMain(m *testing.M) { @@ -55,6 +56,68 @@ func TestNeedsLinuxNoPIE(t *testing.T) { } } +func TestNeedsLinuxExportDynamic(t *testing.T) { + t.Setenv(llgoFuncInfo, "") + ctx := &context{buildConf: &Config{Goos: "linux"}} + if !needsLinuxExportDynamic(ctx) { + t.Fatal("linux funcinfo executable should export dynamic symbols") + } + if got := linuxExportDynamicArgs(ctx); strings.Join(got, " ") != "-Wl,--export-dynamic-symbol=main.* -Wl,--export-dynamic-symbol=command-line-arguments.*" { + t.Fatalf("linuxExportDynamicArgs = %v", got) + } + t.Setenv(llgoFuncInfo, "0") + if needsLinuxExportDynamic(ctx) { + t.Fatal("LLGO_FUNCINFO=0 should disable dynamic symbol export") + } + if got := linuxExportDynamicArgs(ctx); got != nil { + t.Fatalf("disabled linuxExportDynamicArgs = %v, want nil", got) + } + t.Setenv(llgoFuncInfo, "1") + ctx.buildConf.Goos = "darwin" + if needsLinuxExportDynamic(ctx) { + t.Fatal("non-linux executable should not export dynamic symbols for funcinfo") + } + ctx.buildConf.Goos = "linux" + ctx.buildConf.Target = "wasi" + if needsLinuxExportDynamic(ctx) { + t.Fatal("named targets should not force host linux dynamic symbol export") + } +} + +func TestIsFuncInfoEnabled(t *testing.T) { + t.Setenv(llgoFuncInfo, "") + if !IsFuncInfoEnabled() { + t.Fatal("funcinfo should be enabled by default") + } + t.Setenv(llgoFuncInfo, "0") + if IsFuncInfoEnabled() { + t.Fatal("LLGO_FUNCINFO=0 should disable funcinfo") + } + t.Setenv(llgoFuncInfo, "1") + if !IsFuncInfoEnabled() { + t.Fatal("LLGO_FUNCINFO=1 should enable funcinfo") + } +} + +func TestLinkedModuleGlobalsSkipsDeclarations(t *testing.T) { + prog := llssa.NewProgram(nil) + lpkg := prog.NewPackage("example.com/p", "example.com/p") + mod := lpkg.Module() + i32 := mod.Context().Int32Type() + + defined := llvm.AddGlobal(mod, i32, "example.com/p.defined") + defined.SetInitializer(llvm.ConstInt(i32, 1, false)) + llvm.AddGlobal(mod, i32, "example.com/p.declared") + + got := linkedModuleGlobals([]Package{{LPkg: lpkg}}) + if _, ok := got["example.com/p.defined"]; !ok { + t.Fatalf("linkedModuleGlobals missing defined global: %#v", got) + } + if _, ok := got["example.com/p.declared"]; ok { + t.Fatalf("linkedModuleGlobals should skip external declarations: %#v", got) + } +} + func mockRun(args []string, cfg *Config) { defer mockable.DisableMock() mockable.EnableMock() diff --git a/internal/build/collect.go b/internal/build/collect.go index ab6d7076f6..dd30daf72b 100644 --- a/internal/build/collect.go +++ b/internal/build/collect.go @@ -82,6 +82,7 @@ func (c *context) collectEnvInputs(m *manifestBuilder) { envVars := []string{ llgoDebug, llgoDbgSyms, + llgoFuncInfo, llgoTrace, llgoOptimize, llgoWasmRuntime, diff --git a/internal/build/funcinfo/funcinfo.go b/internal/build/funcinfo/funcinfo.go new file mode 100644 index 0000000000..76c7cc123c --- /dev/null +++ b/internal/build/funcinfo/funcinfo.go @@ -0,0 +1,416 @@ +/* + * 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 funcinfo + +import ( + "fmt" + "math" + "sort" + "strings" +) + +type Record struct { + Symbol string + Name string + File string + Line uint32 + Column uint32 +} + +type PCLineRecord struct { + ID uint64 + Symbol string + File string + Line uint32 + Column uint32 +} + +type EncodedRecord struct { + SymbolPkg uint16 + SymbolName uint16 + NamePkg uint16 + NameName uint16 + FileRoot uint16 + FileName uint16 + Line uint32 +} + +type EncodedPCLineRecord struct { + ID uint64 + Func uint32 + File uint32 + Line uint32 +} + +type Table struct { + Records []EncodedRecord + PCLines []EncodedPCLineRecord + StringOffsets []uint32 + Strings []byte + Hash []uint16 +} + +func Encode(records []Record) (Table, error) { + return EncodeWithPCLines(records, nil) +} + +func EncodeWithPCLines(records []Record, pcLines []PCLineRecord) (Table, error) { + funcIndex := make(map[string]uint32, len(records)) + for i, rec := range records { + if rec.Symbol != "" { + funcIndex[rec.Symbol] = uint32(i + 1) + } + } + filteredPCLines := make([]PCLineRecord, 0, len(pcLines)) + for _, rec := range pcLines { + if rec.ID == 0 || funcIndex[rec.Symbol] == 0 { + continue + } + filteredPCLines = append(filteredPCLines, rec) + } + if len(records) == 0 && len(filteredPCLines) == 0 { + return Table{}, nil + } + ids, offsets, strings, err := buildStringTable(collectStrings(records, filteredPCLines)) + if err != nil { + return Table{}, err + } + out := Table{ + Records: make([]EncodedRecord, 0, len(records)), + StringOffsets: offsets, + Strings: strings, + } + for _, rec := range records { + symPkg, symName := splitQualifiedName(rec.Symbol) + namePkg, nameName := splitQualifiedName(rec.Name) + fileRoot, fileName := splitFileName(rec.File) + out.Records = append(out.Records, EncodedRecord{ + SymbolPkg: ids[symPkg], + SymbolName: ids[symName], + NamePkg: ids[namePkg], + NameName: ids[nameName], + FileRoot: ids[fileRoot], + FileName: ids[fileName], + Line: rec.Line, + }) + } + out.PCLines = make([]EncodedPCLineRecord, 0, len(filteredPCLines)) + for _, rec := range filteredPCLines { + idx := funcIndex[rec.Symbol] + fileRoot, fileName := splitFileName(rec.File) + out.PCLines = append(out.PCLines, EncodedPCLineRecord{ + ID: rec.ID, + Func: idx, + File: packStringIDs(ids[fileRoot], ids[fileName]), + Line: rec.Line, + }) + } + sort.Slice(out.PCLines, func(i, j int) bool { + return out.PCLines[i].ID < out.PCLines[j].ID + }) + out.Hash, err = buildHash(records) + if err != nil { + return Table{}, err + } + return out, nil +} + +func collectStrings(records []Record, pcLines []PCLineRecord) []string { + seen := make(map[string]bool) + for _, rec := range records { + for _, s := range splitRecordStrings(rec) { + seen[s] = true + } + } + for _, rec := range pcLines { + fileRoot, fileName := splitFileName(rec.File) + seen[fileRoot] = true + seen[fileName] = true + } + delete(seen, "") + out := make([]string, 0, len(seen)) + for s := range seen { + out = append(out, s) + } + sort.Slice(out, func(i, j int) bool { + if len(out[i]) != len(out[j]) { + return len(out[i]) > len(out[j]) + } + return out[i] < out[j] + }) + return out +} + +func packStringIDs(hi, lo uint16) uint32 { + return uint32(hi)<<16 | uint32(lo) +} + +func splitRecordStrings(rec Record) []string { + symPkg, symName := splitQualifiedName(rec.Symbol) + namePkg, nameName := splitQualifiedName(rec.Name) + fileRoot, fileName := splitFileName(rec.File) + return []string{symPkg, symName, namePkg, nameName, fileRoot, fileName} +} + +func buildStringTable(strings []string) (map[string]uint16, []uint32, []byte, error) { + ids := map[string]uint16{"": 0} + values := []string{""} + for _, s := range strings { + if _, ok := ids[s]; ok { + continue + } + if len(values) > math.MaxUint16 { + return nil, nil, nil, fmt.Errorf("funcinfo string id table exceeds 65535 entries") + } + ids[s] = uint16(len(values)) + values = append(values, s) + } + pool := stringPool{ + offsets: map[string]uint32{"": 0}, + data: []byte{0}, + text: "\x00", + } + offsets := make([]uint32, len(values)) + for id, s := range values { + off, err := pool.offset(s) + if err != nil { + return nil, nil, nil, err + } + offsets[id] = off + } + return ids, offsets, pool.data, nil +} + +func splitQualifiedName(name string) (pkg, local string) { + if name == "" { + return "", "" + } + start := strings.LastIndexByte(name, '/') + if start < 0 { + start = 0 + } else { + start++ + } + if dot := strings.IndexByte(name[start:], '.'); dot >= 0 { + idx := start + dot + return name[:idx], name[idx+1:] + } + return "", name +} + +func splitFileName(file string) (root, name string) { + if file == "" { + return "", "" + } + if slash := strings.LastIndexByte(file, '/'); slash >= 0 { + return file[:slash+1], file[slash+1:] + } + return "", file +} + +type stringPool struct { + offsets map[string]uint32 + data []byte + text string +} + +func (p *stringPool) offset(s string) (uint32, error) { + if off, ok := p.offsets[s]; ok { + return off, nil + } + if off := strings.Index(p.text, s+"\x00"); off >= 0 { + uoff := uint32(off) + p.offsets[s] = uoff + return uoff, nil + } + if len(p.data)+len(s)+1 > math.MaxUint32 { + return 0, fmt.Errorf("funcinfo string table exceeds 4 GiB") + } + off := uint32(len(p.data)) + p.data = append(p.data, s...) + p.data = append(p.data, 0) + p.text = string(p.data) + p.offsets[s] = off + return off, nil +} + +func buildHash(records []Record) ([]uint16, error) { + if len(records) == 0 { + return nil, nil + } + if len(records) > math.MaxUint16 { + // Runtime hash slots store 1-based uint16 record indexes. Larger + // tables remain correct by omitting the hash and using linear lookup. + return nil, nil + } + buckets := 1 + for buckets*3 < len(records)*4 { + buckets <<= 1 + } + hash := make([]uint16, buckets) + for i, rec := range records { + slot := int(HashString(rec.Symbol) & uint32(buckets-1)) + for hash[slot] != 0 { + slot = (slot + 1) & (buckets - 1) + } + hash[slot] = uint16(i + 1) + } + return hash, nil +} + +func HashString(s string) uint32 { + const ( + offset = uint32(2166136261) + prime = uint32(16777619) + ) + h := offset + for i := 0; i < len(s); i++ { + h ^= uint32(s[i]) + h *= prime + } + return h +} + +func (t Table) String(id uint16) string { + if int(id) >= len(t.StringOffsets) { + return "" + } + return cstring(t.Strings, t.StringOffsets[id]) +} + +func (t Table) Symbol(rec EncodedRecord) string { + return joinQualified(t.String(rec.SymbolPkg), t.String(rec.SymbolName)) +} + +func (t Table) Name(rec EncodedRecord) string { + return joinQualified(t.String(rec.NamePkg), t.String(rec.NameName)) +} + +func (t Table) File(rec EncodedRecord) string { + return t.String(rec.FileRoot) + t.String(rec.FileName) +} + +func (t Table) PCLineFile(rec EncodedPCLineRecord) string { + return t.String(uint16(rec.File>>16)) + t.String(uint16(rec.File)) +} + +func (t Table) LookupSymbol(symbol string) (int, bool) { + if len(t.Hash) == 0 { + return 0, false + } + mask := uint32(len(t.Hash) - 1) + slot := HashString(symbol) & mask + for probes := 0; probes < len(t.Hash); probes++ { + idx := t.Hash[slot] + if idx == 0 { + return 0, false + } + rec := t.Records[idx-1] + if t.Symbol(rec) == symbol { + return int(idx - 1), true + } + slot = (slot + 1) & mask + } + return 0, false +} + +func (t Table) SizeBytes() int { + return len(t.Records)*16 + len(t.PCLines)*24 + len(t.StringOffsets)*4 + len(t.Strings) + len(t.Hash)*2 +} + +func joinQualified(pkg, local string) string { + if pkg == "" { + return local + } + if local == "" { + return pkg + } + return pkg + "." + local +} + +func cstring(data []byte, off uint32) string { + end := int(off) + for end < len(data) && data[end] != 0 { + end++ + } + return string(data[off:end]) +} + +type PCIndex struct { + PageShift uint + Base uint64 + Pages []uint32 +} + +const DefaultPCPageShift = 12 + +func BuildPCIndex(entries []uint64) PCIndex { + return BuildPCIndexWithShift(entries, DefaultPCPageShift) +} + +func BuildPCIndexWithShift(entries []uint64, shift uint) PCIndex { + if len(entries) == 0 { + return PCIndex{PageShift: shift} + } + base := entries[0] >> shift + last := entries[len(entries)-1] >> shift + pages := make([]uint32, last-base+2) + next := 0 + for page := range pages { + limit := (base + uint64(page)) << shift + for next < len(entries) && entries[next] < limit { + next++ + } + pages[page] = uint32(next) + } + return PCIndex{ + PageShift: shift, + Base: base, + Pages: pages, + } +} + +func LookupPC(entries []uint64, index PCIndex, pc uint64) int { + if len(entries) == 0 { + return -1 + } + lo, hi := 0, len(entries) + page := pc >> index.PageShift + if len(index.Pages) != 0 && page >= index.Base { + off := page - index.Base + if off < uint64(len(index.Pages)) { + lo = int(index.Pages[off]) + if off+1 < uint64(len(index.Pages)) { + hi = int(index.Pages[off+1]) + } + if lo > 0 { + lo-- + } + if hi < len(entries) { + hi++ + } + } + } + i := sort.Search(hi-lo, func(i int) bool { + return entries[lo+i] > pc + }) + idx := lo + i - 1 + if idx < 0 { + return -1 + } + return idx +} diff --git a/internal/build/funcinfo/funcinfo_test.go b/internal/build/funcinfo/funcinfo_test.go new file mode 100644 index 0000000000..78a59fad2a --- /dev/null +++ b/internal/build/funcinfo/funcinfo_test.go @@ -0,0 +1,242 @@ +/* + * 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 funcinfo + +import "testing" + +func TestEncodePoolsStringsAndBuildsHash(t *testing.T) { + table, err := Encode([]Record{ + {Symbol: "example.com/p.a", Name: "example.com/p.A", File: "/src/p/shared.go", Line: 10, Column: 1}, + {Symbol: "example.com/p.b", Name: "example.com/p.B", File: "shared.go", Line: 20, Column: 2}, + }) + if err != nil { + t.Fatal(err) + } + if len(table.Records) != 2 { + t.Fatalf("encoded records = %d, want 2", len(table.Records)) + } + if table.Records[0].FileRoot == table.Records[1].FileRoot { + t.Fatalf("distinct file roots should use distinct ids") + } + if got := table.File(table.Records[1]); got != "shared.go" { + t.Fatalf("suffix file string = %q, want shared.go", got) + } + if len(table.Hash) == 0 || len(table.Hash)&(len(table.Hash)-1) != 0 { + t.Fatalf("hash bucket count = %d, want power-of-two non-zero", len(table.Hash)) + } + if idx, ok := table.LookupSymbol("example.com/p.a"); !ok || idx != 0 { + t.Fatalf("lookup a = %d, %v; want 0, true", idx, ok) + } + if idx, ok := table.LookupSymbol("example.com/p.b"); !ok || idx != 1 { + t.Fatalf("lookup b = %d, %v; want 1, true", idx, ok) + } + if _, ok := table.LookupSymbol("missing"); ok { + t.Fatalf("lookup missing succeeded") + } +} + +func TestEncodeWithPCLines(t *testing.T) { + table, err := EncodeWithPCLines( + []Record{ + {Symbol: "example.com/p.f", Name: "example.com/p.F", File: "/src/p/f.go", Line: 10, Column: 1}, + {Symbol: "example.com/p.g", Name: "example.com/p.G", File: "/src/p/g.go", Line: 20, Column: 1}, + }, + []PCLineRecord{ + {ID: 3, Symbol: "missing", File: "missing.go", Line: 30}, + {ID: 2, Symbol: "example.com/p.g", File: "/src/p/call_g.go", Line: 22}, + {ID: 1, Symbol: "example.com/p.f", File: "/src/p/call_f.go", Line: 12}, + {ID: 0, Symbol: "example.com/p.f", File: "zero.go", Line: 1}, + }, + ) + if err != nil { + t.Fatal(err) + } + if len(table.PCLines) != 2 { + t.Fatalf("encoded pclines = %d, want 2", len(table.PCLines)) + } + if got := table.PCLines[0]; got.ID != 1 || got.Func != 1 || got.Line != 12 { + t.Fatalf("first pcline = %+v, want id 1 func 1 line 12", got) + } + if got := table.PCLines[1]; got.ID != 2 || got.Func != 2 || got.Line != 22 { + t.Fatalf("second pcline = %+v, want id 2 func 2 line 22", got) + } + if got := table.PCLineFile(table.PCLines[0]); got != "/src/p/call_f.go" { + t.Fatalf("pcline file = %q, want /src/p/call_f.go", got) + } +} + +func TestEncodeRoundTripsSingleRecord(t *testing.T) { + table, err := Encode([]Record{{Symbol: "s", Name: "n", File: "f", Line: 1, Column: 2}}) + if err != nil { + t.Fatal(err) + } + if got, want := len(table.Records), 1; got != want { + t.Fatalf("records = %d, want %d", got, want) + } + rec := table.Records[0] + if got, want := table.Symbol(rec), "s"; got != want { + t.Fatalf("symbol = %q, want %q", got, want) + } + if got, want := table.Name(rec), "n"; got != want { + t.Fatalf("name = %q, want %q", got, want) + } + if got, want := table.File(rec), "f"; got != want { + t.Fatalf("file = %q, want %q", got, want) + } + if rec.Line != 1 { + t.Fatalf("source line = %d, want 1", rec.Line) + } +} + +func TestEncodeHashHandlesCollisions(t *testing.T) { + a, b := collisionPair(t) + table, err := Encode([]Record{ + {Symbol: a, Name: a, File: "a.go"}, + {Symbol: b, Name: b, File: "b.go"}, + }) + if err != nil { + t.Fatal(err) + } + if idx, ok := table.LookupSymbol(a); !ok || idx != 0 { + t.Fatalf("lookup collision a = %d, %v; want 0, true", idx, ok) + } + if idx, ok := table.LookupSymbol(b); !ok || idx != 1 { + t.Fatalf("lookup collision b = %d, %v; want 1, true", idx, ok) + } +} + +func TestEncodeOmitsHashWhenRecordIndexesDoNotFitUint16(t *testing.T) { + records := make([]Record, 1<<16) + for i := range records { + records[i] = Record{Symbol: "example.com/p.f", Name: "example.com/p.F"} + } + table, err := Encode(records) + if err != nil { + t.Fatal(err) + } + if table.Hash != nil { + t.Fatalf("hash buckets = %d, want nil fallback for oversized table", len(table.Hash)) + } + if len(table.Records) != len(records) { + t.Fatalf("records = %d, want %d", len(table.Records), len(records)) + } +} + +func TestEncodeSplitsPackageAndFilePrefixes(t *testing.T) { + records := []Record{ + {Symbol: "example.com/p.alpha", Name: "example.com/p.Alpha", File: "/home/me/mod/p/alpha.go", Line: 10}, + {Symbol: "example.com/p.beta", Name: "example.com/p.Beta", File: "/home/me/mod/p/beta.go", Line: 20}, + {Symbol: "example.com/q.gamma", Name: "example.com/q.Gamma", File: "/home/me/mod/q/gamma.go", Line: 30}, + } + table, err := Encode(records) + if err != nil { + t.Fatal(err) + } + for i, rec := range table.Records { + if got := table.Symbol(rec); got != records[i].Symbol { + t.Fatalf("record %d symbol = %q, want %q", i, got, records[i].Symbol) + } + if got := table.Name(rec); got != records[i].Name { + t.Fatalf("record %d name = %q, want %q", i, got, records[i].Name) + } + if got := table.File(rec); got != records[i].File { + t.Fatalf("record %d file = %q, want %q", i, got, records[i].File) + } + } + if table.Records[0].SymbolPkg != table.Records[1].SymbolPkg { + t.Fatalf("same package prefix got different ids: %d vs %d", table.Records[0].SymbolPkg, table.Records[1].SymbolPkg) + } + if table.Records[0].FileRoot != table.Records[1].FileRoot { + t.Fatalf("same file root got different ids: %d vs %d", table.Records[0].FileRoot, table.Records[1].FileRoot) + } + if got := table.SizeBytes(); got >= legacySizeBytes(records) { + t.Fatalf("compressed table size = %d, want below legacy %d", got, legacySizeBytes(records)) + } +} + +func TestLookupPCUsesPageIndex(t *testing.T) { + entries := []uint64{0x1000, 0x1010, 0x2800, 0x4000, 0x4010} + index := BuildPCIndex(entries) + tests := []struct { + pc uint64 + want int + }{ + {0xfff, -1}, + {0x1000, 0}, + {0x100f, 0}, + {0x1010, 1}, + {0x27ff, 1}, + {0x2800, 2}, + {0x4018, 4}, + } + for _, tt := range tests { + if got := LookupPC(entries, index, tt.pc); got != tt.want { + t.Fatalf("LookupPC(%#x) = %d, want %d", tt.pc, got, tt.want) + } + } +} + +func BenchmarkLookupPCRandom(b *testing.B) { + entries := make([]uint64, 8192) + for i := range entries { + entries[i] = 0x100000 + uint64(i)*37 + } + index := BuildPCIndex(entries) + var sum int + for i := 0; i < b.N; i++ { + pc := entries[(i*1103515245+12345)&(len(entries)-1)] + uint64(i&31) + sum += LookupPC(entries, index, pc) + } + if sum == 0 { + b.Fatal(sum) + } +} + +func collisionPair(t *testing.T) (string, string) { + t.Helper() + const mask = uint32(3) + seen := make(map[uint32]string) + for i := 0; i < 100; i++ { + s := string(rune('a' + i)) + slot := HashString(s) & mask + if prev, ok := seen[slot]; ok { + return prev, s + } + seen[slot] = s + } + t.Fatal("failed to find hash collision") + return "", "" +} + +func legacySizeBytes(records []Record) int { + seen := make(map[string]bool) + stringsBytes := 1 + for _, rec := range records { + for _, s := range []string{rec.Symbol, rec.Name, rec.File} { + if s == "" || seen[s] { + continue + } + seen[s] = true + stringsBytes += len(s) + 1 + } + } + buckets := 1 + for buckets*3 < len(records)*4 { + buckets <<= 1 + } + return len(records)*20 + stringsBytes + buckets*4 +} diff --git a/internal/build/funcinfo_table.go b/internal/build/funcinfo_table.go new file mode 100644 index 0000000000..ea57e7a865 --- /dev/null +++ b/internal/build/funcinfo_table.go @@ -0,0 +1,961 @@ +/* + * 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 build + +import ( + "sort" + "strings" + + "github.com/xgo-dev/llvm" + + buildfuncinfo "github.com/goplus/llgo/internal/build/funcinfo" + llssa "github.com/goplus/llgo/ssa" +) + +const ( + funcInfoTableSymbol = "__llgo_funcinfo_table" + funcInfoCountSymbol = "__llgo_funcinfo_count" + funcInfoStringsSymbol = "__llgo_funcinfo_strings" + funcInfoStringOffsetsSymbol = "__llgo_funcinfo_string_offsets" + funcInfoStringCountSymbol = "__llgo_funcinfo_string_count" + funcInfoHashSymbol = "__llgo_funcinfo_hash" + funcInfoHashMaskSymbol = "__llgo_funcinfo_hash_mask" + funcInfoSymbolIndexSymbol = "__llgo_funcinfo_symbol_index" + funcInfoSymbolIndexCountSymbol = "__llgo_funcinfo_symbol_index_count" + funcInfoStubIndexesSymbol = "__llgo_funcinfo_stub_indexes" + funcInfoStubCountSymbol = "__llgo_funcinfo_stub_count" + funcInfoEntryStartPtrSymbol = "__llgo_funcinfo_entry_start" + funcInfoEntryEndPtrSymbol = "__llgo_funcinfo_entry_end" + funcInfoStubSiteStartPtrSymbol = "__llgo_funcinfo_stubsite_start" + funcInfoStubSiteEndPtrSymbol = "__llgo_funcinfo_stubsite_end" + pcLineTableSymbol = "__llgo_pcline_table" + pcLineCountSymbol = "__llgo_pcline_count" + pcSiteStartPtrSymbol = "__llgo_pcsite_start" + pcSiteEndPtrSymbol = "__llgo_pcsite_end" + funcInfoDataSymbol = "__llgo_funcinfo_table$data" + pcLineDataSymbol = "__llgo_pcline_table$data" + funcInfoStringsDataSymbol = "__llgo_funcinfo_strings$data" + funcInfoStringOffsetsDataSymbol = "__llgo_funcinfo_string_offsets$data" + funcInfoHashDataSymbol = "__llgo_funcinfo_hash$data" + funcInfoSymbolIndexDataSymbol = "__llgo_funcinfo_symbol_index$data" + funcInfoStubIndexesDataSymbol = "__llgo_funcinfo_stub_indexes$data" + closureStubPrefix = "__llgo_stub." +) + +type funcInfoRecord struct { + symbol string + name string + file string + line uint32 + column uint32 +} + +type pcLineRecord struct { + id uint64 + symbol string + file string + line uint32 + column uint32 +} + +type funcInfoStubRecord struct { + symbol string + funcIndex uint32 +} + +type funcInfoSymbolIndexRecord struct { + symbolID uint64 + funcIndex uint32 +} + +func collectFuncInfo(pkgs []Package) []funcInfoRecord { + seen := make(map[string]funcInfoRecord) + for _, pkg := range pkgs { + if pkg == nil || pkg.LPkg == nil { + continue + } + for _, rec := range readFuncInfo(pkg.LPkg.Module()) { + if rec.symbol == "" { + continue + } + if _, ok := seen[rec.symbol]; !ok { + seen[rec.symbol] = rec + } + } + } + if len(seen) == 0 { + return nil + } + out := make([]funcInfoRecord, 0, len(seen)) + for _, rec := range seen { + out = append(out, rec) + } + sort.Slice(out, func(i, j int) bool { + return out[i].symbol < out[j].symbol + }) + return out +} + +func collectPCLineInfo(pkgs []Package) []pcLineRecord { + var out []pcLineRecord + seen := make(map[uint64]none) + for _, pkg := range pkgs { + if pkg == nil || pkg.LPkg == nil { + continue + } + for _, rec := range readPCLineInfo(pkg.LPkg.Module()) { + if rec.id == 0 || rec.symbol == "" { + continue + } + if _, ok := seen[rec.id]; ok { + continue + } + seen[rec.id] = none{} + out = append(out, rec) + } + } + sort.Slice(out, func(i, j int) bool { + if out[i].symbol != out[j].symbol { + return out[i].symbol < out[j].symbol + } + if out[i].line != out[j].line { + return out[i].line < out[j].line + } + return out[i].id < out[j].id + }) + return out +} + +func collectFuncInfoStubRecords(pkgs []Package, records []funcInfoRecord) []funcInfoStubRecord { + if len(records) == 0 { + return nil + } + recordBySymbol := make(map[string]uint32, len(records)) + for i, rec := range records { + if rec.symbol != "" { + recordBySymbol[rec.symbol] = uint32(i + 1) + } + } + seen := make(map[string]funcInfoStubRecord) + for _, pkg := range pkgs { + if pkg == nil || pkg.LPkg == nil { + continue + } + fn := pkg.LPkg.Module().FirstFunction() + for !fn.IsNil() { + if fn.IsDeclaration() || fn.BasicBlocksCount() == 0 { + fn = llvm.NextFunction(fn) + continue + } + name := fn.Name() + if target, ok := strings.CutPrefix(name, closureStubPrefix); ok { + if idx := recordBySymbol[target]; idx != 0 { + seen[name] = funcInfoStubRecord{symbol: name, funcIndex: idx} + } + } + fn = llvm.NextFunction(fn) + } + } + if len(seen) == 0 { + return nil + } + out := make([]funcInfoStubRecord, 0, len(seen)) + for _, rec := range seen { + out = append(out, rec) + } + sort.Slice(out, func(i, j int) bool { + return out[i].symbol < out[j].symbol + }) + return out +} + +func collectFuncInfoSymbolIndexRecords(records []funcInfoRecord) []funcInfoSymbolIndexRecord { + if len(records) == 0 { + return nil + } + seen := make(map[uint64]uint32, len(records)) + for i, rec := range records { + if rec.symbol == "" { + continue + } + id := funcInfoSymbolID(rec.symbol) + idx := uint32(i + 1) + if prev, ok := seen[id]; ok && prev != idx { + seen[id] = 0 + continue + } + seen[id] = idx + } + if len(seen) == 0 { + return nil + } + out := make([]funcInfoSymbolIndexRecord, 0, len(seen)) + for id, idx := range seen { + if id == 0 || idx == 0 { + continue + } + out = append(out, funcInfoSymbolIndexRecord{symbolID: id, funcIndex: idx}) + } + sort.Slice(out, func(i, j int) bool { + return out[i].symbolID < out[j].symbolID + }) + return out +} + +func prepareFuncInfoTableRecords(records []funcInfoRecord, liveSymbols map[string]none) []funcInfoRecord { + if len(records) == 0 { + return nil + } + // A nil liveSymbols means no post-DCE live symbol set is available yet. + // The current table is still DCE-compatible because it stores only strings, + // never function pointers or llvm.compiler.used references. Once the linker + // or an LTO hook exposes a live-symbol set, pass it here to drop metadata for + // functions removed by global DCE before materializing the runtime table. + if liveSymbols == nil { + return records + } + out := records[:0] + for _, rec := range records { + if _, ok := liveSymbols[rec.symbol]; ok { + out = append(out, rec) + } + } + if len(out) == 0 { + return nil + } + return out +} + +func readFuncInfo(mod llvm.Module) []funcInfoRecord { + rows := mod.NamedMetadataOperands(llssa.FuncInfoMetadataName) + if len(rows) == 0 { + return nil + } + out := make([]funcInfoRecord, 0, len(rows)) + for _, row := range rows { + fields := row.MDNodeOperands() + if len(fields) != 6 || fields[0].ZExtValue() != 1 { + continue + } + if !fields[1].IsAMDString() || !fields[2].IsAMDString() || !fields[3].IsAMDString() { + continue + } + out = append(out, funcInfoRecord{ + symbol: fields[1].MDString(), + name: fields[2].MDString(), + file: fields[3].MDString(), + line: uint32(fields[4].ZExtValue()), + column: uint32(fields[5].ZExtValue()), + }) + } + return out +} + +func readPCLineInfo(mod llvm.Module) []pcLineRecord { + rows := mod.NamedMetadataOperands(llssa.PCLineMetadataName) + if len(rows) == 0 { + return nil + } + out := make([]pcLineRecord, 0, len(rows)) + for _, row := range rows { + fields := row.MDNodeOperands() + if len(fields) != 6 || fields[0].ZExtValue() != 1 { + continue + } + if !fields[2].IsAMDString() || !fields[3].IsAMDString() { + continue + } + out = append(out, pcLineRecord{ + id: fields[1].ZExtValue(), + symbol: fields[2].MDString(), + file: fields[3].MDString(), + line: uint32(fields[4].ZExtValue()), + column: uint32(fields[5].ZExtValue()), + }) + } + return out +} + +func emitFuncInfoTable(ctx *context, pkg llssa.Package, records []funcInfoRecord, pcLines []pcLineRecord, stubRecords []funcInfoStubRecord) { + mod := pkg.Module() + llvmCtx := mod.Context() + i8Type := llvmCtx.Int8Type() + i16Type := llvmCtx.Int16Type() + i32Type := llvmCtx.Int32Type() + i64Type := llvmCtx.Int64Type() + countType := llvmCtx.IntType(ctx.prog.PointerSize() * 8) + recordType := llvmCtx.StructType([]llvm.Type{ + i16Type, + i16Type, + i16Type, + i16Type, + i16Type, + i16Type, + i32Type, + }, false) + pcLineRecordType := llvmCtx.StructType([]llvm.Type{ + i64Type, + i32Type, + i32Type, + i32Type, + }, false) + symbolIndexRecordType := llvmCtx.StructType([]llvm.Type{ + i64Type, + i32Type, + }, false) + funcEntryRecordType := llvmCtx.StructType([]llvm.Type{ + llvm.PointerType(i8Type, 0), + i64Type, + }, false) + stubSiteRecordType := llvmCtx.StructType([]llvm.Type{ + llvm.PointerType(i8Type, 0), + i64Type, + }, false) + pcSiteRecordType := llvmCtx.StructType([]llvm.Type{ + llvm.PointerType(i8Type, 0), + i64Type, + }, false) + + tablePtr := llvm.AddGlobal(mod, llvm.PointerType(recordType, 0), funcInfoTableSymbol) + pcLinePtr := llvm.AddGlobal(mod, llvm.PointerType(pcLineRecordType, 0), pcLineTableSymbol) + pcSiteStartPtr := llvm.AddGlobal(mod, llvm.PointerType(pcSiteRecordType, 0), pcSiteStartPtrSymbol) + pcSiteEndPtr := llvm.AddGlobal(mod, llvm.PointerType(pcSiteRecordType, 0), pcSiteEndPtrSymbol) + entryStartPtr := llvm.AddGlobal(mod, llvm.PointerType(funcEntryRecordType, 0), funcInfoEntryStartPtrSymbol) + entryEndPtr := llvm.AddGlobal(mod, llvm.PointerType(funcEntryRecordType, 0), funcInfoEntryEndPtrSymbol) + stubSiteStartPtr := llvm.AddGlobal(mod, llvm.PointerType(stubSiteRecordType, 0), funcInfoStubSiteStartPtrSymbol) + stubSiteEndPtr := llvm.AddGlobal(mod, llvm.PointerType(stubSiteRecordType, 0), funcInfoStubSiteEndPtrSymbol) + stringsPtr := llvm.AddGlobal(mod, llvm.PointerType(i8Type, 0), funcInfoStringsSymbol) + stringOffsetsPtr := llvm.AddGlobal(mod, llvm.PointerType(i32Type, 0), funcInfoStringOffsetsSymbol) + stringCount := llvm.AddGlobal(mod, countType, funcInfoStringCountSymbol) + hashPtr := llvm.AddGlobal(mod, llvm.PointerType(i16Type, 0), funcInfoHashSymbol) + symbolIndexPtr := llvm.AddGlobal(mod, llvm.PointerType(symbolIndexRecordType, 0), funcInfoSymbolIndexSymbol) + count := llvm.AddGlobal(mod, countType, funcInfoCountSymbol) + symbolIndexCount := llvm.AddGlobal(mod, countType, funcInfoSymbolIndexCountSymbol) + stubIndexesPtr := llvm.AddGlobal(mod, llvm.PointerType(i32Type, 0), funcInfoStubIndexesSymbol) + stubCount := llvm.AddGlobal(mod, countType, funcInfoStubCountSymbol) + pcLineCount := llvm.AddGlobal(mod, countType, pcLineCountSymbol) + hashMask := llvm.AddGlobal(mod, countType, funcInfoHashMaskSymbol) + if len(records) == 0 && len(pcLines) == 0 { + tablePtr.SetInitializer(llvm.ConstPointerNull(tablePtr.GlobalValueType())) + pcLinePtr.SetInitializer(llvm.ConstPointerNull(pcLinePtr.GlobalValueType())) + pcSiteStartPtr.SetInitializer(llvm.ConstPointerNull(pcSiteStartPtr.GlobalValueType())) + pcSiteEndPtr.SetInitializer(llvm.ConstPointerNull(pcSiteEndPtr.GlobalValueType())) + entryStartPtr.SetInitializer(llvm.ConstPointerNull(entryStartPtr.GlobalValueType())) + entryEndPtr.SetInitializer(llvm.ConstPointerNull(entryEndPtr.GlobalValueType())) + stubSiteStartPtr.SetInitializer(llvm.ConstPointerNull(stubSiteStartPtr.GlobalValueType())) + stubSiteEndPtr.SetInitializer(llvm.ConstPointerNull(stubSiteEndPtr.GlobalValueType())) + stringsPtr.SetInitializer(llvm.ConstPointerNull(stringsPtr.GlobalValueType())) + stringOffsetsPtr.SetInitializer(llvm.ConstPointerNull(stringOffsetsPtr.GlobalValueType())) + stringCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + hashPtr.SetInitializer(llvm.ConstPointerNull(hashPtr.GlobalValueType())) + symbolIndexPtr.SetInitializer(llvm.ConstPointerNull(symbolIndexPtr.GlobalValueType())) + count.SetInitializer(llvm.ConstInt(countType, 0, false)) + symbolIndexCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + stubIndexesPtr.SetInitializer(llvm.ConstPointerNull(stubIndexesPtr.GlobalValueType())) + stubCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + pcLineCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + hashMask.SetInitializer(llvm.ConstInt(countType, 0, false)) + return + } + + encoded, err := buildfuncinfo.EncodeWithPCLines(toFuncInfoRecords(records), toPCLineRecords(pcLines)) + if err != nil { + panic(err) + } + if len(encoded.Records) == 0 && len(encoded.PCLines) == 0 { + tablePtr.SetInitializer(llvm.ConstPointerNull(tablePtr.GlobalValueType())) + pcLinePtr.SetInitializer(llvm.ConstPointerNull(pcLinePtr.GlobalValueType())) + pcSiteStartPtr.SetInitializer(llvm.ConstPointerNull(pcSiteStartPtr.GlobalValueType())) + pcSiteEndPtr.SetInitializer(llvm.ConstPointerNull(pcSiteEndPtr.GlobalValueType())) + entryStartPtr.SetInitializer(llvm.ConstPointerNull(entryStartPtr.GlobalValueType())) + entryEndPtr.SetInitializer(llvm.ConstPointerNull(entryEndPtr.GlobalValueType())) + stubSiteStartPtr.SetInitializer(llvm.ConstPointerNull(stubSiteStartPtr.GlobalValueType())) + stubSiteEndPtr.SetInitializer(llvm.ConstPointerNull(stubSiteEndPtr.GlobalValueType())) + stringsPtr.SetInitializer(llvm.ConstPointerNull(stringsPtr.GlobalValueType())) + stringOffsetsPtr.SetInitializer(llvm.ConstPointerNull(stringOffsetsPtr.GlobalValueType())) + stringCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + hashPtr.SetInitializer(llvm.ConstPointerNull(hashPtr.GlobalValueType())) + symbolIndexPtr.SetInitializer(llvm.ConstPointerNull(symbolIndexPtr.GlobalValueType())) + count.SetInitializer(llvm.ConstInt(countType, 0, false)) + symbolIndexCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + stubIndexesPtr.SetInitializer(llvm.ConstPointerNull(stubIndexesPtr.GlobalValueType())) + stubCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + pcLineCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + hashMask.SetInitializer(llvm.ConstInt(countType, 0, false)) + return + } + + values := make([]llvm.Value, 0, len(encoded.Records)) + for _, rec := range encoded.Records { + values = append(values, llvm.ConstNamedStruct(recordType, []llvm.Value{ + llvm.ConstInt(i16Type, uint64(rec.SymbolPkg), false), + llvm.ConstInt(i16Type, uint64(rec.SymbolName), false), + llvm.ConstInt(i16Type, uint64(rec.NamePkg), false), + llvm.ConstInt(i16Type, uint64(rec.NameName), false), + llvm.ConstInt(i16Type, uint64(rec.FileRoot), false), + llvm.ConstInt(i16Type, uint64(rec.FileName), false), + llvm.ConstInt(i32Type, uint64(rec.Line), false), + })) + } + arrayType := llvm.ArrayType(recordType, len(values)) + data := llvm.AddGlobal(mod, arrayType, funcInfoDataSymbol) + data.SetInitializer(llvm.ConstArray(recordType, values)) + data.SetLinkage(llvm.PrivateLinkage) + data.SetGlobalConstant(true) + data.SetUnnamedAddr(true) + data.SetAlignment(4) + + pcLineValues := make([]llvm.Value, 0, len(encoded.PCLines)) + for _, rec := range encoded.PCLines { + pcLineValues = append(pcLineValues, llvm.ConstNamedStruct(pcLineRecordType, []llvm.Value{ + llvm.ConstInt(i64Type, rec.ID, false), + llvm.ConstInt(i32Type, uint64(rec.Func), false), + llvm.ConstInt(i32Type, uint64(rec.File), false), + llvm.ConstInt(i32Type, uint64(rec.Line), false), + })) + } + if len(pcLineValues) == 0 { + pcLinePtr.SetInitializer(llvm.ConstPointerNull(pcLinePtr.GlobalValueType())) + pcLineCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + pcSiteStartPtr.SetInitializer(llvm.ConstPointerNull(pcSiteStartPtr.GlobalValueType())) + pcSiteEndPtr.SetInitializer(llvm.ConstPointerNull(pcSiteEndPtr.GlobalValueType())) + } else { + pcLineArrayType := llvm.ArrayType(pcLineRecordType, len(pcLineValues)) + pcLineData := llvm.AddGlobal(mod, pcLineArrayType, pcLineDataSymbol) + pcLineData.SetInitializer(llvm.ConstArray(pcLineRecordType, pcLineValues)) + pcLineData.SetLinkage(llvm.PrivateLinkage) + pcLineData.SetGlobalConstant(true) + pcLineData.SetUnnamedAddr(true) + pcLineData.SetAlignment(8) + pcLinePtr.SetInitializer(llvm.ConstInBoundsGEP(pcLineArrayType, pcLineData, []llvm.Value{ + llvm.ConstInt(countType, 0, false), + llvm.ConstInt(countType, 0, false), + })) + pcLineCount.SetInitializer(llvm.ConstInt(countType, uint64(len(encoded.PCLines)), false)) + if shouldEmitRuntimeSites(ctx) { + startName, endName := pcLineSiteSectionInfo.boundary(shouldEmitRuntimeMachOSites(ctx)) + pcSiteStart := llvm.AddGlobal(mod, pcSiteRecordType, startName) + pcSiteEnd := llvm.AddGlobal(mod, pcSiteRecordType, endName) + pcSiteStartPtr.SetInitializer(pcSiteStart) + pcSiteEndPtr.SetInitializer(pcSiteEnd) + } else { + pcSiteStartPtr.SetInitializer(llvm.ConstPointerNull(pcSiteStartPtr.GlobalValueType())) + pcSiteEndPtr.SetInitializer(llvm.ConstPointerNull(pcSiteEndPtr.GlobalValueType())) + } + } + machOSites := shouldEmitRuntimeMachOSites(ctx) + emitSites := shouldEmitRuntimeSites(ctx) + emitEntrySites := shouldEmitRuntimeEntryELFSites(ctx) && len(encoded.Records) != 0 + emitStubSites := shouldEmitRuntimeStubELFSites(ctx) + emitRuntimeFuncInfoSites(mod, ctx.prog.PointerSize(), machOSites, emitSites && len(pcLineValues) != 0, emitEntrySites, emitStubSites && len(stubRecords) != 0) + if emitEntrySites { + startName, endName := entrySiteSectionInfo.boundary(machOSites) + entryStart := llvm.AddGlobal(mod, funcEntryRecordType, startName) + entryEnd := llvm.AddGlobal(mod, funcEntryRecordType, endName) + entryStartPtr.SetInitializer(entryStart) + entryEndPtr.SetInitializer(entryEnd) + } else { + entryStartPtr.SetInitializer(llvm.ConstPointerNull(entryStartPtr.GlobalValueType())) + entryEndPtr.SetInitializer(llvm.ConstPointerNull(entryEndPtr.GlobalValueType())) + } + if emitStubSites && len(stubRecords) != 0 { + startName, endName := stubSiteSectionInfo.boundary(machOSites) + stubSiteStart := llvm.AddGlobal(mod, stubSiteRecordType, startName) + stubSiteEnd := llvm.AddGlobal(mod, stubSiteRecordType, endName) + stubSiteStartPtr.SetInitializer(stubSiteStart) + stubSiteEndPtr.SetInitializer(stubSiteEnd) + } else { + stubSiteStartPtr.SetInitializer(llvm.ConstPointerNull(stubSiteStartPtr.GlobalValueType())) + stubSiteEndPtr.SetInitializer(llvm.ConstPointerNull(stubSiteEndPtr.GlobalValueType())) + } + + stringArrayType := llvm.ArrayType(i8Type, len(encoded.Strings)) + stringData := llvm.AddGlobal(mod, stringArrayType, funcInfoStringsDataSymbol) + stringData.SetInitializer(llvmCtx.ConstString(string(encoded.Strings), false)) + stringData.SetLinkage(llvm.PrivateLinkage) + stringData.SetGlobalConstant(true) + stringData.SetUnnamedAddr(true) + stringData.SetAlignment(1) + + stringOffsetValues := make([]llvm.Value, 0, len(encoded.StringOffsets)) + for _, off := range encoded.StringOffsets { + stringOffsetValues = append(stringOffsetValues, llvm.ConstInt(i32Type, uint64(off), false)) + } + stringOffsetsArrayType := llvm.ArrayType(i32Type, len(stringOffsetValues)) + stringOffsetsData := llvm.AddGlobal(mod, stringOffsetsArrayType, funcInfoStringOffsetsDataSymbol) + stringOffsetsData.SetInitializer(llvm.ConstArray(i32Type, stringOffsetValues)) + stringOffsetsData.SetLinkage(llvm.PrivateLinkage) + stringOffsetsData.SetGlobalConstant(true) + stringOffsetsData.SetUnnamedAddr(true) + stringOffsetsData.SetAlignment(4) + + tablePtr.SetInitializer(llvm.ConstInBoundsGEP(arrayType, data, []llvm.Value{ + llvm.ConstInt(countType, 0, false), + llvm.ConstInt(countType, 0, false), + })) + stringsPtr.SetInitializer(llvm.ConstInBoundsGEP(stringArrayType, stringData, []llvm.Value{ + llvm.ConstInt(countType, 0, false), + llvm.ConstInt(countType, 0, false), + })) + stringOffsetsPtr.SetInitializer(llvm.ConstInBoundsGEP(stringOffsetsArrayType, stringOffsetsData, []llvm.Value{ + llvm.ConstInt(countType, 0, false), + llvm.ConstInt(countType, 0, false), + })) + stringCount.SetInitializer(llvm.ConstInt(countType, uint64(len(encoded.StringOffsets)), false)) + if len(encoded.Hash) == 0 { + hashPtr.SetInitializer(llvm.ConstPointerNull(hashPtr.GlobalValueType())) + hashMask.SetInitializer(llvm.ConstInt(countType, 0, false)) + } else { + hashValues := make([]llvm.Value, 0, len(encoded.Hash)) + for _, idx := range encoded.Hash { + hashValues = append(hashValues, llvm.ConstInt(i16Type, uint64(idx), false)) + } + hashArrayType := llvm.ArrayType(i16Type, len(hashValues)) + hashData := llvm.AddGlobal(mod, hashArrayType, funcInfoHashDataSymbol) + hashData.SetInitializer(llvm.ConstArray(i16Type, hashValues)) + hashData.SetLinkage(llvm.PrivateLinkage) + hashData.SetGlobalConstant(true) + hashData.SetUnnamedAddr(true) + hashData.SetAlignment(2) + hashPtr.SetInitializer(llvm.ConstInBoundsGEP(hashArrayType, hashData, []llvm.Value{ + llvm.ConstInt(countType, 0, false), + llvm.ConstInt(countType, 0, false), + })) + hashMask.SetInitializer(llvm.ConstInt(countType, uint64(len(encoded.Hash)-1), false)) + } + count.SetInitializer(llvm.ConstInt(countType, uint64(len(encoded.Records)), false)) + symbolIndexRecords := collectFuncInfoSymbolIndexRecords(records) + symbolIndexValues := make([]llvm.Value, 0, len(symbolIndexRecords)) + for _, rec := range symbolIndexRecords { + if rec.funcIndex == 0 || int(rec.funcIndex) > len(encoded.Records) { + continue + } + symbolIndexValues = append(symbolIndexValues, llvm.ConstNamedStruct(symbolIndexRecordType, []llvm.Value{ + llvm.ConstInt(i64Type, rec.symbolID, false), + llvm.ConstInt(i32Type, uint64(rec.funcIndex), false), + })) + } + if len(symbolIndexValues) == 0 { + symbolIndexPtr.SetInitializer(llvm.ConstPointerNull(symbolIndexPtr.GlobalValueType())) + symbolIndexCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + } else { + symbolIndexArrayType := llvm.ArrayType(symbolIndexRecordType, len(symbolIndexValues)) + symbolIndexData := llvm.AddGlobal(mod, symbolIndexArrayType, funcInfoSymbolIndexDataSymbol) + symbolIndexData.SetInitializer(llvm.ConstArray(symbolIndexRecordType, symbolIndexValues)) + symbolIndexData.SetLinkage(llvm.PrivateLinkage) + symbolIndexData.SetGlobalConstant(true) + symbolIndexData.SetUnnamedAddr(true) + symbolIndexData.SetAlignment(8) + symbolIndexPtr.SetInitializer(llvm.ConstInBoundsGEP(symbolIndexArrayType, symbolIndexData, []llvm.Value{ + llvm.ConstInt(countType, 0, false), + llvm.ConstInt(countType, 0, false), + })) + symbolIndexCount.SetInitializer(llvm.ConstInt(countType, uint64(len(symbolIndexValues)), false)) + } + stubIndexSeen := make(map[uint32]none, len(stubRecords)) + stubIndexValues := make([]llvm.Value, 0, len(stubRecords)) + for _, stub := range stubRecords { + idx := stub.funcIndex + if idx == 0 || int(idx) > len(encoded.Records) { + continue + } + if _, ok := stubIndexSeen[idx]; ok { + continue + } + stubIndexSeen[idx] = none{} + stubIndexValues = append(stubIndexValues, llvm.ConstInt(i32Type, uint64(idx), false)) + } + if len(stubIndexValues) == 0 { + stubIndexesPtr.SetInitializer(llvm.ConstPointerNull(stubIndexesPtr.GlobalValueType())) + stubCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + } else { + stubIndexArrayType := llvm.ArrayType(i32Type, len(stubIndexValues)) + stubIndexData := llvm.AddGlobal(mod, stubIndexArrayType, funcInfoStubIndexesDataSymbol) + stubIndexData.SetInitializer(llvm.ConstArray(i32Type, stubIndexValues)) + stubIndexData.SetLinkage(llvm.PrivateLinkage) + stubIndexData.SetGlobalConstant(true) + stubIndexData.SetUnnamedAddr(true) + stubIndexData.SetAlignment(4) + stubIndexesPtr.SetInitializer(llvm.ConstInBoundsGEP(stubIndexArrayType, stubIndexData, []llvm.Value{ + llvm.ConstInt(countType, 0, false), + llvm.ConstInt(countType, 0, false), + })) + stubCount.SetInitializer(llvm.ConstInt(countType, uint64(len(stubIndexValues)), false)) + } +} + +func shouldEmitRuntimeELFSites(ctx *context) bool { + return ctx != nil && + ctx.buildConf != nil && + ctx.buildConf.Goos == "linux" && + ctx.buildConf.Target == "" +} + +func shouldEmitRuntimeMachOSites(ctx *context) bool { + return ctx != nil && + ctx.buildConf != nil && + ctx.buildConf.Goos == "darwin" && + ctx.buildConf.Target == "" +} + +// shouldEmitRuntimeSites reports whether the target object format has a +// DCE-safe section story for metadata site records. ELF uses SHF_LINK_ORDER +// associated sections (honored by --gc-sections). Mach-O uses live_support +// sections: under ld64/lld -dead_strip a live_support atom survives only if +// the atom it references (the anchor inside the function body) is live, which +// is the same records-follow-function semantics. Sites are additionally +// gated per Program: debug builds keep the funcinfo tables but drop the +// body-embedded site records (see Program.EnableFuncInfoSites). +func shouldEmitRuntimeSites(ctx *context) bool { + if ctx == nil || ctx.prog == nil || !ctx.prog.FuncInfoSitesEnabled() { + return false + } + return shouldEmitRuntimeELFSites(ctx) || shouldEmitRuntimeMachOSites(ctx) +} + +func shouldEmitRuntimeStubELFSites(ctx *context) bool { + return shouldEmitRuntimeSites(ctx) +} + +func shouldEmitRuntimeEntryELFSites(ctx *context) bool { + return shouldEmitRuntimeSites(ctx) +} + +// siteSectionInfo names one metadata site section in both object formats. +// Mach-O section names are capped at 16 characters, hence the short forms. +type siteSectionInfo struct { + elf string + machO string +} + +var ( + entrySiteSectionInfo = siteSectionInfo{elf: "llgo_funcinfo_entry", machO: "__DATA,__llgo_fie"} + stubSiteSectionInfo = siteSectionInfo{elf: "llgo_funcinfo_stubsite", machO: "__DATA,__llgo_stub"} + pcLineSiteSectionInfo = siteSectionInfo{elf: "llgo_pcline", machO: "__DATA,__llgo_pcl"} +) + +func (s siteSectionInfo) push(machO bool, anchor string) string { + if machO { + return ".pushsection " + s.machO + ",regular,live_support" + } + return ".pushsection " + s.elf + ",\"ao\",@progbits," + anchor +} + +// recordSymbol returns the extra label line each Mach-O record needs: the +// lowercase-l linker-private symbol splits the section into one atom per +// record, so -dead_strip can drop records individually, and the symbol itself +// is discarded at link time. ELF needs nothing here. +func (s siteSectionInfo) recordSymbol(machO bool, kind string) string { + if !machO { + return "" + } + return "l_llgo_" + kind + "_rec_${:uid}:\n" +} + +func (s siteSectionInfo) retain(machO bool) string { + if machO { + return ".section " + s.machO + ",regular,live_support" + } + return ".section " + s.elf + ",\"aR\",@progbits" +} + +// retainSymbol returns the label lines that pin the zero record under +// -dead_strip on Mach-O; nothing references the zero record, so it must be a +// no_dead_strip atom for the section (and its boundary symbols) to survive. +func (s siteSectionInfo) retainSymbol(machO bool, kind string) string { + if !machO { + return "" + } + sym := "l_llgo_" + kind + "_zero" + return sym + ":\n.no_dead_strip " + sym + "\n" +} + +// boundary returns the linker-synthesized section boundary symbols: ELF +// __start_/__stop_ for C-identifier section names, ld64 section$start$/ +// section$end$ for Mach-O. +func (s siteSectionInfo) boundary(machO bool) (start, end string) { + if machO { + base := strings.Replace(s.machO, ",", "$", 1) + // The \x01 prefix makes LLVM emit the name verbatim. Without it the + // Mach-O mangler prepends an underscore and the linker no longer + // recognizes the exact section$start$SEG$SECT boundary spelling. + return "\x01section$start$" + base, "\x01section$end$" + base + } + return "__start_" + s.elf, "__stop_" + s.elf +} + +func siteAnchorLabel(machO bool, kind string) string { + if machO { + // Mach-O assembler-local labels use the plain "L" prefix. + return "Lllgo_" + kind + "_anchor_${:uid}" + } + return ".Lllgo_" + kind + "_anchor_${:uid}" +} + +func emitFuncInfoEntrySites(ctx *context, pkg llssa.Package) { + if !shouldEmitRuntimeEntryELFSites(ctx) || pkg == nil || !ctx.prog.FuncInfoMetadataEnabled() { + return + } + mod := pkg.Module() + records := readFuncInfo(mod) + if len(records) == 0 { + return + } + symbolIDs := make(map[string]uint64, len(records)) + for _, rec := range records { + if rec.symbol != "" { + symbolIDs[rec.symbol] = funcInfoSymbolID(rec.symbol) + } + } + if len(symbolIDs) == 0 { + return + } + // This is LLGo's DCE-safe substitute for the function PC list that Go's + // linker has while building pclntab. The inline-asm fragment lives in a + // section tied to the function body (SHF_LINK_ORDER on ELF; live_support + // on Mach-O), so dead functions do not leave stale entry records behind. + // Runtime still sorts these final PCs before building the Go-style + // findfunc bucket index, because LLVM IR generation does not know final + // linked text order. + // + // Known limitation: because the record is emitted inside the function + // body, LTO inlining duplicates it into every inline site, bloating the + // section (~4x on multipkg) and registering host-function PCs under the + // inlinee's symbol ID; the runtime only consults this table when native + // symbolization fails, which bounds the impact. Data-global alternatives + // were tried and do not work with the current LLVM semantics: !associated + // affects only linker GC, so IR-level GlobalDCE deletes every record; + // keeping records via llvm.compiler.used makes their function-address + // initializers pin dead functions alive; and noduplicate on the asm call + // blocks inlining outright. Deduplicating the section is therefore + // link-phase work and lands together with the final ftab generation. + machO := shouldEmitRuntimeMachOSites(ctx) + llvmCtx := mod.Context() + builder := llvmCtx.NewBuilder() + defer builder.Dispose() + asmType := llvm.FunctionType(llvmCtx.VoidType(), nil, false) + ptrDirective := ".quad" + align := "3" + if ctx.prog.PointerSize() == 4 { + ptrDirective = ".long" + align = "2" + } + for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { + if fn.IsDeclaration() || fn.BasicBlocksCount() == 0 { + continue + } + symbol := fn.Name() + symbolID := symbolIDs[symbol] + if symbolID == 0 { + continue + } + entry := fn.EntryBasicBlock() + if entry.IsNil() { + continue + } + first := entry.FirstInstruction() + if first.IsNil() { + builder.SetInsertPointAtEnd(entry) + } else { + builder.SetInsertPointBefore(first) + } + anchor := siteAnchorLabel(machO, "funcinfo_entry") + instruction := anchor + ":\n" + + entrySiteSectionInfo.push(machO, anchor) + "\n" + + ".p2align " + align + "\n" + + entrySiteSectionInfo.recordSymbol(machO, "funcinfo_entry") + + ptrDirective + " " + anchor + "\n" + + ".quad " + uint64Hex(symbolID) + "\n" + + ".popsection" + asm := llvm.InlineAsm(asmType, instruction, "", true, false, llvm.InlineAsmDialectATT, false) + builder.CreateCall(asmType, asm, nil, "") + } +} + +func emitFuncInfoStubSites(ctx *context, pkg llssa.Package) { + if !shouldEmitRuntimeStubELFSites(ctx) || pkg == nil || !ctx.prog.FuncInfoMetadataEnabled() { + return + } + machO := shouldEmitRuntimeMachOSites(ctx) + mod := pkg.Module() + llvmCtx := mod.Context() + builder := llvmCtx.NewBuilder() + defer builder.Dispose() + asmType := llvm.FunctionType(llvmCtx.VoidType(), nil, false) + ptrDirective := ".quad" + align := "3" + if ctx.prog.PointerSize() == 4 { + ptrDirective = ".long" + align = "2" + } + for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { + if fn.IsDeclaration() || fn.BasicBlocksCount() == 0 { + continue + } + symbol := fn.Name() + target, ok := strings.CutPrefix(symbol, closureStubPrefix) + if !ok || target == "" { + continue + } + entry := fn.EntryBasicBlock() + if entry.IsNil() { + continue + } + first := entry.FirstInstruction() + if first.IsNil() { + builder.SetInsertPointAtEnd(entry) + } else { + builder.SetInsertPointBefore(first) + } + anchor := siteAnchorLabel(machO, "funcinfo_stubsite") + instruction := anchor + ":\n" + + stubSiteSectionInfo.push(machO, anchor) + "\n" + + ".p2align " + align + "\n" + + stubSiteSectionInfo.recordSymbol(machO, "funcinfo_stubsite") + + ptrDirective + " " + anchor + "\n" + + ".quad " + uint64Hex(funcInfoSymbolID(target)) + "\n" + + ".popsection" + asm := llvm.InlineAsm(asmType, instruction, "", true, false, llvm.InlineAsmDialectATT, false) + builder.CreateCall(asmType, asm, nil, "") + } +} + +func funcInfoSymbolID(symbol string) uint64 { + const ( + offset = uint64(14695981039346656037) + prime = uint64(1099511628211) + ) + h := offset + for i := 0; i < len(symbol); i++ { + h ^= uint64(symbol[i]) + h *= prime + } + if h == 0 { + return 1 + } + return h +} + +func uint64Hex(v uint64) string { + const hexdigits = "0123456789abcdef" + var buf [18]byte + buf[0] = '0' + buf[1] = 'x' + for i := len(buf) - 1; i >= 2; i-- { + buf[i] = hexdigits[v&0xf] + v >>= 4 + } + return string(buf[:]) +} + +// emitRuntimeFuncInfoSites emits one zero record per used site section so the +// section always exists and the linker-synthesized boundary symbols resolve +// even when no package contributed records. Runtime skips zero records. +// funcInfoMetaRecordMagic marks the entry-section meta record consumed by +// internal/pclnpost ("LLGOMET1" little-endian). +const funcInfoMetaRecordMagic = uint64(0x3154454D4F474C4C) + +func emitRuntimeFuncInfoSites(mod llvm.Module, pointerSize int, machO bool, pcSite bool, entrySite bool, stubSite bool) { + if !pcSite && !entrySite && !stubSite { + return + } + ptrDirective := ".quad" + align := "3" + if pointerSize == 4 { + ptrDirective = ".long" + align = "2" + } + var asm strings.Builder + writeZeroRecord := func(info siteSectionInfo, kind string) { + asm.WriteString(info.retain(machO) + "\n") + asm.WriteString(".p2align " + align + "\n") + asm.WriteString(info.retainSymbol(machO, kind)) + asm.WriteString(ptrDirective + " 0\n") + asm.WriteString(".quad 0\n") + } + if pcSite { + writeZeroRecord(pcLineSiteSectionInfo, "pcline") + } + if entrySite { + writeZeroRecord(entrySiteSectionInfo, "funcinfo_entry") + // Meta records for the link-phase tool: relocations carrying the + // addresses of the symbol-index pointer global and its count global. + // Relocations are resolved by the linker regardless of what LTO + // internalization does to the symbol table, which is what keeps this + // reachable in +LTO binaries. The runtime skips all three rows: the + // first has pc==0 and the other two have symbolID==0. + idxSym, cntSym := funcInfoSymbolIndexSymbol, funcInfoSymbolIndexCountSymbol + if machO { + idxSym, cntSym = "_"+idxSym, "_"+cntSym + } + asm.WriteString(ptrDirective + " 0\n") + asm.WriteString(".quad " + uint64Hex(funcInfoMetaRecordMagic) + "\n") + asm.WriteString(ptrDirective + " " + idxSym + "\n") + asm.WriteString(".quad 0\n") + asm.WriteString(ptrDirective + " " + cntSym + "\n") + asm.WriteString(".quad 0\n") + } + if stubSite { + writeZeroRecord(stubSiteSectionInfo, "funcinfo_stubsite") + } + mod.SetInlineAsm(asm.String()) +} + +func asmQuoteELFSymbol(symbol string) string { + var b strings.Builder + b.Grow(len(symbol) + 2) + b.WriteByte('"') + for i := 0; i < len(symbol); i++ { + switch symbol[i] { + case '\\', '"': + b.WriteByte('\\') + case '$': + b.WriteByte('$') + } + b.WriteByte(symbol[i]) + } + b.WriteByte('"') + return b.String() +} + +func toFuncInfoRecords(records []funcInfoRecord) []buildfuncinfo.Record { + out := make([]buildfuncinfo.Record, len(records)) + for i, rec := range records { + out[i] = buildfuncinfo.Record{ + Symbol: rec.symbol, + Name: rec.name, + File: rec.file, + Line: rec.line, + Column: rec.column, + } + } + return out +} + +func toPCLineRecords(records []pcLineRecord) []buildfuncinfo.PCLineRecord { + out := make([]buildfuncinfo.PCLineRecord, len(records)) + for i, rec := range records { + out[i] = buildfuncinfo.PCLineRecord{ + ID: rec.id, + Symbol: rec.symbol, + File: rec.file, + Line: rec.line, + Column: rec.column, + } + } + return out +} diff --git a/internal/build/funcinfo_table_test.go b/internal/build/funcinfo_table_test.go new file mode 100644 index 0000000000..1c330d3e3f --- /dev/null +++ b/internal/build/funcinfo_table_test.go @@ -0,0 +1,495 @@ +/* + * 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 build + +import ( + "strings" + "testing" + + "github.com/xgo-dev/llvm" + + "github.com/goplus/llgo/internal/lto" + "github.com/goplus/llgo/internal/packages" + llssa "github.com/goplus/llgo/ssa" +) + +func TestFuncInfoTableMaterializesMetadataWithoutFunctionPointers(t *testing.T) { + prog := llssa.NewProgram(nil) + prog.EnableFuncInfoSites(true) + src := prog.NewPackage("example.com/p", "example.com/p") + src.EmitFuncInfo("example.com/p.live", "example.com/p.Live", "live.go", 17, 3) + src.EmitFuncInfo("example.com/p.live", "example.com/p.LiveDuplicate", "dup.go", 19, 1) + + records := collectFuncInfo([]Package{{LPkg: src}}) + if len(records) != 1 { + t.Fatalf("collectFuncInfo returned %d records, want 1", len(records)) + } + if got := records[0]; got.symbol != "example.com/p.live" || got.name != "example.com/p.Live" || got.file != "live.go" || got.line != 17 || got.column != 3 { + t.Fatalf("unexpected record: %+v", got) + } + + ctx := &context{ + prog: prog, + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "linux", + Goarch: "amd64", + }, + } + entry := genMainModule(ctx, llssa.PkgRuntime, &packages.Package{ + PkgPath: "example.com/main", + ExportFile: "main.a", + }, &genConfig{funcInfo: records}) + ir := entry.LPkg.String() + for _, want := range []string{ + "@__llgo_funcinfo_table = global ptr", + "@__llgo_pcline_table = global ptr null", + "@__llgo_pcsite_start = global ptr null", + "@__llgo_pcsite_end = global ptr null", + "@__llgo_funcinfo_strings = global ptr", + "@__llgo_funcinfo_string_offsets = global ptr", + "@__llgo_funcinfo_string_count = global i64 5", + "@__llgo_funcinfo_hash = global ptr", + "@__llgo_funcinfo_symbol_index = global ptr", + "@__llgo_funcinfo_count = global i64 1", + "@__llgo_funcinfo_symbol_index_count = global i64 1", + "@__llgo_funcinfo_entry_start = global ptr @__start_llgo_funcinfo_entry", + "@__llgo_funcinfo_entry_end = global ptr @__stop_llgo_funcinfo_entry", + "@__llgo_funcinfo_stub_indexes = global ptr null", + "@__llgo_funcinfo_stub_count = global i64 0", + "@__llgo_pcline_count = global i64 0", + "@__llgo_funcinfo_hash_mask = global i64 1", + "module asm \".section llgo_funcinfo_entry", + `@"__llgo_funcinfo_table$data" = private unnamed_addr constant [1 x { i16, i16, i16, i16, i16, i16, i32 }]`, + `@"__llgo_funcinfo_string_offsets$data" = private unnamed_addr constant`, + `@"__llgo_funcinfo_hash$data" = private unnamed_addr constant [2 x i16]`, + `@"__llgo_funcinfo_symbol_index$data" = private unnamed_addr constant [1 x { i64, i32 }]`, + `example.com/p\00`, + `live\00`, + `Live\00`, + `live.go\00`, + "i32 17", + } { + if !strings.Contains(ir, want) { + t.Fatalf("funcinfo table IR missing %q:\n%s", want, ir) + } + } + if strings.Contains(ir, `ptr @"example.com/p.live"`) { + t.Fatalf("funcinfo table must not reference function pointers:\n%s", ir) + } +} + +func TestFuncInfoTableMaterializesEntrySites(t *testing.T) { + prog := llssa.NewProgram(nil) + src := prog.NewPackage("example.com/p", "example.com/p") + src.EmitFuncInfo("example.com/p.live", "example.com/p.Live", "live.go", 17, 3) + src.EmitFuncInfo("example.com/p.missing", "example.com/p.Missing", "missing.go", 19, 1) + liveFn := src.NewFunc("example.com/p.live", llssa.NoArgsNoRet, llssa.InC) + liveFn.MakeBody(1).Return() + otherFn := src.NewFunc("example.com/p.other", llssa.NoArgsNoRet, llssa.InC) + otherFn.MakeBody(1).Return() + ctx := &context{ + prog: prog, + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "linux", + Goarch: "amd64", + }, + } + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + emitFuncInfoEntrySites(ctx, src) + srcIR := src.String() + for _, want := range []string{ + "call void asm sideeffect", + ".pushsection llgo_funcinfo_entry", + ".Lllgo_funcinfo_entry_anchor_", + ".quad .Lllgo_funcinfo_entry_anchor_", + ".quad 0x", + } { + if !strings.Contains(srcIR, want) { + t.Fatalf("package entry site IR missing %q:\n%s", want, srcIR) + } + } + for _, bad := range []string{ + `.quad \22example.com/p.live\22`, + `.quad \22example.com/p.other\22`, + `.quad \22example.com/p.missing\22`, + } { + if strings.Contains(srcIR, bad) { + t.Fatalf("package entry site IR should not contain %q:\n%s", bad, srcIR) + } + } + + records := collectFuncInfo([]Package{{LPkg: src}}) + entry := genMainModule(ctx, llssa.PkgRuntime, &packages.Package{ + PkgPath: "example.com/main", + ExportFile: "main.a", + }, &genConfig{funcInfo: records}) + ir := entry.LPkg.String() + for _, want := range []string{ + "@__llgo_funcinfo_entry_start = global ptr @__start_llgo_funcinfo_entry", + "@__llgo_funcinfo_entry_end = global ptr @__stop_llgo_funcinfo_entry", + "module asm \".section llgo_funcinfo_entry", + } { + if !strings.Contains(ir, want) { + t.Fatalf("funcinfo entry table IR missing %q:\n%s", want, ir) + } + } + + ltoCtx := &context{ + prog: prog, + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "linux", + Goarch: "amd64", + LTO: lto.Full, + }, + } + ltoEntry := genMainModule(ltoCtx, llssa.PkgRuntime, &packages.Package{ + PkgPath: "example.com/main", + ExportFile: "main.a", + }, &genConfig{funcInfo: records}) + ltoIR := ltoEntry.LPkg.String() + for _, want := range []string{ + "@__llgo_funcinfo_entry_start = global ptr @__start_llgo_funcinfo_entry", + "@__llgo_funcinfo_entry_end = global ptr @__stop_llgo_funcinfo_entry", + "module asm \".section llgo_funcinfo_entry", + } { + if !strings.Contains(ltoIR, want) { + t.Fatalf("full LTO funcinfo table IR missing %q:\n%s", want, ltoIR) + } + } +} + +func TestFuncInfoTableSitesDisabledKeepsTables(t *testing.T) { + prog := llssa.NewProgram(nil) + src := prog.NewPackage("example.com/p", "example.com/p") + src.EmitFuncInfo("example.com/p.live", "example.com/p.Live", "live.go", 17, 3) + src.EmitPCLineInfo(0x1234, "example.com/p.live", "call.go", 23, 5) + liveFn := src.NewFunc("example.com/p.live", llssa.NoArgsNoRet, llssa.InC) + liveFn.MakeBody(1).Return() + ctx := &context{ + prog: prog, + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "linux", + Goarch: "amd64", + }, + } + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(false) + + emitFuncInfoEntrySites(ctx, src) + emitFuncInfoStubSites(ctx, src) + srcIR := src.String() + for _, bad := range []string{"llgo_funcinfo_entry", "llgo_funcinfo_stubsite", "call void asm sideeffect"} { + if strings.Contains(srcIR, bad) { + t.Fatalf("sites disabled: package IR should not contain %q:\n%s", bad, srcIR) + } + } + + records := collectFuncInfo([]Package{{LPkg: src}}) + pcLines := collectPCLineInfo([]Package{{LPkg: src}}) + entry := genMainModule(ctx, llssa.PkgRuntime, &packages.Package{ + PkgPath: "example.com/main", + ExportFile: "main.a", + }, &genConfig{funcInfo: records, pcLineInfo: pcLines}) + ir := entry.LPkg.String() + // The metadata tables must still materialize... + for _, want := range []string{ + "@__llgo_funcinfo_table = global ptr", + "@__llgo_funcinfo_count = global", + "@__llgo_pcline_table = global ptr", + } { + if !strings.Contains(ir, want) { + t.Fatalf("sites disabled: funcinfo table IR missing %q:\n%s", want, ir) + } + } + // ...while the site sections and their boundary symbols must not. + for _, bad := range []string{ + "@__start_llgo_funcinfo_entry", + "@__start_llgo_funcinfo_stubsite", + "@__start_llgo_pcline", + "module asm \".section llgo_", + } { + if strings.Contains(ir, bad) { + t.Fatalf("sites disabled: funcinfo table IR should not contain %q:\n%s", bad, ir) + } + } +} + +func TestFuncInfoTableMaterializesClosureStubIndexes(t *testing.T) { + prog := llssa.NewProgram(nil) + src := prog.NewPackage("example.com/p", "example.com/p") + src.EmitFuncInfo("example.com/p.live", "example.com/p.Live", "live.go", 17, 3) + src.EmitFuncInfo("example.com/p.other", "example.com/p.Other", "other.go", 23, 1) + stubFn := src.NewFunc(closureStubPrefix+"example.com/p.live", llssa.NoArgsNoRet, llssa.InC) + stubFn.MakeBody(1).Return() + ctx := &context{ + prog: prog, + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "linux", + Goarch: "amd64", + }, + } + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + emitFuncInfoStubSites(ctx, src) + srcIR := src.String() + for _, want := range []string{ + "call void asm sideeffect", + ".pushsection llgo_funcinfo_stubsite", + ".Lllgo_funcinfo_stubsite_anchor_", + ".quad .Lllgo_funcinfo_stubsite_anchor_", + ".quad 0x", + } { + if !strings.Contains(srcIR, want) { + t.Fatalf("package stub site IR missing %q:\n%s", want, srcIR) + } + } + if strings.Contains(srcIR, `.quad \22__llgo_stub.example.com/p.live\22`) { + t.Fatalf("package stub site must not reference stub function symbols:\n%s", srcIR) + } + + records := collectFuncInfo([]Package{{LPkg: src}}) + stubs := collectFuncInfoStubRecords([]Package{{LPkg: src}}, records) + if len(stubs) != 1 || records[stubs[0].funcIndex-1].symbol != "example.com/p.live" || + stubs[0].symbol != closureStubPrefix+"example.com/p.live" { + t.Fatalf("stub indexes = %+v for records %+v, want live", stubs, records) + } + + entry := genMainModule(ctx, llssa.PkgRuntime, &packages.Package{ + PkgPath: "example.com/main", + ExportFile: "main.a", + }, &genConfig{funcInfo: records, funcInfoStubs: stubs}) + ir := entry.LPkg.String() + for _, want := range []string{ + "@__llgo_funcinfo_stub_indexes = global ptr", + "@__llgo_funcinfo_stub_count = global i64 1", + "@__llgo_funcinfo_symbol_index = global ptr", + "@__llgo_funcinfo_symbol_index_count = global i64 2", + "@__llgo_funcinfo_stubsite_start = global ptr @__start_llgo_funcinfo_stubsite", + "@__llgo_funcinfo_stubsite_end = global ptr @__stop_llgo_funcinfo_stubsite", + `@"__llgo_funcinfo_stub_indexes$data" = private unnamed_addr constant [1 x i32]`, + "@__llgo_funcinfo_count = global i64 2", + "module asm \".section llgo_funcinfo_stubsite", + ".quad 0", + } { + if !strings.Contains(ir, want) { + t.Fatalf("funcinfo stub index table IR missing %q:\n%s", want, ir) + } + } + if strings.Contains(ir, closureStubPrefix+"example.com/p.live\\00") { + t.Fatalf("stub index table should not add stub symbol strings:\n%s", ir) + } + + ltoCtx := &context{ + prog: prog, + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "linux", + Goarch: "amd64", + LTO: lto.Full, + }, + } + ltoEntry := genMainModule(ltoCtx, llssa.PkgRuntime, &packages.Package{ + PkgPath: "example.com/main", + ExportFile: "main.a", + }, &genConfig{funcInfo: records, funcInfoStubs: stubs}) + ltoIR := ltoEntry.LPkg.String() + for _, want := range []string{ + "@__llgo_funcinfo_stubsite_start = global ptr @__start_llgo_funcinfo_stubsite", + "@__llgo_funcinfo_stubsite_end = global ptr @__stop_llgo_funcinfo_stubsite", + "module asm \".section llgo_funcinfo_stubsite", + } { + if !strings.Contains(ltoIR, want) { + t.Fatalf("full LTO funcinfo stub site table IR missing %q:\n%s", want, ltoIR) + } + } +} + +func TestFuncInfoTableMaterializesPCLineMetadata(t *testing.T) { + prog := llssa.NewProgram(nil) + prog.EnableFuncInfoSites(true) + src := prog.NewPackage("example.com/p", "example.com/p") + src.EmitFuncInfo("example.com/p.live", "example.com/p.Live", "live.go", 17, 3) + src.EmitPCLineInfo(0x1234, "example.com/p.live", "call.go", 23, 5) + src.EmitPCLineInfo(0x5678, "example.com/p.missing", "missing.go", 99, 1) + + records := collectFuncInfo([]Package{{LPkg: src}}) + pcLines := collectPCLineInfo([]Package{{LPkg: src}}) + if len(records) != 1 { + t.Fatalf("collectFuncInfo returned %d records, want 1", len(records)) + } + if len(pcLines) != 2 { + t.Fatalf("collectPCLineInfo returned %d records, want 2", len(pcLines)) + } + + ctx := &context{ + prog: prog, + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "linux", + Goarch: "amd64", + }, + } + entry := genMainModule(ctx, llssa.PkgRuntime, &packages.Package{ + PkgPath: "example.com/main", + ExportFile: "main.a", + }, &genConfig{funcInfo: records, pcLineInfo: pcLines}) + ir := entry.LPkg.String() + for _, want := range []string{ + "@__llgo_pcline_table = global ptr", + "@__llgo_pcsite_start = global ptr @__start_llgo_pcline", + "@__llgo_pcsite_end = global ptr @__stop_llgo_pcline", + "@__llgo_pcline_count = global i64 1", + "@__llgo_funcinfo_string_count = global i64 6", + "module asm \".section llgo_pcline", + `@"__llgo_pcline_table$data" = private unnamed_addr constant [1 x { i64, i32, i32, i32 }]`, + "i64 4660", + "i32 23", + `call.go\00`, + } { + if !strings.Contains(ir, want) { + t.Fatalf("pcline table IR missing %q:\n%s", want, ir) + } + } + if strings.Contains(ir, "missing.go") || strings.Contains(ir, "i64 22136") { + t.Fatalf("pcline table should drop records without matching function metadata:\n%s", ir) + } + if strings.Contains(ir, `ptr @"example.com/p.live"`) { + t.Fatalf("pcline table must not reference function pointers:\n%s", ir) + } +} + +func TestPrepareFuncInfoTableRecordsFiltersLiveSymbols(t *testing.T) { + records := []funcInfoRecord{ + {symbol: "dead", name: "dead"}, + {symbol: "live", name: "live"}, + } + if got := prepareFuncInfoTableRecords(nil, nil); got != nil { + t.Fatalf("empty records = %+v, want nil", got) + } + if got := prepareFuncInfoTableRecords(records, nil); len(got) != 2 { + t.Fatalf("nil live set kept %d records, want 2", len(got)) + } + got := prepareFuncInfoTableRecords(records, map[string]none{"live": {}}) + if len(got) != 1 || got[0].symbol != "live" { + t.Fatalf("filtered records = %+v, want live only", got) + } + if got := prepareFuncInfoTableRecords(records, map[string]none{}); got != nil { + t.Fatalf("empty live set = %+v, want nil", got) + } +} + +func TestFuncInfoTablePoolsRepeatedStrings(t *testing.T) { + prog := llssa.NewProgram(nil) + records := []funcInfoRecord{ + {symbol: "example.com/p.a", name: "example.com/p.A", file: "shared.go", line: 10}, + {symbol: "example.com/p.b", name: "example.com/p.B", file: "shared.go", line: 20}, + } + ctx := &context{ + prog: prog, + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "linux", + Goarch: "amd64", + }, + } + entry := genMainModule(ctx, llssa.PkgRuntime, &packages.Package{ + PkgPath: "example.com/main", + ExportFile: "main.a", + }, &genConfig{funcInfo: records}) + if got := strings.Count(entry.LPkg.String(), `shared.go\00`); got != 1 { + t.Fatalf("shared file string emitted %d times, want 1", got) + } +} + +func TestFuncInfoTableEmptyDefinitions(t *testing.T) { + prog := llssa.NewProgram(nil) + ctx := &context{ + prog: prog, + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "linux", + Goarch: "amd64", + }, + } + entry := genMainModule(ctx, llssa.PkgRuntime, &packages.Package{ + PkgPath: "example.com/main", + ExportFile: "main.a", + }, &genConfig{}) + ir := entry.LPkg.String() + for _, want := range []string{ + "@__llgo_funcinfo_table = global ptr null", + "@__llgo_pcline_table = global ptr null", + "@__llgo_pcsite_start = global ptr null", + "@__llgo_pcsite_end = global ptr null", + "@__llgo_funcinfo_strings = global ptr null", + "@__llgo_funcinfo_string_offsets = global ptr null", + "@__llgo_funcinfo_string_count = global i64 0", + "@__llgo_funcinfo_hash = global ptr null", + "@__llgo_funcinfo_symbol_index = global ptr null", + "@__llgo_funcinfo_count = global i64 0", + "@__llgo_funcinfo_symbol_index_count = global i64 0", + "@__llgo_funcinfo_entry_start = global ptr null", + "@__llgo_funcinfo_entry_end = global ptr null", + "@__llgo_funcinfo_stub_indexes = global ptr null", + "@__llgo_funcinfo_stub_count = global i64 0", + "@__llgo_pcline_count = global i64 0", + "@__llgo_funcinfo_hash_mask = global i64 0", + } { + if !strings.Contains(ir, want) { + t.Fatalf("empty funcinfo table IR missing %q:\n%s", want, ir) + } + } +} + +func TestFuncInfoTableIgnoresInvalidMetadata(t *testing.T) { + prog := llssa.NewProgram(nil) + pkg := prog.NewPackage("example.com/p", "example.com/p") + mod := pkg.Module() + ctx := mod.Context() + i32 := ctx.Int32Type() + mdstr := func(s string) llvm.Metadata { return ctx.MDString(s) } + mdint := func(v uint64) llvm.Metadata { + return llvm.ConstInt(i32, v, false).ConstantAsMetadata() + } + add := func(fields ...llvm.Metadata) { + mod.AddNamedMetadataOperand(llssa.FuncInfoMetadataName, ctx.MDNode(fields)) + } + + add(mdstr("short")) + add(mdint(2), mdstr("bad.version"), mdstr("bad.version"), mdstr("bad.go"), mdint(1), mdint(1)) + add(mdint(1), mdint(0), mdstr("bad.symbol"), mdstr("bad.go"), mdint(1), mdint(1)) + add(mdint(1), mdstr(""), mdstr("empty.symbol"), mdstr("empty.go"), mdint(1), mdint(1)) + + if got := readFuncInfo(mod); len(got) != 1 || got[0].symbol != "" { + t.Fatalf("readFuncInfo invalid rows = %+v, want one empty-symbol row", got) + } + if got := collectFuncInfo([]Package{nil, {}, {LPkg: pkg}}); len(got) != 0 { + t.Fatalf("collectFuncInfo invalid rows = %+v, want none", got) + } + + empty := ctx.NewModule("empty") + defer empty.Dispose() + if got := readFuncInfo(empty); got != nil { + t.Fatalf("readFuncInfo(empty) = %+v, want nil", got) + } +} diff --git a/internal/build/main_module.go b/internal/build/main_module.go index d5ac73671e..67378f6e2e 100644 --- a/internal/build/main_module.go +++ b/internal/build/main_module.go @@ -43,6 +43,9 @@ type genConfig struct { methodByIndex map[int]none methodByName map[string]none abiSymbols map[string]none + funcInfo []funcInfoRecord + pcLineInfo []pcLineRecord + funcInfoStubs []funcInfoStubRecord } // genMainModule generates the main entry module for an llgo program. @@ -60,6 +63,7 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g argvValueType := prog.Pointer(prog.CStr()) argvVar := mainPkg.NewVarEx("__llgo_argv", prog.Pointer(argvValueType)) argvVar.InitNil() + emitFuncInfoTable(ctx, mainPkg, cfg.funcInfo, cfg.pcLineInfo, cfg.funcInfoStubs) exportFile := pkg.ExportFile if exportFile == "" { diff --git a/internal/build/pclntab_llvm.go b/internal/build/pclntab_llvm.go new file mode 100644 index 0000000000..e7cc079ac3 --- /dev/null +++ b/internal/build/pclntab_llvm.go @@ -0,0 +1,38 @@ +//go:build !llgo +// +build !llgo + +package build + +import ( + "github.com/goplus/llgo/internal/pclntab" + llvm "github.com/xgo-dev/llvm" +) + +// emitPCLnFindFuncBuckets is the LLVM materialization layer for the Go-style +// findfunctab data produced by internal/pclntab. Keep the algorithm in that +// package; this function should only translate buckets into IR constants. +func emitPCLnFindFuncBuckets(mod llvm.Module, symbol string, buckets []pclntab.FindFuncBucket) llvm.Value { + ctx := mod.Context() + i8Type := ctx.Int8Type() + i32Type := ctx.Int32Type() + subType := llvm.ArrayType(i8Type, pclntab.FindFuncSubbucket) + bucketType := ctx.StructType([]llvm.Type{i32Type, subType}, false) + arrayType := llvm.ArrayType(bucketType, len(buckets)) + values := make([]llvm.Value, 0, len(buckets)) + for _, bucket := range buckets { + subs := make([]llvm.Value, 0, len(bucket.Subbuckets)) + for _, sub := range bucket.Subbuckets { + subs = append(subs, llvm.ConstInt(i8Type, uint64(sub), false)) + } + values = append(values, llvm.ConstNamedStruct(bucketType, []llvm.Value{ + llvm.ConstInt(i32Type, uint64(bucket.Idx), false), + llvm.ConstArray(i8Type, subs), + })) + } + global := llvm.AddGlobal(mod, arrayType, symbol) + global.SetInitializer(llvm.ConstArray(bucketType, values)) + global.SetGlobalConstant(true) + global.SetUnnamedAddr(true) + global.SetAlignment(4) + return global +} diff --git a/internal/build/pclntab_llvm_test.go b/internal/build/pclntab_llvm_test.go new file mode 100644 index 0000000000..1e74667238 --- /dev/null +++ b/internal/build/pclntab_llvm_test.go @@ -0,0 +1,36 @@ +//go:build !llgo +// +build !llgo + +package build + +import ( + "strings" + "testing" + + "github.com/goplus/llgo/internal/pclntab" + llvm "github.com/xgo-dev/llvm" +) + +func TestEmitPCLnFindFuncBuckets(t *testing.T) { + llvm.InitializeAllTargets() + ctx := llvm.NewContext() + defer ctx.Dispose() + mod := ctx.NewModule("pclntab-test") + defer mod.Dispose() + + buckets := []pclntab.FindFuncBucket{ + {Idx: 0, Subbuckets: [16]uint8{0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}}, + {Idx: 3, Subbuckets: [16]uint8{0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}}, + } + emitPCLnFindFuncBuckets(mod, "__llgo_findfunctab", buckets) + ir := mod.String() + for _, want := range []string{ + `@__llgo_findfunctab = unnamed_addr constant [2 x { i32, [16 x i8] }]`, + `{ i32 0, [16 x i8] c"\00\01\02`, + `{ i32 3, [16 x i8] c"\00\00\01`, + } { + if !strings.Contains(ir, want) { + t.Fatalf("IR missing %q:\n%s", want, ir) + } + } +} diff --git a/internal/pclnpost/binary.go b/internal/pclnpost/binary.go new file mode 100644 index 0000000000..0a9ce4a6e1 --- /dev/null +++ b/internal/pclnpost/binary.go @@ -0,0 +1,408 @@ +/* + * 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 pclnpost implements the P1/P2 prototype of link-phase ftab/findfunctab +// generation (doc/design/pclntab-linkphase.md). It parses a linked LLGo +// binary's funcinfo site sections, deduplicates LTO inline copies against the +// symbol table, sorts the entries, builds the Go-layout findfunctab via +// internal/pclntab, and prints what the P2 build integration would write +// back. It performs no writes; its purpose is to prove the risky steps on +// real binaries. +package pclnpost + +import ( + "debug/elf" + "debug/macho" + "encoding/binary" + "fmt" + "os" + "sort" + + "github.com/goplus/llgo/internal/pclntab" +) + +type siteRecord struct { + pc uint64 + symbolID uint64 +} + +type textSym struct { + addr uint64 + size uint64 + name string +} + +type secInfo struct { + vmaddr uint64 + size uint64 + fileOff uint64 +} + +type binaryInfo struct { + format string + raw []byte + entrySec []byte + stubSec []byte + textStart uint64 + textEnd uint64 + imageBase uint64 + syms []textSym // sorted by addr, text symbols only + secs []secInfo + // Mach-O chained-fixup import targets, ordinal -> resolved vmaddr (0 if + // the import name has no local definition). Exported symbols' pointer + // slots are emitted as BIND nodes even when they bind to this image, so + // record decoding needs the imports table, not just rebase decoding. + bindTargets []uint64 + + entryVMAddr, entryVMSize, entryFileOff uint64 + stubVMSize, stubFileOff uint64 +} + +// readVM returns n bytes at a link-time virtual address. +func readVM(info *binaryInfo, addr uint64, n int) []byte { + for _, s := range info.secs { + if addr >= s.vmaddr && addr+uint64(n) <= s.vmaddr+s.size { + off := s.fileOff + (addr - s.vmaddr) + return info.raw[off : off+uint64(n)] + } + } + return make([]byte, n) +} + +func load(path string) (*binaryInfo, error) { + if mf, err := macho.Open(path); err == nil { + defer mf.Close() + info := &binaryInfo{format: "macho"} + info.raw, _ = os.ReadFile(path) + for _, s := range mf.Sections { + info.secs = append(info.secs, secInfo{vmaddr: s.Addr, size: s.Size, fileOff: uint64(s.Offset)}) + } + if s := mf.Section("__llgo_fie"); s != nil { + info.entrySec, _ = s.Data() + info.entryVMAddr, info.entryVMSize, info.entryFileOff = s.Addr, s.Size, uint64(s.Offset) + } + if s := mf.Section("__llgo_stub"); s != nil { + info.stubSec, _ = s.Data() + info.stubVMSize, info.stubFileOff = s.Size, uint64(s.Offset) + } + if s := mf.Section("__text"); s != nil { + info.textStart, info.textEnd = s.Addr, s.Addr+s.Size + info.imageBase = s.Addr &^ 0xFFFFFFF + } + if mf.Symtab != nil { + for _, sym := range mf.Symtab.Syms { + if sym.Value >= info.textStart && sym.Value < info.textEnd && sym.Name != "" { + info.syms = append(info.syms, textSym{addr: sym.Value, name: sym.Name}) + } + } + } + loadBindTargets(info, mf) + finish(info) + return info, nil + } + ef, err := elf.Open(path) + if err != nil { + return nil, fmt.Errorf("not Mach-O and not ELF: %w", err) + } + defer ef.Close() + info := &binaryInfo{format: "elf"} + info.raw, _ = os.ReadFile(path) + for _, s := range ef.Sections { + if s.Type != elf.SHT_NOBITS && s.Addr != 0 { + info.secs = append(info.secs, secInfo{vmaddr: s.Addr, size: s.Size, fileOff: s.Offset}) + } + } + if s := ef.Section("llgo_funcinfo_entry"); s != nil { + info.entrySec, _ = s.Data() + info.entryVMAddr, info.entryVMSize, info.entryFileOff = s.Addr, s.Size, s.Offset + } + if s := ef.Section("llgo_funcinfo_stubsite"); s != nil { + info.stubSec, _ = s.Data() + info.stubVMSize, info.stubFileOff = s.Size, s.Offset + } + if s := ef.Section(".text"); s != nil { + info.textStart, info.textEnd = s.Addr, s.Addr+s.Size + info.imageBase = s.Addr &^ 0xFFFFFFF + } + syms, _ := ef.Symbols() + for _, sym := range syms { + if elf.ST_TYPE(sym.Info) == elf.STT_FUNC && sym.Value >= info.textStart && sym.Value < info.textEnd { + info.syms = append(info.syms, textSym{addr: sym.Value, size: sym.Size, name: sym.Name}) + } + } + finish(info) + return info, nil +} + +func finish(info *binaryInfo) { + sort.Slice(info.syms, func(i, j int) bool { return info.syms[i].addr < info.syms[j].addr }) + // Collapse same-address aliases, then derive missing extents from the + // next distinct symbol start (Mach-O nlist carries no sizes; Go's linker + // uses the same next-start rule for its final ftab). + dedup := info.syms[:0] + for _, s := range info.syms { + if len(dedup) > 0 && dedup[len(dedup)-1].addr == s.addr { + continue + } + dedup = append(dedup, s) + } + info.syms = dedup + for i := range info.syms { + if info.syms[i].size == 0 { + if i+1 < len(info.syms) { + info.syms[i].size = info.syms[i+1].addr - info.syms[i].addr + } else { + info.syms[i].size = info.textEnd - info.syms[i].addr + } + } + } +} + +func parseRecords(info *binaryInfo, sec []byte) []siteRecord { + var out []siteRecord + for off := 0; off+16 <= len(sec); off += 16 { + pc := binary.LittleEndian.Uint64(sec[off:]) + id := binary.LittleEndian.Uint64(sec[off+8:]) + if pc == 0 || id == 0 { // zero keep-alive record + continue + } + // Mach-O pointer slots in the on-disk file hold dyld chained-fixup + // encodings; dyld rewrites them at load. Rebase nodes + // (DYLD_CHAINED_PTR_64) carry the target in the low 36 bits. Anchors + // naming *exported* functions — every `__llgo_stub.*` and any + // exported Go function — are emitted as BIND nodes instead (bit 63 + // set, import ordinal in the low 24 bits, addend above), even though + // they bind back into this same image, so those resolve through the + // imports table. The P2 write-back avoids the problem entirely by + // storing anchor-relative offsets instead of pointers. + if info.format == "macho" && (pc < info.textStart || pc >= info.textEnd) { + if pc>>63 != 0 { // DYLD_CHAINED_PTR_64_BIND + ordinal := pc & (1<<24 - 1) + addend := (pc >> 24) & 0xFF + if ordinal >= uint64(len(info.bindTargets)) || info.bindTargets[ordinal] == 0 { + continue + } + pc = info.bindTargets[ordinal] + addend + } else if t := pc & (1<<36 - 1); t >= info.textStart && t < info.textEnd { + pc = t + } + } + out = append(out, siteRecord{pc: pc, symbolID: id}) + } + return out +} + +// owner returns the text symbol containing addr. +func owner(info *binaryInfo, addr uint64) (textSym, bool) { + i := sort.Search(len(info.syms), func(i int) bool { return info.syms[i].addr > addr }) + if i == 0 { + return textSym{}, false + } + s := info.syms[i-1] + if addr >= s.addr+s.size { + return textSym{}, false + } + return s, true +} + +// fnv64 mirrors funcInfoSymbolID in internal/build/funcinfo_table.go. +func fnv64(name string) uint64 { + const offset = uint64(14695981039346656037) + const prime = uint64(1099511628211) + h := offset + for i := 0; i < len(name); i++ { + h ^= uint64(name[i]) + h *= prime + } + if h == 0 { + return 1 + } + return h +} + +const stubPrefix = "__llgo_stub." + +// canonicalOwner reports whether owner symbol `name` is the function the +// record's symbolID names, or that function's `__llgo_stub.` wrapper. +// Mach-O symbol names carry a C-mangling underscore, and debug/macho's +// suffix-shared string table can surface one underscore more or less than +// the source-level name, so try each plausible normalization — matching a +// specific 64-bit FNV makes a false positive practically impossible. +func canonicalOwner(info *binaryInfo, name string, symbolID uint64) bool { + for { + cand := name + if len(cand) > len(stubPrefix) { + if i := stringIndex(cand, stubPrefix); i >= 0 { + cand = cand[i+len(stubPrefix):] + } + } + if fnv64(cand) == symbolID { + return true + } + if info.format == "macho" && len(name) > 1 && name[0] == '_' { + name = name[1:] + continue + } + return false + } +} + +func stringIndex(s, prefix string) int { + // prefix at the start, allowing for leading mangling underscores only + for i := 0; i+len(prefix) <= len(s) && i <= 2; i++ { + if s[i:i+len(prefix)] == prefix { + return i + } + if s[i] != '_' { + break + } + } + return -1 +} + +// dedupe keeps exactly the canonical record per emitting function: a record +// is canonical when the symbol that owns its anchor PC is the function the +// symbolID names (id == fnv64(owner)) or that function's closure stub +// (owner "__llgo_stub.X" with id == fnv64(X) — stubs share the target's +// symbolID by design). Everything else with a known owner is an LTO inline +// copy: inlining duplicated the body-embedded record into a host function. +// Kept records are normalized to their owner's true entry address. Records +// whose owner cannot be determined are dropped conservatively. +func dedupe(info *binaryInfo, recs []siteRecord, verbose bool) (kept []siteRecord, droppedInline, droppedUnknown int) { + seenOwner := make(map[uint64]bool, len(recs)) + for _, r := range recs { + sym, ok := owner(info, r.pc) + if !ok { + droppedUnknown++ + continue + } + if !canonicalOwner(info, sym.name, r.symbolID) { + droppedInline++ + if verbose { + fmt.Printf(" inline copy: id=%#x pc=%#x inside %s\n", r.symbolID, r.pc, sym.name) + } + continue + } + if seenOwner[sym.addr] { + continue + } + seenOwner[sym.addr] = true + kept = append(kept, siteRecord{pc: sym.addr, symbolID: r.symbolID}) + } + return kept, droppedInline, droppedUnknown +} + +// buildFtab returns the sorted table plus the base PC (Go's minpc): offsets +// are relative to the first recorded function so ftab[0].EntryOff == 0, as +// internal/pclntab requires. +func buildFtab(info *binaryInfo, kept []siteRecord) ([]pclntab.FuncTabEntry, uint64) { + sort.Slice(kept, func(i, j int) bool { return kept[i].pc < kept[j].pc }) + if len(kept) == 0 { + return nil, info.textStart + } + base := kept[0].pc + ftab := make([]pclntab.FuncTabEntry, 0, len(kept)+1) + prev := uint64(0) + for i, r := range kept { + if r.pc == prev { + continue // two symbolIDs at one entry (aliases); keep first + } + prev = r.pc + ftab = append(ftab, pclntab.FuncTabEntry{EntryOff: uint32(r.pc - base), FuncOff: uint32(i)}) + } + // Go-style sentinel at end of text. + ftab = append(ftab, pclntab.FuncTabEntry{EntryOff: uint32(info.textEnd - base), FuncOff: ^uint32(0)}) + return ftab, base +} + +// loadBindTargets parses the LC_DYLD_CHAINED_FIXUPS imports table and +// resolves each import ordinal to the address of its local definition (this +// is a main executable: every funcinfo bind target is defined in-image). +func loadBindTargets(info *binaryInfo, mf *macho.File) { + raw := info.raw + if len(raw) < 32 { + return + } + ncmds := binary.LittleEndian.Uint32(raw[16:]) + var fixOff, fixSize uint64 + off := uint64(32) + for i := uint32(0); i < ncmds && off+8 <= uint64(len(raw)); i++ { + cmd := binary.LittleEndian.Uint32(raw[off:]) + size := binary.LittleEndian.Uint32(raw[off+4:]) + if cmd == lcDyldChainedFixups { + fixOff = uint64(binary.LittleEndian.Uint32(raw[off+8:])) + fixSize = uint64(binary.LittleEndian.Uint32(raw[off+12:])) + } + off += uint64(size) + } + if fixOff == 0 || fixOff+28 > uint64(len(raw)) { + return + } + hdr := raw[fixOff : fixOff+fixSize] + importsOff := binary.LittleEndian.Uint32(hdr[8:]) + symbolsOff := binary.LittleEndian.Uint32(hdr[12:]) + importsCount := binary.LittleEndian.Uint32(hdr[16:]) + importsFormat := binary.LittleEndian.Uint32(hdr[20:]) + if importsCount == 0 || importsCount > 1<<24 { + return + } + var stride, nameShift uint32 + switch importsFormat { + case 1: // DYLD_CHAINED_IMPORT: u32 {lib:8, weak:1, name_offset:23} + stride, nameShift = 4, 9 + case 2: // DYLD_CHAINED_IMPORT_ADDEND: {u32, i32 addend} + stride, nameShift = 8, 9 + default: // ADDEND64 or unknown: leave unresolved + return + } + byName := make(map[string]uint64, len(info.syms)) + if mf.Symtab != nil { + for _, sym := range mf.Symtab.Syms { + if sym.Value != 0 && sym.Name != "" { + byName[sym.Name] = sym.Value + } + } + } + cstr := func(b []byte) string { + for i, c := range b { + if c == 0 { + return string(b[:i]) + } + } + return string(b) + } + targets := make([]uint64, importsCount) + for i := uint32(0); i < importsCount; i++ { + rec := uint64(importsOff) + uint64(i*stride) + if rec+4 > uint64(len(hdr)) { + break + } + v := binary.LittleEndian.Uint32(hdr[rec:]) + nameOff := uint64(symbolsOff) + uint64(v>>nameShift) + if nameOff >= uint64(len(hdr)) { + continue + } + name := cstr(hdr[nameOff:]) + addr, ok := byName[name] + if !ok && len(name) > 1 && name[0] == '_' { + // debug/macho's Symtab names may carry one less mangling + // underscore than the import strings. + addr = byName[name[1:]] + } + targets[i] = addr + } + info.bindTargets = targets +} diff --git a/internal/pclnpost/binary_test.go b/internal/pclnpost/binary_test.go new file mode 100644 index 0000000000..3ea8508e65 --- /dev/null +++ b/internal/pclnpost/binary_test.go @@ -0,0 +1,49 @@ +/* + * 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 pclnpost + +import "testing" + +func TestCanonicalOwner(t *testing.T) { + elf := &binaryInfo{format: "elf"} + macho := &binaryInfo{format: "macho"} + id := fnv64("example.com/p.F") + cases := []struct { + info *binaryInfo + name string + want bool + }{ + // ELF: symbol names are source-level. + {elf, "example.com/p.F", true}, + {elf, "__llgo_stub.example.com/p.F", true}, + {elf, "example.com/p.G", false}, + // Mach-O: one C-mangling underscore, and debug/macho's suffix-shared + // string table can surface one underscore more or less. + {macho, "_example.com/p.F", true}, + {macho, "example.com/p.F", true}, + {macho, "___llgo_stub.example.com/p.F", true}, + {macho, "__llgo_stub.example.com/p.F", true}, + // An LTO inline copy: record id names F but the owner is the host. + {macho, "_example.com/p.Host", false}, + {macho, "___llgo_stub.example.com/p.G", false}, + } + for _, c := range cases { + if got := canonicalOwner(c.info, c.name, id); got != c.want { + t.Errorf("canonicalOwner(%s, %q) = %v, want %v", c.info.format, c.name, got, c.want) + } + } +} diff --git a/internal/pclnpost/fixups.go b/internal/pclnpost/fixups.go new file mode 100644 index 0000000000..8af02d419d --- /dev/null +++ b/internal/pclnpost/fixups.go @@ -0,0 +1,247 @@ +/* + * 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 pclnpost + +import ( + "encoding/binary" + "fmt" + "sort" +) + +// Mach-O dyld chained fixups surgery. +// +// The on-disk pointer slots of the rewritten sections participate in dyld's +// chained-fixup page chains. If the chains are left untouched, dyld walks +// them at load time and rebases 8-byte slots inside our freshly written +// table, corrupting it — and terminating a chain early inside a zeroed +// section would also skip later fixups in the same page, corrupting +// unrelated data. unchainRanges removes every chain node that falls inside +// the given file-offset ranges: predecessors' next links (or the page_start +// table) are repointed to the first surviving successor. + +const ( + lcDyldChainedFixups = 0x80000034 + lcSegment64 = 0x19 + + chainedPtrStartNone = 0xFFFF + chainedPtrStartMulti = 0x8000 + + // pointer_format values with a 12-bit next field at bit 51, stride 4. + chainedPtr64 = 2 + chainedPtr64Offset = 6 +) + +type segRange struct { + fileOff uint64 + fileSz uint64 + vmaddr uint64 +} + +// fixupInsert asks for a rebase fixup node at fileOff whose loaded value will +// be targetVM + slide. Slot bytes are returned as pending writes so the +// caller can apply them after overwriting the section contents. +type fixupInsert struct { + fileOff uint64 + targetVM uint64 +} + +type pendingWrite struct { + fileOff uint64 + val uint64 +} + +// unchainRanges edits chain metadata in raw in place: every chain node inside +// `ranges` (file-offset [start,end) pairs) is unlinked, and the requested +// `inserts` are spliced into the page chains as rebase nodes. Because insert +// slots usually lie inside a section the caller is about to overwrite, their +// encoded slot values are returned as pending writes to apply afterwards. +func unchainRanges(raw []byte, ranges [][2]uint64, inserts []fixupInsert) ([]pendingWrite, error) { + inRange := func(off uint64) bool { + for _, r := range ranges { + if off >= r[0] && off < r[1] { + return true + } + } + return false + } + + // Locate LC_DYLD_CHAINED_FIXUPS and the segment table. + if len(raw) < 32 || binary.LittleEndian.Uint32(raw) != 0xFEEDFACF { + return nil, fmt.Errorf("not a 64-bit little-endian Mach-O") + } + ncmds := binary.LittleEndian.Uint32(raw[16:]) + off := uint64(32) + var fixOff, fixSize uint64 + var segs []segRange + for i := uint32(0); i < ncmds; i++ { + cmd := binary.LittleEndian.Uint32(raw[off:]) + size := binary.LittleEndian.Uint32(raw[off+4:]) + switch cmd { + case lcDyldChainedFixups: + fixOff = uint64(binary.LittleEndian.Uint32(raw[off+8:])) + fixSize = uint64(binary.LittleEndian.Uint32(raw[off+12:])) + case lcSegment64: + segs = append(segs, segRange{ + vmaddr: binary.LittleEndian.Uint64(raw[off+24:]), + fileOff: binary.LittleEndian.Uint64(raw[off+40:]), + fileSz: binary.LittleEndian.Uint64(raw[off+48:]), + }) + } + off += uint64(size) + } + if fixOff == 0 { + if len(inserts) > 0 { + return nil, fmt.Errorf("no chained fixups to splice inserts into") + } + return nil, nil // no chained fixups (classic dyld info); nothing to do + } + _ = fixSize + imageBase := ^uint64(0) + for _, sg := range segs { + if sg.vmaddr != 0 && sg.vmaddr < imageBase { + imageBase = sg.vmaddr + } + } + var pending []pendingWrite + consumed := make(map[uint64]bool, len(inserts)) + + hdr := raw[fixOff:] + startsOff := fixOff + uint64(binary.LittleEndian.Uint32(hdr[4:])) + segCount := binary.LittleEndian.Uint32(raw[startsOff:]) + if int(segCount) != len(segs) { + // seg_count counts all segments incl. ones without fixups; trust it. + } + for si := uint32(0); si < segCount; si++ { + segInfoOff := binary.LittleEndian.Uint32(raw[startsOff+4+uint64(si)*4:]) + if segInfoOff == 0 { + continue + } + sOff := startsOff + uint64(segInfoOff) + pageSize := uint64(binary.LittleEndian.Uint16(raw[sOff+4:])) + ptrFormat := binary.LittleEndian.Uint16(raw[sOff+6:]) + pageCount := uint64(binary.LittleEndian.Uint16(raw[sOff+20:])) + if ptrFormat != chainedPtr64 && ptrFormat != chainedPtr64Offset { + // Only reject if this segment's pages intersect our ranges. + segFile := segs[si].fileOff + touches := false + for _, r := range ranges { + if r[0] < segFile+pageCount*pageSize && r[1] > segFile { + touches = true + } + } + if !touches { + continue + } + return nil, fmt.Errorf("unsupported pointer_format %d", ptrFormat) + } + encode := func(targetVM, next uint64) uint64 { + t := targetVM + if ptrFormat == chainedPtr64Offset { + t = targetVM - imageBase + } + return (t & (1<<36 - 1)) | (next << 51) + } + segFileOff := segs[si].fileOff + for pi := uint64(0); pi < pageCount; pi++ { + psOff := sOff + 22 + pi*2 + pageFile := segFileOff + pi*pageSize + pageEnd := pageFile + pageSize + // Inserts requested for this page. + var ins []fixupInsert + for _, in := range inserts { + if in.fileOff >= pageFile && in.fileOff < pageEnd { + ins = append(ins, in) + consumed[in.fileOff] = true + } + } + pStart := binary.LittleEndian.Uint16(raw[psOff:]) + if pStart == chainedPtrStartNone && len(ins) == 0 { + continue + } + if pStart != chainedPtrStartNone && pStart&chainedPtrStartMulti != 0 { + return nil, fmt.Errorf("multi-start pages not supported") + } + // Collect the existing chain. + var nodes []uint64 + if pStart != chainedPtrStartNone { + node := pageFile + uint64(pStart) + for { + nodes = append(nodes, node) + val := binary.LittleEndian.Uint64(raw[node:]) + next := (val >> 51) & 0xFFF + if next == 0 { + break + } + node += next * 4 + } + } + // Rebuild: out-of-range survivors plus requested inserts. + type finalNode struct { + off uint64 + insert bool + targetVM uint64 + } + var final []finalNode + removed := 0 + for _, n := range nodes { + if inRange(n) { + removed++ + } else { + final = append(final, finalNode{off: n}) + } + } + for _, in := range ins { + final = append(final, finalNode{off: in.fileOff, insert: true, targetVM: in.targetVM}) + } + if removed == 0 && len(ins) == 0 { + continue + } + sort.Slice(final, func(i, j int) bool { return final[i].off < final[j].off }) + if len(final) == 0 { + binary.LittleEndian.PutUint16(raw[psOff:], chainedPtrStartNone) + continue + } + binary.LittleEndian.PutUint16(raw[psOff:], uint16(final[0].off-pageFile)) + for i, n := range final { + var next uint64 + if i+1 < len(final) { + delta := final[i+1].off - n.off + if delta%4 != 0 || delta/4 > 0xFFF { + return nil, fmt.Errorf("chain gap %d not encodable", delta) + } + next = delta / 4 + } + if n.insert { + pending = append(pending, pendingWrite{fileOff: n.off, val: encode(n.targetVM, next)}) + } else { + val := binary.LittleEndian.Uint64(raw[n.off:]) + val = (val &^ (uint64(0xFFF) << 51)) | (next << 51) + binary.LittleEndian.PutUint64(raw[n.off:], val) + } + } + } + } + // An unconsumed insert means its slot never joined a fixup chain; on a + // PIE binary the runtime would then read an unslid value. Fail loudly so + // the caller falls back to first-use construction instead. + for _, in := range inserts { + if !consumed[in.fileOff] { + return nil, fmt.Errorf("fixup insert at %#x not within any chain page", in.fileOff) + } + } + return pending, nil +} diff --git a/internal/pclnpost/pclnpost.go b/internal/pclnpost/pclnpost.go new file mode 100644 index 0000000000..c65f815054 --- /dev/null +++ b/internal/pclnpost/pclnpost.go @@ -0,0 +1,69 @@ +/* + * 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 pclnpost + +import ( + "encoding/binary" + "fmt" +) + +// Stats summarizes one rewrite. +type Stats struct { + Format string + EntryRecords int + StubRecords int + Kept int + InlineCopies int + NoSymbol int + FtabEntries int + Buckets int +} + +// Rewrite parses the linked binary's funcinfo site sections, deduplicates +// LTO inline copies against the symbol table, builds the Go-layout prebuilt +// table and rewrites the entry section in place (voiding the stub section). +// The runtime adopts the table when it sees the magic header and falls back +// to first-use construction otherwise, so failures here leave a fully +// functional binary. +func Rewrite(path string) (Stats, error) { + var st Stats + info, err := load(path) + if err != nil { + return st, err + } + st.Format = info.format + if len(info.entrySec) >= 8 && binary.LittleEndian.Uint64(info.entrySec) == prebuiltMagic { + return st, fmt.Errorf("already rewritten") + } + entries := parseRecords(info, info.entrySec) + stubs := parseRecords(info, info.stubSec) + st.EntryRecords, st.StubRecords = len(entries), len(stubs) + if len(entries) == 0 { + return st, fmt.Errorf("no entry records") + } + kept, inline, nosym := dedupe(info, append(entries, stubs...), false) + st.Kept, st.InlineCopies, st.NoSymbol = len(kept), inline, nosym + if len(kept) == 0 { + return st, fmt.Errorf("no records survived dedup") + } + ftab, buckets, err := writeBack(path, info, kept) + if err != nil { + return st, err + } + st.FtabEntries, st.Buckets = ftab, buckets + return st, nil +} diff --git a/internal/pclnpost/write.go b/internal/pclnpost/write.go new file mode 100644 index 0000000000..8ccd24d11c --- /dev/null +++ b/internal/pclnpost/write.go @@ -0,0 +1,288 @@ +/* + * 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 pclnpost + +import ( + "debug/elf" + "debug/macho" + "encoding/binary" + "fmt" + "os" + "os/exec" + "runtime" + "sort" +) + +// Prebuilt blob layout — keep in sync with runtime/internal/lib/runtime +// (runtimePrebuiltMagic and adoptPrebuiltFuncPCTable): +// +// u64 magic "LLGOFTB1"; u64 linkSectAddr; u64 base +// u32 count (incl sentinel); u32 bucketCount +// count × {u32 entryOff, u32 funcIndex} +// bucketCount × {u32 idx; 16 × u16 subbuckets} +// +// The base slot holds the runtime PC of the first table entry: on Mach-O it +// is re-linked into the dyld chained-fixup chain (dyld rebases it at load), +// on non-PIE ELF the link-time value already equals the runtime address. +const prebuiltMagic = uint64(0x314254464F474C4C) + +const ( + bucketSize = 4096 + subbucketCnt = 16 + subbucketSize = bucketSize / subbucketCnt + bucketBytes = 4 + 2*subbucketCnt +) + +type symIndexEntry struct { + id uint64 + idx uint32 +} + +// writeBack rewrites the entry-site section in place with the prebuilt table +// and voids the stub section (its records are merged into the table). +func writeBack(path string, info *binaryInfo, kept []siteRecord) (ftabCount, bucketCount int, err error) { + symIdx, err := loadSymbolIndex(path, info) + if err != nil { + return 0, 0, err + } + sort.Slice(kept, func(i, j int) bool { return kept[i].pc < kept[j].pc }) + type row struct { + pc uint64 + idx uint32 + } + rows := make([]row, 0, len(kept)) + prev := uint64(0) + for _, r := range kept { + if r.pc == prev { + continue + } + idx, ok := lookupSymIndex(symIdx, r.symbolID) + if !ok { + continue + } + prev = r.pc + rows = append(rows, row{pc: r.pc, idx: idx}) + } + if len(rows) == 0 { + return 0, 0, fmt.Errorf("no resolvable entries") + } + base := rows[0].pc + count := len(rows) + 1 // + sentinel + + // findfunctab in the runtime's uint16 layout, mirroring + // buildRuntimeFuncPCIndex (base aligned down to a bucket boundary). + alignedBase := base &^ (bucketSize - 1) + last := rows[len(rows)-1].pc + nbuckets := int((last-alignedBase)/bucketSize + 1) + pcs := make([]uint64, len(rows)) + for i, r := range rows { + pcs[i] = r.pc + } + lastLE := func(pc uint64) int { // last index with pcs[i] <= pc, clamped like the runtime + i := sort.Search(len(pcs), func(i int) bool { return pcs[i] > pc }) - 1 + if i < 0 { + i = 0 + } + return i + } + buckets := make([]byte, 0, nbuckets*bucketBytes) + for b := 0; b < nbuckets; b++ { + bucketStart := alignedBase + uint64(b)*bucketSize + baseIdx := lastLE(bucketStart) + var tmp [bucketBytes]byte + binary.LittleEndian.PutUint32(tmp[0:], uint32(baseIdx)) + for s := 0; s < subbucketCnt; s++ { + subIdx := lastLE(bucketStart + uint64(s)*subbucketSize) + delta := subIdx - baseIdx + if delta < 0 || delta > 0xffff { + return 0, 0, fmt.Errorf("subbucket delta overflow: %d", delta) + } + binary.LittleEndian.PutUint16(tmp[4+2*s:], uint16(delta)) + } + buckets = append(buckets, tmp[:]...) + } + + need := 32 + count*8 + len(buckets) + entrySize := int(info.entryVMSize) + if need > entrySize { + return 0, 0, fmt.Errorf("prebuilt blob %dB does not fit entry section %dB", need, entrySize) + } + blob := make([]byte, entrySize) // zero tail + binary.LittleEndian.PutUint64(blob[0:], prebuiltMagic) + binary.LittleEndian.PutUint64(blob[8:], info.entryVMAddr) + binary.LittleEndian.PutUint64(blob[16:], base) + binary.LittleEndian.PutUint32(blob[24:], uint32(count)) + binary.LittleEndian.PutUint32(blob[28:], uint32(len(buckets)/bucketBytes)) + off := 32 + for _, r := range rows { + binary.LittleEndian.PutUint32(blob[off:], uint32(r.pc-base)) + binary.LittleEndian.PutUint32(blob[off+4:], r.idx) + off += 8 + } + // Sentinel: end of text, funcIndex 0. + binary.LittleEndian.PutUint32(blob[off:], uint32(info.textEnd-base)) + off += 8 + copy(blob[off:], buckets) + + raw := make([]byte, len(info.raw)) + copy(raw, info.raw) + var pending []pendingWrite + if info.format == "macho" { + // Remove the rewritten sections' pointer slots from dyld's chained + // fixup page chains first: otherwise dyld rebases 8-byte slots + // inside the new table at load time, and a chain terminating early + // inside the zeroed stub section would skip unrelated fixups later + // in the same page. + ranges := [][2]uint64{{info.entryFileOff, info.entryFileOff + info.entryVMSize}} + if info.stubVMSize > 0 { + ranges = append(ranges, [2]uint64{info.stubFileOff, info.stubFileOff + info.stubVMSize}) + } + // The header's base slot is spliced back into the chain as a live + // rebase node: dyld writes the *slid* text base there at load, so + // the runtime reads a ready runtime PC with no slide arithmetic. + inserts := []fixupInsert{{fileOff: info.entryFileOff + 16, targetVM: base}} + pending, err = unchainRanges(raw, ranges, inserts) + if err != nil { + return 0, 0, fmt.Errorf("chained fixups: %w", err) + } + } + copy(raw[info.entryFileOff:], blob) + for _, pw := range pending { + binary.LittleEndian.PutUint64(raw[pw.fileOff:], pw.val) + } + // Void the stub section: zero its records so the runtime's fallback scan + // finds nothing (stub entries are already merged into the table above). + if info.stubVMSize > 0 { + zero := raw[info.stubFileOff : info.stubFileOff+info.stubVMSize] + for i := range zero { + zero[i] = 0 + } + } + st, err := os.Stat(path) + if err != nil { + return 0, 0, err + } + if err := os.WriteFile(path, raw, st.Mode()); err != nil { + return 0, 0, err + } + if info.format == "macho" && runtime.GOOS == "darwin" { + if out, err := exec.Command("codesign", "-f", "-s", "-", path).CombinedOutput(); err != nil { + return 0, 0, fmt.Errorf("codesign: %v: %s", err, out) + } + } + return count, len(buckets) / bucketBytes, nil +} + +// metaRecordMagic marks the entry-section meta record ("LLGOMET1" LE); keep +// in sync with internal/build/funcinfo_table.go. +const metaRecordMagic = uint64(0x3154454D4F474C4C) + +// metaGlobalAddrs scans the raw entry section for the meta record and +// returns the link-time addresses of the symbol-index pointer global and its +// count global. Works in +LTO binaries where the symbols are internalized +// away, because the addresses come from relocations, not the symbol table. +func metaGlobalAddrs(info *binaryInfo) (idxPtr, cntPtr uint64, ok bool) { + sec := info.entrySec + for off := 0; off+48 <= len(sec); off += 16 { + pc := binary.LittleEndian.Uint64(sec[off:]) + id := binary.LittleEndian.Uint64(sec[off+8:]) + if pc == 0 && id == metaRecordMagic { + idxPtr = decodePtrVal(info, binary.LittleEndian.Uint64(sec[off+16:])) + cntPtr = decodePtrVal(info, binary.LittleEndian.Uint64(sec[off+32:])) + return idxPtr, cntPtr, idxPtr != 0 && cntPtr != 0 + } + } + return 0, 0, false +} + +// loadSymbolIndex reads the {u64 symbolID, u32 funcIndex} table, locating it +// through the entry-section meta record (LTO-safe) with the symbol table as +// fallback for older binaries. +func loadSymbolIndex(path string, info *binaryInfo) ([]symIndexEntry, error) { + ptrAddr, cntAddr, ok := metaGlobalAddrs(info) + if !ok { + var err error + ptrAddr, err = symbolAddr(path, "__llgo_funcinfo_symbol_index") + if err != nil { + return nil, err + } + cntAddr, err = symbolAddr(path, "__llgo_funcinfo_symbol_index_count") + if err != nil { + return nil, err + } + } + dataAddr := decodePtr(info, readVM(info, ptrAddr, 8)) + count := binary.LittleEndian.Uint64(readVM(info, cntAddr, 8)) + if count == 0 || count > 1<<20 { + return nil, fmt.Errorf("bad symbol index count %d", count) + } + raw := readVM(info, dataAddr, int(count)*16) + out := make([]symIndexEntry, count) + for i := range out { + out[i] = symIndexEntry{ + id: binary.LittleEndian.Uint64(raw[i*16:]), + idx: binary.LittleEndian.Uint32(raw[i*16+8:]), + } + } + return out, nil +} + +func lookupSymIndex(idx []symIndexEntry, id uint64) (uint32, bool) { + i := sort.Search(len(idx), func(i int) bool { return idx[i].id >= id }) + if i < len(idx) && idx[i].id == id { + return idx[i].idx, true + } + return 0, false +} + +// decodePtr resolves an on-disk pointer slot (Mach-O chained fixup or plain). +func decodePtr(info *binaryInfo, b []byte) uint64 { + return decodePtrVal(info, binary.LittleEndian.Uint64(b)) +} + +func decodePtrVal(info *binaryInfo, v uint64) uint64 { + if info.format == "macho" { + if t := v & (1<<36 - 1); t != v && t >= info.imageBase { + return t + } + } + return v +} + +func symbolAddr(path, name string) (uint64, error) { + if mf, err := macho.Open(path); err == nil { + defer mf.Close() + for _, s := range mf.Symtab.Syms { + if s.Name == "_"+name || s.Name == name { + return s.Value, nil + } + } + return 0, fmt.Errorf("symbol %s not found", name) + } + ef, err := elf.Open(path) + if err != nil { + return 0, err + } + defer ef.Close() + syms, _ := ef.Symbols() + for _, s := range syms { + if s.Name == name { + return s.Value, nil + } + } + return 0, fmt.Errorf("symbol %s not found", name) +} diff --git a/internal/pclntab/pclntab.go b/internal/pclntab/pclntab.go new file mode 100644 index 0000000000..9058c14830 --- /dev/null +++ b/internal/pclntab/pclntab.go @@ -0,0 +1,121 @@ +// Package pclntab contains the Go-style findfunc bucket/index algorithm used +// by LLGo runtime metadata. It is intentionally free of LLVM dependencies so +// build-time emitters and tests share one implementation of the pclntab logic. +package pclntab + +import "fmt" + +const ( + // These constants intentionally match Go's pclntab findfunc layout: + // cmd/link builds one 4096-byte text bucket, split into 16 256-byte + // subbuckets, and runtime.findfunc starts scanning from the recorded + // bucket base plus subbucket delta. + MinFuncSize = uint32(16) + FuncTabBucketSize = uint32(256) * MinFuncSize + FindFuncSubbucket = 16 +) + +// FuncTabEntry mirrors the two pieces of data Go's linker stores in functab: +// a PC offset sorted by final text address, and an opaque function metadata +// offset. LLGo's current caller uses FuncOff as a payload index. +type FuncTabEntry struct { + EntryOff uint32 + FuncOff uint32 +} + +// FindFuncBucket mirrors runtime.findfuncbucket: one uint32 base function +// index plus 16 one-byte deltas into the sorted functab. +type FindFuncBucket struct { + Idx uint32 + Subbuckets [FindFuncSubbucket]uint8 +} + +// BuildFindFuncBuckets ports Go's cmd/link findfunctab construction for a +// sorted functab. It deliberately stays independent of LLVM so build/link code +// can use it without duplicating the algorithm. +func BuildFindFuncBuckets(ftab []FuncTabEntry, textSize uint32) ([]FindFuncBucket, error) { + if textSize == 0 { + return nil, nil + } + if len(ftab) < 2 { + return nil, fmt.Errorf("pclntab ftab needs at least one function and one sentinel") + } + for i := 1; i < len(ftab); i++ { + if ftab[i].EntryOff <= ftab[i-1].EntryOff { + return nil, fmt.Errorf("pclntab ftab entries must be strictly increasing") + } + } + if ftab[0].EntryOff != 0 { + return nil, fmt.Errorf("pclntab first entry offset must be zero") + } + if ftab[len(ftab)-1].EntryOff < textSize { + return nil, fmt.Errorf("pclntab sentinel offset %d below text size %d", ftab[len(ftab)-1].EntryOff, textSize) + } + + nbuckets := int((textSize + FuncTabBucketSize - 1) / FuncTabBucketSize) + buckets := make([]FindFuncBucket, nbuckets) + subSize := FuncTabBucketSize / FindFuncSubbucket + for b := range buckets { + bucketStart := uint32(b) * FuncTabBucketSize + baseIdx := FuncIndexForPC(ftab, bucketStart) + buckets[b].Idx = uint32(baseIdx) + for s := 0; s < FindFuncSubbucket; s++ { + pc := bucketStart + uint32(s)*subSize + if pc >= textSize { + pc = textSize - 1 + } + subIdx := FuncIndexForPC(ftab, pc) + delta := subIdx - baseIdx + if delta < 0 || delta > 255 { + return nil, fmt.Errorf("pclntab subbucket delta overflow: bucket=%d subbucket=%d delta=%d", b, s, delta) + } + buckets[b].Subbuckets[s] = uint8(delta) + } + } + return buckets, nil +} + +// FuncIndexForPC is the slow reference lookup over the sorted functab. It is +// kept for tests and for building the compact bucket table. +func FuncIndexForPC(ftab []FuncTabEntry, pcOff uint32) int { + lo, hi := 0, len(ftab)-1 // last entry is the sentinel. + for lo+1 < hi { + mid := int(uint(lo+hi) >> 1) + if ftab[mid].EntryOff <= pcOff { + lo = mid + } else { + hi = mid + } + } + for lo+1 < len(ftab) && ftab[lo+1].EntryOff <= pcOff { + lo++ + } + if lo >= len(ftab)-1 { + return len(ftab) - 2 + } + return lo +} + +// LookupFuncIndex mirrors runtime.findfunc's hot lookup: use the bucket and +// subbucket to jump near the target function, then linearly scan the remaining +// entries in that small range. +func LookupFuncIndex(ftab []FuncTabEntry, buckets []FindFuncBucket, pcOff uint32) int { + if len(ftab) < 2 || len(buckets) == 0 { + return -1 + } + bucket := pcOff / FuncTabBucketSize + if bucket >= uint32(len(buckets)) { + return -1 + } + subSize := FuncTabBucketSize / FindFuncSubbucket + sub := (pcOff % FuncTabBucketSize) / subSize + b := buckets[bucket] + idx := int(b.Idx) + int(b.Subbuckets[sub]) + for idx+1 < len(ftab) && ftab[idx+1].EntryOff <= pcOff { + idx++ + } + if idx >= len(ftab)-1 { + return len(ftab) - 2 + } + return idx +} diff --git a/internal/pclntab/pclntab_test.go b/internal/pclntab/pclntab_test.go new file mode 100644 index 0000000000..26903ebbea --- /dev/null +++ b/internal/pclntab/pclntab_test.go @@ -0,0 +1,51 @@ +package pclntab + +import "testing" + +func TestBuildFindFuncBucketsLookup(t *testing.T) { + ftab := []FuncTabEntry{ + {EntryOff: 0, FuncOff: 11}, + {EntryOff: 16, FuncOff: 22}, + {EntryOff: 64, FuncOff: 33}, + {EntryOff: 4096, FuncOff: 44}, + {EntryOff: 4352, FuncOff: 55}, + {EntryOff: 8192, FuncOff: 0}, // sentinel + } + buckets, err := BuildFindFuncBuckets(ftab, 8192) + if err != nil { + t.Fatalf("BuildFindFuncBuckets: %v", err) + } + if got, want := len(buckets), 2; got != want { + t.Fatalf("bucket count = %d, want %d", got, want) + } + for _, tt := range []struct { + pc uint32 + want int + }{ + {pc: 0, want: 0}, + {pc: 15, want: 0}, + {pc: 16, want: 1}, + {pc: 63, want: 1}, + {pc: 64, want: 2}, + {pc: 4095, want: 2}, + {pc: 4096, want: 3}, + {pc: 4351, want: 3}, + {pc: 4352, want: 4}, + {pc: 8191, want: 4}, + } { + if got := LookupFuncIndex(ftab, buckets, tt.pc); got != tt.want { + t.Fatalf("lookup(%d) = %d, want %d", tt.pc, got, tt.want) + } + } +} + +func TestBuildFindFuncBucketsRejectsOverflow(t *testing.T) { + ftab := make([]FuncTabEntry, 0, 302) + for i := 0; i < 301; i++ { + ftab = append(ftab, FuncTabEntry{EntryOff: uint32(i), FuncOff: uint32(i + 1)}) + } + ftab = append(ftab, FuncTabEntry{EntryOff: FuncTabBucketSize, FuncOff: 0}) + if _, err := BuildFindFuncBuckets(ftab, FuncTabBucketSize); err == nil { + t.Fatal("expected subbucket overflow error") + } +} diff --git a/runtime/internal/clite/bdwgc/bdwgc.go b/runtime/internal/clite/bdwgc/bdwgc.go index 9f0bec38e6..4a07b2d44b 100644 --- a/runtime/internal/clite/bdwgc/bdwgc.go +++ b/runtime/internal/clite/bdwgc/bdwgc.go @@ -108,6 +108,9 @@ func GetGCNo() uintptr //go:linkname GetHeapUsageSafe C.GC_get_heap_usage_safe func GetHeapUsageSafe(heapSize, freeBytes, unmappedBytes, bytesSinceGC, totalBytes *uintptr) +//go:linkname ClearStack C.GC_clear_stack +func ClearStack(arg c.Pointer) c.Pointer + //go:linkname GetMemoryUse C.GC_get_memory_use func GetMemoryUse() uintptr diff --git a/runtime/internal/clite/debug/_wrap/debug.c b/runtime/internal/clite/debug/_wrap/debug.c index 32d87903bf..a03fb3ca1c 100644 --- a/runtime/internal/clite/debug/_wrap/debug.c +++ b/runtime/internal/clite/debug/_wrap/debug.c @@ -7,6 +7,7 @@ #endif #include +#include #include void *llgo_address() { @@ -14,10 +15,21 @@ void *llgo_address() { } int llgo_addrinfo(void *addr, Dl_info *info) { - return dladdr(addr, info); + int saved_errno = errno; + int ret = dladdr(addr, info); + errno = saved_errno; + return ret; +} + +void *llgo_symbol(char *name) { + int saved_errno = errno; + void *ret = dlsym(RTLD_DEFAULT, name); + errno = saved_errno; + return ret; } void llgo_stacktrace(int skip, void *ctx, int (*fn)(void *ctx, void *pc, void *offset, void *sp, char *name)) { + int saved_errno = errno; unw_cursor_t cursor; unw_context_t context; unw_word_t offset, pc, sp; @@ -31,11 +43,17 @@ void llgo_stacktrace(int skip, void *ctx, int (*fn)(void *ctx, void *pc, void *o continue; } if (unw_get_reg(&cursor, UNW_REG_IP, &pc) == 0) { - unw_get_proc_name(&cursor, fname, sizeof(fname), &offset); + fname[0] = 0; + offset = 0; + if (unw_get_proc_name(&cursor, fname, sizeof(fname), &offset) == 0) { + fname[sizeof(fname) - 1] = 0; + } unw_get_reg(&cursor, UNW_REG_SP, &sp); if (fn(ctx, (void*)pc, (void*)offset, (void*)sp, fname) == 0) { + errno = saved_errno; return; } } } -} \ No newline at end of file + errno = saved_errno; +} diff --git a/runtime/internal/clite/debug/debug.go b/runtime/internal/clite/debug/debug.go index d35899cd99..d58a5f1941 100644 --- a/runtime/internal/clite/debug/debug.go +++ b/runtime/internal/clite/debug/debug.go @@ -25,6 +25,9 @@ func Address() unsafe.Pointer //go:linkname Addrinfo C.llgo_addrinfo func Addrinfo(addr unsafe.Pointer, info *Info) c.Int +//go:linkname Symbol C.llgo_symbol +func Symbol(name *c.Char) unsafe.Pointer + //go:linkname stacktrace C.llgo_stacktrace func stacktrace(skip c.Int, ctx unsafe.Pointer, fn func(ctx, pc, offset, sp unsafe.Pointer, name *c.Char) c.Int) diff --git a/runtime/internal/lib/runtime/_wrap/runtime.c b/runtime/internal/lib/runtime/_wrap/runtime.c index 4dc23cfd53..2cdda12d1a 100644 --- a/runtime/internal/lib/runtime/_wrap/runtime.c +++ b/runtime/internal/lib/runtime/_wrap/runtime.c @@ -1,3 +1,9 @@ +#if defined(__linux__) && !defined(_GNU_SOURCE) +#define _GNU_SOURCE +#endif + +#include +#include #include int llgo_maxprocs() @@ -8,3 +14,65 @@ int llgo_maxprocs() return 1; #endif } + +void llgo_clobber_pointer_regs(uintptr_t a0, uintptr_t a1, uintptr_t a2, uintptr_t a3, + uintptr_t a4, uintptr_t a5, uintptr_t a6, uintptr_t a7) +{ + volatile uintptr_t sink = a0 | a1 | a2 | a3 | a4 | a5 | a6 | a7; + (void)sink; +} + +void llgo_clear_stack_ptr(uintptr_t target) +{ + if (target == 0) { + return; + } + + volatile uintptr_t marker = 0; + uintptr_t *cur = 0; + uintptr_t *end = 0; + +#if defined(__APPLE__) + void *stackaddr = pthread_get_stackaddr_np(pthread_self()); + size_t stacksize = pthread_get_stacksize_np(pthread_self()); + if (stackaddr != 0 && stacksize != 0) { + uintptr_t *mark = (uintptr_t *)▮ + uintptr_t *lo = (uintptr_t *)((char *)stackaddr - stacksize); + uintptr_t *hi = (uintptr_t *)stackaddr; + if (mark >= lo && mark < hi) { + cur = lo; + end = hi; + } else { + lo = (uintptr_t *)stackaddr; + hi = (uintptr_t *)((char *)stackaddr + stacksize); + if (mark >= lo && mark < hi) { + cur = lo; + end = hi; + } + } + } +#elif defined(__linux__) + pthread_attr_t attr; + void *stackaddr = 0; + size_t stacksize = 0; + if (pthread_getattr_np(pthread_self(), &attr) == 0) { + if (pthread_attr_getstack(&attr, &stackaddr, &stacksize) == 0) { + cur = (uintptr_t *)stackaddr; + end = (uintptr_t *)((char *)stackaddr + stacksize); + } + pthread_attr_destroy(&attr); + } +#endif + + if (cur == 0 || end == 0 || end <= cur) { + return; + } + if ((uintptr_t *)target >= cur && (uintptr_t *)target < end) { + return; + } + for (; cur < end; cur++) { + if (*cur == target) { + *cur = 0; + } + } +} diff --git a/runtime/internal/lib/runtime/extern.go b/runtime/internal/lib/runtime/extern.go index 1fb397dd8a..66f95de36f 100644 --- a/runtime/internal/lib/runtime/extern.go +++ b/runtime/internal/lib/runtime/extern.go @@ -6,19 +6,44 @@ package runtime import ( clitedebug "github.com/goplus/llgo/runtime/internal/clite/debug" + rtdebug "github.com/goplus/llgo/runtime/internal/runtime" ) func Caller(skip int) (pc uintptr, file string, line int, ok bool) { - // llgo currently doesn't have reliable source file/line mapping from PC. - // Return a stable placeholder location so stdlib log/testing can proceed. + if frame, ok := rtdebug.Caller(skip); ok { + file = frame.File + line = frame.Line + if file == "" { + file = "???" + } + if line == 0 { + line = 1 + } + return frame.PC, file, line, true + } var pcs [1]uintptr - if Callers(skip+1, pcs[:]) < 1 { + if Callers(skip+2, pcs[:]) < 1 { return 0, "", 0, false } - return pcs[0], "???", 1, true + sym := frameSymbol(pcs[0]) + file, line = sym.file, sym.line + if file == "" { + file = "???" + } + if line == 0 { + line = 1 + } + return pcs[0], file, line, true } func Callers(skip int, pc []uintptr) int { + if n := rtdebug.Callers(skip, pc); n > 0 { + return n + } + return callers(skip+1, pc) +} + +func callers(skip int, pc []uintptr) int { if len(pc) == 0 { return 0 } @@ -28,6 +53,8 @@ func Callers(skip int, pc []uintptr) int { return false } pc[n] = fr.PC + recordFrameSymbol(fr.PC, fr.Offset, fr.Name) + rtdebug.BindCallerLocation(fr.PC, fr.Name) n++ return true }) diff --git a/runtime/internal/lib/runtime/mfinal.go b/runtime/internal/lib/runtime/mfinal.go index 7ed607e65f..b2015bf3c5 100644 --- a/runtime/internal/lib/runtime/mfinal.go +++ b/runtime/internal/lib/runtime/mfinal.go @@ -44,14 +44,14 @@ func initFinalizerState() { } func SetFinalizer(obj any, finalizer any) { - objFace := (*eface)(unsafe.Pointer(&obj)) + objFace := *(*eface)(unsafe.Pointer(&obj)) if objFace._type == nil { throw("runtime.SetFinalizer: first argument is nil") } if objFace._type.Kind() != abi.Pointer { throw("runtime.SetFinalizer: first argument is " + objFace._type.String() + ", not pointer") } - objPtr := ifacePointerData(objFace) + objPtr := ifacePointerData(&objFace) if objPtr == nil { throw("runtime.SetFinalizer: first argument is nil") } @@ -67,7 +67,7 @@ func SetFinalizer(obj any, finalizer any) { } finalizerState.mu.Unlock() - finalizerFace := (*eface)(unsafe.Pointer(&finalizer)) + finalizerFace := *(*eface)(unsafe.Pointer(&finalizer)) if finalizerFace._type == nil { return } diff --git a/runtime/internal/lib/runtime/nanotime_darwin_llgo.go b/runtime/internal/lib/runtime/nanotime_darwin_llgo.go new file mode 100644 index 0000000000..7d487edadf --- /dev/null +++ b/runtime/internal/lib/runtime/nanotime_darwin_llgo.go @@ -0,0 +1,36 @@ +//go:build darwin && !baremetal + +/* + * 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 runtime + +import ( + _ "unsafe" +) + +// Mirrors Go's runtime.nanotime1 on Darwin (sys_darwin.go): read +// CLOCK_UPTIME_RAW through clock_gettime_nsec_np. Darwin serves +// clock_gettime(CLOCK_MONOTONIC) with only microsecond granularity, while +// CLOCK_UPTIME_RAW is mach_absolute_time with full nanosecond resolution. +const _CLOCK_UPTIME_RAW = 8 + +//go:linkname c_clock_gettime_nsec_np C.clock_gettime_nsec_np +func c_clock_gettime_nsec_np(clockID int32) uint64 + +func nanotime1() int64 { + return int64(c_clock_gettime_nsec_np(_CLOCK_UPTIME_RAW)) +} diff --git a/runtime/internal/lib/runtime/nanotime_linux_llgo.go b/runtime/internal/lib/runtime/nanotime_linux_llgo.go new file mode 100644 index 0000000000..a07def21eb --- /dev/null +++ b/runtime/internal/lib/runtime/nanotime_linux_llgo.go @@ -0,0 +1,40 @@ +//go:build linux && !baremetal + +/* + * 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 runtime + +import ( + "unsafe" + + c "github.com/goplus/llgo/runtime/internal/clite" + ct "github.com/goplus/llgo/runtime/internal/clite/time" +) + +// Linux CLOCK_MONOTONIC (see ), which has nanosecond +// resolution. Deliberately a local constant: ct.CLOCK_MONOTONIC carries +// Darwin's id (6), which Linux interprets as CLOCK_MONOTONIC_COARSE — a +// millisecond-granularity clock that quantized every monotonic timestamp +// the runtime produced. +const _CLOCK_MONOTONIC = 1 + +// nanotime1 mirrors Go's runtime.nanotime1 on Linux. +func nanotime1() int64 { + tv := (*ct.Timespec)(c.Alloca(unsafe.Sizeof(ct.Timespec{}))) + ct.ClockGettime(ct.ClockidT(_CLOCK_MONOTONIC), tv) + return int64(tv.Sec)*1e9 + int64(tv.Nsec) +} diff --git a/runtime/internal/lib/runtime/nanotime_other_llgo.go b/runtime/internal/lib/runtime/nanotime_other_llgo.go new file mode 100644 index 0000000000..8478b3ec8f --- /dev/null +++ b/runtime/internal/lib/runtime/nanotime_other_llgo.go @@ -0,0 +1,33 @@ +//go:build !darwin && !linux && !baremetal + +/* + * 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 runtime + +import ( + "unsafe" + + c "github.com/goplus/llgo/runtime/internal/clite" + ct "github.com/goplus/llgo/runtime/internal/clite/time" +) + +// nanotime1 keeps the previous behavior on remaining platforms. +func nanotime1() int64 { + tv := (*ct.Timespec)(c.Alloca(unsafe.Sizeof(ct.Timespec{}))) + ct.ClockGettime(ct.CLOCK_MONOTONIC, tv) + return int64(tv.Sec)*1e9 + int64(tv.Nsec) +} diff --git a/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go b/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go index 5b8155d0e8..3da596f877 100644 --- a/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go +++ b/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go @@ -84,6 +84,155 @@ func NumGoroutine() int { func SetCPUProfileRate(hz int) {} +const funcForPCCacheSets = 1024 +const funcForPCCacheWays = 4 + +type funcForPCCacheEntry struct { + pc uintptr + fn *Func +} + +var funcForPCCache [funcForPCCacheSets][funcForPCCacheWays]funcForPCCacheEntry +var funcForPCCacheNext [funcForPCCacheSets]uint8 +var funcForPCLast funcForPCCacheEntry + func FuncForPC(pc uintptr) *Func { - return nil + if fn := funcForPCLast.fn; fn != nil && funcForPCLast.pc == pc { + return fn + } + set := &funcForPCCache[funcForPCCacheIndex(pc)] + for i := 0; i < funcForPCCacheWays; i++ { + if fn := set[i].fn; fn != nil && set[i].pc == pc { + funcForPCLast = funcForPCCacheEntry{pc: pc, fn: fn} + return fn + } + } + return funcForPCSlow(pc) +} + +func funcForPCSlow(pc uintptr) *Func { + if pc&3 != 0 { + if sym := frameSymbol(pc); sym.ok { + fn := newFuncForPC(pc, sym) + cacheFuncForPC(pc, fn) + return fn + } + } else if pc != 0 { + // Cold fast path: before the entry frame table has been built, resolve + // an exact function-entry PC without paying first-use table + // construction. First a bounded linear scan of the raw entry-site + // section (compile-time data, no dynamic-loader query), then one + // dladdr as fallback. Requiring an exact entry match means a + // stripped-local misattribution (dladdr returning the nearest + // exported symbol) can never be accepted, so this path only ever + // answers true function-value PCs. The path is intentionally capped: + // each cold lookup costs microseconds, so after a handful of them the + // sorted table is the cheaper answer and we fall through to build it. + if !runtimeFuncPCFramesBuilt() && coldFuncPCLookupBudget() { + if sym, ok := coldFuncInfoEntryLookup(pc); ok { + fn := newFuncForPC(pc, sym) + cacheFuncForPC(pc, fn) + return fn + } + if sym := addrInfoSymbol(pc); sym.ok && sym.entry == pc && sym.function != "" { + fn := newFuncForPC(pc, sym) + cacheFuncForPC(pc, fn) + return fn + } + } + // Function-value PCs point at the real function entry. ELF funcinfo + // entry-site anchors are emitted from LLVM IR and can land after the + // backend prologue, so an exact entry PC may sort before its anchor. + // Prefer the section table when it can match within the entry slack; + // native symbol lookup is kept only as a fallback. + if sym, ok := funcPCFrameForEntryPC(pc); ok { + fn := newFuncForPC(pc, sym) + cacheFuncForPC(pc, fn) + return fn + } + if sym := addrInfoSymbol(pc); sym.ok && sym.entry == pc && sym.function != "" { + fn := newFuncForPC(pc, sym) + cacheFuncForPC(pc, fn) + return fn + } + } + if sym, ok := funcPCFrameForPC(pc); ok { + fn := newFuncForPC(pc, sym) + cacheFuncForPC(pc, fn) + return fn + } + sym := frameSymbol(pc) + fn := newFuncForPC(pc, sym) + cacheFuncForPC(pc, fn) + return fn +} + +func newFuncForPC(pc uintptr, sym pcSymbol) *Func { + if !sym.ok && sym.function == "" { + return &Func{entry: pc, name: unknownFunctionName(pc), pc: pc} + } + name := sym.function + if name == "" { + name = unknownFunctionName(pc) + } + entry := sym.entry + if entry == 0 { + entry = pc + } + return &Func{ + entry: entry, + name: name, + pc: pc, + file: sym.file, + line: sym.line, + } +} + +// frameFuncForPC returns the *Func for a frame PC that Frames.Next already +// symbolized, going through the FuncForPC cache so repeated CallersFrames +// walks over the same PCs stop allocating a Func per frame. +func frameFuncForPC(pc uintptr, sym pcSymbol, name string) *Func { + if fn := funcForPCLast.fn; fn != nil && funcForPCLast.pc == pc { + return fn + } + set := &funcForPCCache[funcForPCCacheIndex(pc)] + for i := 0; i < funcForPCCacheWays; i++ { + if fn := set[i].fn; fn != nil && set[i].pc == pc { + return fn + } + } + fn := &Func{ + entry: sym.entry, + name: name, + pc: pc, + file: sym.file, + line: sym.line, + } + // FuncForPC's own constructor falls back to entry == pc; keep frames with + // an unresolved entry out of the shared cache so a later FuncForPC(pc) + // does not observe Entry() == 0. + if sym.entry != 0 { + cacheFuncForPC(pc, fn) + } + return fn +} + +func cacheFuncForPC(pc uintptr, fn *Func) { + setIndex := funcForPCCacheIndex(pc) + set := &funcForPCCache[setIndex] + for i := 0; i < funcForPCCacheWays; i++ { + if set[i].fn == nil || set[i].pc == pc { + set[i] = funcForPCCacheEntry{pc: pc, fn: fn} + funcForPCLast = set[i] + return + } + } + way := funcForPCCacheNext[setIndex] & (funcForPCCacheWays - 1) + funcForPCCacheNext[setIndex] = way + 1 + set[way] = funcForPCCacheEntry{pc: pc, fn: fn} + funcForPCLast = set[way] +} + +func funcForPCCacheIndex(pc uintptr) uintptr { + return (pc >> 4) & (funcForPCCacheSets - 1) } diff --git a/runtime/internal/lib/runtime/runtime2.go b/runtime/internal/lib/runtime/runtime2.go index 7327f9892c..8bf049e087 100644 --- a/runtime/internal/lib/runtime/runtime2.go +++ b/runtime/internal/lib/runtime/runtime2.go @@ -17,7 +17,55 @@ type _func struct { } func Stack(buf []byte, all bool) int { - return 0 + var pcs [64]uintptr + n := Callers(0, pcs[:]) + out := make([]byte, 0, 1024) + out = append(out, "goroutine 1 [running]:\n"...) + frames := CallersFrames(pcs[:n]) + for { + frame, more := frames.Next() + if frame.Function == "" { + frame.Function = unknownFunctionName(frame.PC) + } + out = append(out, frame.Function...) + out = append(out, "()\n\t"...) + if frame.File == "" { + out = append(out, "???"...) + } else { + out = append(out, frame.File...) + } + out = append(out, ':') + out = appendInt(out, frame.Line) + out = append(out, ' ') + out = append(out, "+0x0\n"...) + if !more { + break + } + } + if len(out) > len(buf) { + copy(buf, out[:len(buf)]) + return len(buf) + } + copy(buf, out) + return len(out) +} + +func appendInt(out []byte, v int) []byte { + if v == 0 { + return append(out, '0') + } + if v < 0 { + out = append(out, '-') + v = -v + } + var digits [20]byte + i := len(digits) + for v > 0 { + i-- + digits[i] = byte('0' + v%10) + v /= 10 + } + return append(out, digits[i:]...) } type traceError string diff --git a/runtime/internal/lib/runtime/runtime_gc.go b/runtime/internal/lib/runtime/runtime_gc.go index d8656f93a4..810d076ebd 100644 --- a/runtime/internal/lib/runtime/runtime_gc.go +++ b/runtime/internal/lib/runtime/runtime_gc.go @@ -36,11 +36,13 @@ func ReadMemStats(m *runtime.MemStats) { } func GC() { + bdwgc.ClearStack(nil) bdwgc.Gcollect() runFinalizers() // BDW finalizers are observed on a subsequent collection cycle. // Run one extra cycle so weak-pointer cleanup hooks (unique/weak) see // finalized state before we trigger map cleanup callbacks. + bdwgc.ClearStack(nil) bdwgc.Gcollect() runFinalizers() unique_runtime_notifyMapCleanup() diff --git a/runtime/internal/lib/runtime/symtab.go b/runtime/internal/lib/runtime/symtab.go index a4d18d9b30..37d5606c38 100644 --- a/runtime/internal/lib/runtime/symtab.go +++ b/runtime/internal/lib/runtime/symtab.go @@ -9,6 +9,9 @@ import ( c "github.com/goplus/llgo/runtime/internal/clite" clitedebug "github.com/goplus/llgo/runtime/internal/clite/debug" + cliteos "github.com/goplus/llgo/runtime/internal/clite/os" + latomic "github.com/goplus/llgo/runtime/internal/lib/sync/atomic" + rtdebug "github.com/goplus/llgo/runtime/internal/runtime" ) // Frames may be used to get function/file/line information for a @@ -105,6 +108,1874 @@ func unknownFunctionName(pc uintptr) string { return "pc=" + uintptrHex(pc) } +type pcSymbol struct { + pc uintptr + entry uintptr + function string + file string + line int + startLine int + ok bool +} + +type frameSymbolCacheEntry struct { + pc uintptr + offset uintptr + name string +} + +const frameSymbolCacheSize = 128 + +var frameSymbolCache [frameSymbolCacheSize]frameSymbolCacheEntry + +func recordFrameSymbol(pc, offset uintptr, name string) { + if pc == 0 || name == "" || isPCSiteSymbol(name) { + return + } + i := (pc >> 4) & (frameSymbolCacheSize - 1) + frameSymbolCache[i] = frameSymbolCacheEntry{pc: pc, offset: offset, name: name} +} + +type runtimeFuncInfoRecord struct { + symbolPkg uint16 + symbolName uint16 + namePkg uint16 + nameName uint16 + fileRoot uint16 + fileName uint16 + line uint32 +} + +//go:linkname runtimeFuncInfoTable __llgo_funcinfo_table +var runtimeFuncInfoTable *runtimeFuncInfoRecord + +//go:linkname runtimeFuncInfoStrings __llgo_funcinfo_strings +var runtimeFuncInfoStrings *c.Char + +//go:linkname runtimeFuncInfoStringOffsets __llgo_funcinfo_string_offsets +var runtimeFuncInfoStringOffsets *uint32 + +//go:linkname runtimeFuncInfoStringCount __llgo_funcinfo_string_count +var runtimeFuncInfoStringCount uintptr + +//go:linkname runtimeFuncInfoHash __llgo_funcinfo_hash +var runtimeFuncInfoHash *uint16 + +//go:linkname runtimeFuncInfoCount __llgo_funcinfo_count +var runtimeFuncInfoCount uintptr + +//go:linkname runtimeFuncInfoHashMask __llgo_funcinfo_hash_mask +var runtimeFuncInfoHashMask uintptr + +type runtimeFuncInfoSymbolIndexRecord struct { + symbolID uint64 + funcIndex uint32 +} + +//go:linkname runtimeFuncInfoSymbolIndex __llgo_funcinfo_symbol_index +var runtimeFuncInfoSymbolIndex *runtimeFuncInfoSymbolIndexRecord + +//go:linkname runtimeFuncInfoSymbolIndexCount __llgo_funcinfo_symbol_index_count +var runtimeFuncInfoSymbolIndexCount uintptr + +//go:linkname runtimeFuncInfoStubIndexes __llgo_funcinfo_stub_indexes +var runtimeFuncInfoStubIndexes *uint32 + +//go:linkname runtimeFuncInfoStubCount __llgo_funcinfo_stub_count +var runtimeFuncInfoStubCount uintptr + +type runtimeFuncInfoEntryRecord struct { + pc uintptr + symbolID uint64 +} + +//go:linkname runtimeFuncInfoEntryStart __llgo_funcinfo_entry_start +var runtimeFuncInfoEntryStart *runtimeFuncInfoEntryRecord + +//go:linkname runtimeFuncInfoEntryEnd __llgo_funcinfo_entry_end +var runtimeFuncInfoEntryEnd *runtimeFuncInfoEntryRecord + +type runtimeFuncInfoStubSiteRecord struct { + pc uintptr + symbolID uint64 +} + +//go:linkname runtimeFuncInfoStubSiteStart __llgo_funcinfo_stubsite_start +var runtimeFuncInfoStubSiteStart *runtimeFuncInfoStubSiteRecord + +//go:linkname runtimeFuncInfoStubSiteEnd __llgo_funcinfo_stubsite_end +var runtimeFuncInfoStubSiteEnd *runtimeFuncInfoStubSiteRecord + +type runtimePCLineRecord struct { + id uint64 + funcIndex uint32 + file uint32 + line uint32 +} + +//go:linkname runtimePCLineTable __llgo_pcline_table +var runtimePCLineTable *runtimePCLineRecord + +//go:linkname runtimePCLineCount __llgo_pcline_count +var runtimePCLineCount uintptr + +type runtimePCSiteRecord struct { + pc uintptr + id uint64 +} + +//go:linkname runtimePCSiteStart __llgo_pcsite_start +var runtimePCSiteStart *runtimePCSiteRecord + +//go:linkname runtimePCSiteEnd __llgo_pcsite_end +var runtimePCSiteEnd *runtimePCSiteRecord + +type runtimePCLineFrame struct { + pc uintptr + entry uintptr + function string + file string + line int + startLine int +} + +var runtimePCLineInitState uint32 +var runtimePCLineFrames []runtimePCLineFrame +var runtimePCLineIndex runtimePCFindIndex + +type runtimeFuncPCFrame struct { + entry uintptr + funcIndex uint32 +} + +type runtimePCFindBucket struct { + idx uint32 + subbuckets [runtimePCFindSubbucket]uint16 +} + +type runtimePCFindIndex struct { + base uintptr + buckets []runtimePCFindBucket +} + +const ( + // Keep the lookup geometry aligned with Go's pclntab findfunc table: + // 4096-byte buckets and 16 subbuckets. Go stores one-byte subbucket + // deltas because its linker guarantees a 16-byte minimum function size; + // LLGo has no minimum size for function entries and indexes call-site + // records that can sit a few bytes apart, so it stores two-byte deltas. + // A delta counts distinct PCs inside one 4096-byte bucket and therefore + // can never exceed 4096, which makes uint16 overflow impossible and the + // index unconditional. LLGo builds the index at first use after reading + // DCE-safe entry PC sections, because the LLVM IR stage does not yet own + // final text addresses the way cmd/link does for Go. + runtimePCMinFuncSize = uintptr(16) + runtimePCFindBucketSize = uintptr(256) * runtimePCMinFuncSize + runtimePCFindSubbucket = 16 + runtimeFuncPCEntrySlack = 64 +) + +var runtimeFuncPCInitState uint32 +var runtimeFuncPCFrames []runtimeFuncPCFrame +var runtimeFuncPCEntries []uintptr +var runtimeFuncPCIndex runtimePCFindIndex + +const ( + runtimeFuncInfoInitUninit uint32 = iota + runtimeFuncInfoInitDone + runtimeFuncInfoInitBusy + runtimeClosureStubPrefix = "__llgo_stub." + runtimePublicClosureStubPrefix = "_llgo_stub." +) + +func hasStringPrefix(s, prefix string) bool { + if len(s) < len(prefix) { + return false + } + for i := 0; i < len(prefix); i++ { + if s[i] != prefix[i] { + return false + } + } + return true +} + +func isPCSiteSymbol(name string) bool { + for i := 0; i < len(name) && name[i] == '_'; i++ { + if hasStringPrefix(name[i:], "__llgo_pcsite_") { + return true + } + } + return false +} + +func publicFunctionName(name string) string { + const commandLineArguments = "command-line-arguments." + if hasStringPrefix(name, commandLineArguments) { + return "main." + name[len(commandLineArguments):] + } + if len(name) > 0 && name[0] == '_' { + name = name[1:] + } + return name +} + +func cStringCompare(cstr *c.Char, s string) int { + if cstr == nil { + if s == "" { + return 0 + } + return -1 + } + ptr := unsafe.Pointer(cstr) + for i := 0; ; i++ { + c := *(*byte)(unsafe.Add(ptr, i)) + if i == len(s) { + if c == 0 { + return 0 + } + return 1 + } + if c == 0 { + return -1 + } + if c < s[i] { + return -1 + } + if c > s[i] { + return 1 + } + } +} + +func cStringLen(cstr *c.Char) int { + if cstr == nil { + return 0 + } + ptr := unsafe.Pointer(cstr) + for i := 0; ; i++ { + if *(*byte)(unsafe.Add(ptr, i)) == 0 { + return i + } + } +} + +func cStringAppend(dst []byte, cstr *c.Char) []byte { + if cstr == nil { + return dst + } + ptr := unsafe.Pointer(cstr) + for i := 0; ; i++ { + c := *(*byte)(unsafe.Add(ptr, i)) + if c == 0 { + return dst + } + dst = append(dst, c) + } +} + +func funcInfoCString(id uint16) *c.Char { + if runtimeFuncInfoStrings == nil || runtimeFuncInfoStringOffsets == nil || + uintptr(id) >= runtimeFuncInfoStringCount { + return nil + } + off := *(*uint32)(unsafe.Add(unsafe.Pointer(runtimeFuncInfoStringOffsets), uintptr(id)*unsafe.Sizeof(*runtimeFuncInfoStringOffsets))) + return (*c.Char)(unsafe.Add(unsafe.Pointer(runtimeFuncInfoStrings), uintptr(off))) +} + +func funcInfoAt(i uintptr) *runtimeFuncInfoRecord { + size := unsafe.Sizeof(*runtimeFuncInfoTable) + return (*runtimeFuncInfoRecord)(unsafe.Add(unsafe.Pointer(runtimeFuncInfoTable), i*size)) +} + +func pcLineAt(i uintptr) *runtimePCLineRecord { + size := unsafe.Sizeof(*runtimePCLineTable) + return (*runtimePCLineRecord)(unsafe.Add(unsafe.Pointer(runtimePCLineTable), i*size)) +} + +func funcInfoStubIndexAt(i uintptr) uint32 { + size := unsafe.Sizeof(*runtimeFuncInfoStubIndexes) + return *(*uint32)(unsafe.Add(unsafe.Pointer(runtimeFuncInfoStubIndexes), i*size)) +} + +func funcInfoHashString(s string) uintptr { + const ( + offset = uint32(2166136261) + prime = uint32(16777619) + ) + h := offset + for i := 0; i < len(s); i++ { + h ^= uint32(s[i]) + h *= prime + } + return uintptr(h) +} + +func funcInfoSymbolEqual(rec *runtimeFuncInfoRecord, symbol string) bool { + pkg := funcInfoCString(rec.symbolPkg) + name := funcInfoCString(rec.symbolName) + pkgLen := cStringLen(pkg) + nameLen := cStringLen(name) + if pkgLen == 0 { + return cStringCompare(name, symbol) == 0 + } + if len(symbol) != pkgLen+1+nameLen { + return false + } + if cStringCompare(pkg, symbol[:pkgLen]) != 0 || symbol[pkgLen] != '.' { + return false + } + return cStringCompare(name, symbol[pkgLen+1:]) == 0 +} + +func funcInfoJoinName(pkgID, nameID uint16) string { + pkg := funcInfoCString(pkgID) + name := funcInfoCString(nameID) + pkgLen := cStringLen(pkg) + nameLen := cStringLen(name) + if pkgLen == 0 { + return safeGoString(name, "") + } + if nameLen == 0 { + return safeGoString(pkg, "") + } + buf := make([]byte, 0, pkgLen+1+nameLen) + buf = cStringAppend(buf, pkg) + buf = append(buf, '.') + buf = cStringAppend(buf, name) + return string(buf) +} + +func funcInfoNameLen(pkgID, nameID uint16) int { + pkgLen := cStringLen(funcInfoCString(pkgID)) + nameLen := cStringLen(funcInfoCString(nameID)) + if pkgLen == 0 { + return nameLen + } + if nameLen == 0 { + return pkgLen + } + return pkgLen + 1 + nameLen +} + +func appendFuncInfoName(dst []byte, pkgID, nameID uint16) []byte { + pkg := funcInfoCString(pkgID) + name := funcInfoCString(nameID) + pkgLen := cStringLen(pkg) + nameLen := cStringLen(name) + if pkgLen == 0 { + return cStringAppend(dst, name) + } + if nameLen == 0 { + return cStringAppend(dst, pkg) + } + dst = cStringAppend(dst, pkg) + dst = append(dst, '.') + return cStringAppend(dst, name) +} + +func funcInfoJoinFile(rootID, nameID uint16) string { + root := funcInfoCString(rootID) + name := funcInfoCString(nameID) + rootLen := cStringLen(root) + nameLen := cStringLen(name) + if rootLen == 0 { + return safeGoString(name, "") + } + if nameLen == 0 { + return safeGoString(root, "") + } + buf := make([]byte, 0, rootLen+nameLen) + buf = cStringAppend(buf, root) + buf = cStringAppend(buf, name) + return string(buf) +} + +func funcInfoPackedFile(file uint32) string { + return funcInfoJoinFile(uint16(file>>16), uint16(file)) +} + +func maxFuncInfoSymbolLen() int { + maxLen := 0 + for i := uintptr(0); i < runtimeFuncInfoCount; i++ { + fn := funcInfoAt(i) + if n := funcInfoNameLen(fn.symbolPkg, fn.symbolName); n > maxLen { + maxLen = n + } + } + return maxLen +} + +func symbolPCBytes(name []byte) uintptr { + if len(name) == 0 { + return 0 + } + name = append(name, 0) + return uintptr(clitedebug.Symbol((*c.Char)(unsafe.Pointer(&name[0])))) +} + +func symbolPCFuncInfoName(buf []byte, pkgID, nameID uint16) uintptr { + name := appendFuncInfoName(buf[:0], pkgID, nameID) + return symbolPCBytes(name) +} + +func symbolPCPrefixedFuncInfoName(buf []byte, prefix string, pkgID, nameID uint16) uintptr { + name := append(buf[:0], prefix...) + name = appendFuncInfoName(name, pkgID, nameID) + return symbolPCBytes(name) +} + +func funcInfoFunctionName(fn *runtimeFuncInfoRecord) string { + if fn == nil { + return "" + } + if function := publicFunctionName(funcInfoJoinName(fn.namePkg, fn.nameName)); function != "" { + return function + } + return publicFunctionName(funcInfoJoinName(fn.symbolPkg, fn.symbolName)) +} + +func funcInfoFileName(fn *runtimeFuncInfoRecord) string { + if fn == nil { + return "" + } + return funcInfoJoinFile(fn.fileRoot, fn.fileName) +} + +func funcInfoForSymbol(symbol string) *runtimeFuncInfoRecord { + if symbol == "" || runtimeFuncInfoTable == nil || runtimeFuncInfoCount == 0 { + return nil + } + if runtimeFuncInfoStrings == nil || runtimeFuncInfoStringOffsets == nil || runtimeFuncInfoCount > 1<<20 || runtimeFuncInfoHashMask > 1<<22 { + return nil + } + if runtimeFuncInfoHash != nil && runtimeFuncInfoHashMask != 0 { + slot := funcInfoHashString(symbol) & runtimeFuncInfoHashMask + for probes := uintptr(0); probes <= runtimeFuncInfoHashMask; probes++ { + idx := *(*uint16)(unsafe.Add(unsafe.Pointer(runtimeFuncInfoHash), slot*unsafe.Sizeof(*runtimeFuncInfoHash))) + if idx == 0 { + return nil + } + if uintptr(idx) <= runtimeFuncInfoCount { + rec := funcInfoAt(uintptr(idx) - 1) + if funcInfoSymbolEqual(rec, symbol) { + return rec + } + } + slot = (slot + 1) & runtimeFuncInfoHashMask + } + return nil + } + for i := uintptr(0); i < runtimeFuncInfoCount; i++ { + rec := funcInfoAt(i) + if funcInfoSymbolEqual(rec, symbol) { + return rec + } + } + return nil +} + +func funcInfoForRuntimeSymbol(symbol string) *runtimeFuncInfoRecord { + if rec := funcInfoForSymbol(symbol); rec != nil { + return rec + } + if hasStringPrefix(symbol, runtimeClosureStubPrefix) { + return funcInfoForSymbol(symbol[len(runtimeClosureStubPrefix):]) + } + if hasStringPrefix(symbol, runtimePublicClosureStubPrefix) { + return funcInfoForSymbol(symbol[len(runtimePublicClosureStubPrefix):]) + } + return nil +} + +func applyFuncInfo(sym *pcSymbol, rawFunction string) { + rec := funcInfoForRuntimeSymbol(rawFunction) + if rec == nil { + public := publicFunctionName(rawFunction) + if public != rawFunction { + rec = funcInfoForRuntimeSymbol(public) + } + } + if rec == nil { + return + } + if name := funcInfoJoinName(rec.namePkg, rec.nameName); name != "" { + sym.function = publicFunctionName(name) + } + if file := funcInfoJoinFile(rec.fileRoot, rec.fileName); file != "" { + if sym.file == "" { + sym.file = file + } + } + if rec.line != 0 { + sym.startLine = int(rec.line) + if sym.line == 0 { + sym.line = int(rec.line) + } + } + sym.ok = sym.ok || sym.function != "" || sym.file != "" +} + +func cachedFrameSymbol(pc uintptr) pcSymbol { + i := (pc >> 4) & (frameSymbolCacheSize - 1) + entry := frameSymbolCache[i] + if entry.pc != pc || entry.name == "" { + return pcSymbol{pc: pc} + } + rawFn := entry.name + if isPCSiteSymbol(rawFn) { + return pcSymbol{pc: pc} + } + fn := publicFunctionName(rawFn) + sym := pcSymbol{ + pc: pc, + entry: pc - entry.offset, + function: fn, + ok: fn != "" || entry.offset != 0, + } + applyFuncInfo(&sym, rawFn) + return sym +} + +func addrInfoSymbol(pc uintptr) pcSymbol { + var info clitedebug.Info + if clitedebug.Addrinfo(unsafe.Pointer(pc), &info) == 0 { + return cachedFrameSymbol(pc) + } + rawFn := safeGoString(info.Sname, "") + if isPCSiteSymbol(rawFn) { + return pcSymbol{pc: pc} + } + if rawFn == "" { + if sym := cachedFrameSymbol(pc); sym.ok { + return sym + } + } + fn := publicFunctionName(rawFn) + sym := pcSymbol{ + pc: pc, + entry: uintptr(info.Saddr), + function: fn, + ok: fn != "" || info.Saddr != nil, + } + applyFuncInfo(&sym, rawFn) + return sym +} + +func initRuntimeFuncPCFrames() { + if latomic.LoadUint32(&runtimeFuncPCInitState) == runtimeFuncInfoInitDone { + return + } + initRuntimeFuncPCFramesSlow() +} + +// runtimeFuncPCFramesBuilt reports whether the entry frame table has already +// been constructed, without triggering its construction. +func runtimeFuncPCFramesBuilt() bool { + return latomic.LoadUint32(&runtimeFuncPCInitState) == runtimeFuncInfoInitDone +} + +// Set LLGO_FUNCINFO_DEBUG=1 to print one line per lazily built runtime +// metadata table. This is how benchmarks and bug reports can tell whether a +// lookup used the compact find index or a degraded full-table fallback. +var runtimeFuncInfoDebugState uint32 + +var runtimeFuncPCFramesFromSites bool +var runtimeFuncPCStubsFromSites bool + +func runtimeFuncInfoDebugEnabled() bool { + state := latomic.LoadUint32(&runtimeFuncInfoDebugState) + if state == 0 { + state = 1 + if p := cliteos.Getenv(c.AllocaCStr("LLGO_FUNCINFO_DEBUG")); p != nil { + if v := c.GoString(p); v != "" && v != "0" { + state = 2 + } + } + latomic.StoreUint32(&runtimeFuncInfoDebugState, state) + } + return state == 2 +} + +func runtimeFuncInfoDebugSource(fromSites bool) string { + if fromSites { + return "sites" + } + return "dlsym" +} + +func runtimeFuncInfoDebugIndex(index runtimePCFindIndex) string { + if len(index.buckets) != 0 { + return "built" + } + return "fallback" +} + +func reportRuntimeFuncPCDebug() { + if !runtimeFuncInfoDebugEnabled() { + return + } + entrySrc := runtimeFuncInfoDebugSource(runtimeFuncPCFramesFromSites) + stubSrc := runtimeFuncInfoDebugSource(runtimeFuncPCStubsFromSites) + if runtimeFuncPCFramesPrebuilt { + entrySrc = "prebuilt" + stubSrc = "prebuilt" + } + frameCount := len(runtimeFuncPCFrames) + if runtimeFuncPCFramesPrebuilt { + frameCount = prebuiltFrameCount() + } + println("llgo funcinfo: func table frames=", frameCount, + " buckets=", len(runtimeFuncPCIndex.buckets), + " index=", runtimeFuncInfoDebugIndex(runtimeFuncPCIndex), + " entries=", entrySrc, + " stubs=", stubSrc) +} + +func reportRuntimePCLineDebug() { + if !runtimeFuncInfoDebugEnabled() { + return + } + println("llgo funcinfo: pcline table frames=", len(runtimePCLineFrames), + " buckets=", len(runtimePCLineIndex.buckets), + " index=", runtimeFuncInfoDebugIndex(runtimePCLineIndex)) +} + +func initRuntimeFuncPCFramesSlow() { + for { + state := latomic.LoadUint32(&runtimeFuncPCInitState) + switch state { + case runtimeFuncInfoInitDone: + return + case runtimeFuncInfoInitUninit: + if latomic.CompareAndSwapUint32(&runtimeFuncPCInitState, runtimeFuncInfoInitUninit, runtimeFuncInfoInitBusy) { + initRuntimeFuncPCFramesOnce() + latomic.StoreUint32(&runtimeFuncPCInitState, runtimeFuncInfoInitDone) + reportRuntimeFuncPCDebug() + return + } + } + c.Usleep(1) + } +} + +// Prebuilt table format written into the entry-site section by the +// link-phase tool (chore/pclnpost -write). Layout, all little-endian, +// 8-byte aligned at the section start: +// +// u64 magic "LLGOFTB1" +// u64 linkSectAddr link-time vmaddr of this section (informational) +// u64 base runtime PC of the first table entry +// u32 count ftab entries incl. trailing sentinel +// u32 bucketCount findfunctab buckets (runtime uint16 layout) +// count × {u32 entryOff /* relative to base */, u32 funcIndex} +// bucketCount × {u32 idx; 16 × u16 subbuckets} +// +// The base slot is a live relocation: on Mach-O the rewriter splices it back +// into the dyld chained-fixup page chain (so dyld both pre-touches the +// table's pages at load and writes the slid address), and on non-PIE ELF the +// link-time value already equals the runtime address. Either way the slot +// holds a runtime PC — no slide arithmetic here. +// +// The tool sorts, deduplicates LTO inline copies against the symbol table, +// and normalizes entries to true symbol starts, so adopting the table also +// retires first-use sorting and the dlsym/stub fallbacks. +const runtimePrebuiltMagic = uint64(0x314254464F474C4C) // "LLGOFTB1" little-endian +const runtimePrebuiltHeaderSize = 8 + 8 + 8 + 4 + 4 + +type runtimePrebuiltFtabEntry struct { + entryOff uint32 + funcIndex uint32 +} + +var runtimeFuncPCFramesPrebuilt bool + +// Zero-copy view of the prebuilt table: lookups binary-search the on-disk +// ftab directly; nothing is materialized at adoption time. +var runtimePrebuiltBase uintptr +var runtimePrebuiltFtab []runtimePrebuiltFtabEntry +var runtimePrebuiltEntriesOnce uint32 + +// adoptPrebuiltFuncPCTable installs a zero-copy view over the prebuilt table +// if the entry section carries the magic header. Returns false to fall back +// to first-use construction. +func adoptPrebuiltFuncPCTable() bool { + if runtimeFuncInfoEntryStart == nil || runtimeFuncInfoEntryEnd == nil { + return false + } + start := uintptr(unsafe.Pointer(runtimeFuncInfoEntryStart)) + end := uintptr(unsafe.Pointer(runtimeFuncInfoEntryEnd)) + if end < start+runtimePrebuiltHeaderSize { + return false + } + if *(*uint64)(unsafe.Pointer(start)) != runtimePrebuiltMagic { + return false + } + base := uintptr(*(*uint64)(unsafe.Pointer(start + 16))) + count := *(*uint32)(unsafe.Pointer(start + 24)) + bucketCount := *(*uint32)(unsafe.Pointer(start + 28)) + need := uintptr(runtimePrebuiltHeaderSize) + uintptr(count)*8 + + uintptr(bucketCount)*unsafe.Sizeof(runtimePCFindBucket{}) + if count < 2 || end < start+need || uintptr(count) > runtimeFuncInfoCount*16+1 { + return false + } + runtimePrebuiltBase = base + runtimePrebuiltFtab = unsafe.Slice((*runtimePrebuiltFtabEntry)(unsafe.Pointer(start+runtimePrebuiltHeaderSize)), count) + runtimeFuncPCIndex = runtimePCFindIndex{ + base: base &^ (runtimePCFindBucketSize - 1), + buckets: unsafe.Slice((*runtimePCFindBucket)(unsafe.Pointer(start+runtimePrebuiltHeaderSize+uintptr(count)*8)), bucketCount), + } + runtimeFuncPCFramesPrebuilt = true + runtimeFuncPCFramesFromSites = true + runtimeFuncPCStubsFromSites = true + return true +} + +// prebuiltFrame returns the ftab row as a runtimeFuncPCFrame view. +func prebuiltFrame(i int) runtimeFuncPCFrame { + e := runtimePrebuiltFtab[i] + return runtimeFuncPCFrame{entry: runtimePrebuiltBase + uintptr(e.entryOff), funcIndex: e.funcIndex} +} + +// prebuiltFrameCount excludes the trailing sentinel. +func prebuiltFrameCount() int { + return len(runtimePrebuiltFtab) - 1 +} + +// materializePrebuiltEntries lazily builds the funcIndex -> entry map that +// only the pcline initializer consumes; FuncForPC lookups never pay for it. +// Two-phase (busy/done) so concurrent losers wait for the winner's store. +func materializePrebuiltEntries() { + for { + switch latomic.LoadUint32(&runtimePrebuiltEntriesOnce) { + case 2: + return + case 0: + if !latomic.CompareAndSwapUint32(&runtimePrebuiltEntriesOnce, 0, 1) { + continue + } + entries := make([]uintptr, runtimeFuncInfoCount+1) + for _, e := range runtimePrebuiltFtab[:prebuiltFrameCount()] { + if e.funcIndex == 0 || uintptr(e.funcIndex) > runtimeFuncInfoCount { + continue + } + pc := runtimePrebuiltBase + uintptr(e.entryOff) + if entries[e.funcIndex] == 0 || pc < entries[e.funcIndex] { + entries[e.funcIndex] = pc + } + } + runtimeFuncPCEntries = entries + latomic.StoreUint32(&runtimePrebuiltEntriesOnce, 2) + return + default: + c.Usleep(1) + } + } +} + +func initRuntimeFuncPCFramesOnce() { + if runtimeFuncInfoTable == nil || + runtimeFuncInfoCount == 0 || + runtimeFuncInfoStrings == nil || + runtimeFuncInfoStringOffsets == nil { + return + } + if runtimeFuncInfoCount > 1<<20 { + return + } + if adoptPrebuiltFuncPCTable() { + return + } + frames := make([]runtimeFuncPCFrame, 0, runtimeFuncInfoCount) + entries := make([]uintptr, runtimeFuncInfoCount+1) + frames, usedEntrySites := appendRuntimeFuncInfoEntryFrames(frames, entries) + symbolBuf := []byte(nil) + if !usedEntrySites { + symbolBuf = make([]byte, 0, maxFuncInfoSymbolLen()+len(runtimeClosureStubPrefix)+1) + for i := uintptr(0); i < runtimeFuncInfoCount; i++ { + fn := funcInfoAt(i) + pc := symbolPCFuncInfoName(symbolBuf, fn.symbolPkg, fn.symbolName) + if pc == 0 { + continue + } + index := uint32(i + 1) + frames = append(frames, runtimeFuncPCFrame{ + entry: pc, + funcIndex: index, + }) + if entries[index] == 0 || pc < entries[index] { + entries[index] = pc + } + } + } + frames, usedStubSites := appendRuntimeFuncInfoStubSiteFrames(frames) + // Closure stubs are an ABI adapter and may go away in a future closure + // lowering. Keep the fallback compatibility table light: it stores only + // target funcinfo record indexes. When the stub-site section is present it + // is authoritative (linkers do not expose local stubs through dlsym), and + // skipping the dlsym loop below matters: each dlsym is a dynamic-loader + // query, and one query per stub used to dominate first-use latency. + if !usedStubSites && runtimeFuncInfoStubIndexes != nil && runtimeFuncInfoStubCount != 0 && runtimeFuncInfoStubCount <= runtimeFuncInfoCount { + if symbolBuf == nil { + symbolBuf = make([]byte, 0, maxFuncInfoSymbolLen()+len(runtimeClosureStubPrefix)+1) + } + for i := uintptr(0); i < runtimeFuncInfoStubCount; i++ { + index := funcInfoStubIndexAt(i) + if index == 0 || uintptr(index) > runtimeFuncInfoCount { + continue + } + fn := funcInfoAt(uintptr(index) - 1) + pc := symbolPCPrefixedFuncInfoName(symbolBuf, runtimeClosureStubPrefix, fn.symbolPkg, fn.symbolName) + if pc == 0 { + continue + } + frames = append(frames, runtimeFuncPCFrame{ + entry: pc, + funcIndex: index, + }) + } + } + sortRuntimeFuncPCFrames(frames) + frames = uniqueRuntimeFuncPCFrames(frames) + runtimeFuncPCFrames = frames + runtimeFuncPCEntries = entries + runtimeFuncPCIndex = buildRuntimeFuncPCIndex(frames) + runtimeFuncPCFramesFromSites = usedEntrySites + runtimeFuncPCStubsFromSites = usedStubSites +} + +func appendRuntimeFuncInfoEntryFrames(frames []runtimeFuncPCFrame, entries []uintptr) ([]runtimeFuncPCFrame, bool) { + if runtimeFuncInfoEntryStart == nil || runtimeFuncInfoEntryEnd == nil { + return frames, false + } + start := uintptr(unsafe.Pointer(runtimeFuncInfoEntryStart)) + end := uintptr(unsafe.Pointer(runtimeFuncInfoEntryEnd)) + size := unsafe.Sizeof(*runtimeFuncInfoEntryStart) + if end <= start || size == 0 || (end-start)%size != 0 { + return frames, false + } + nsite := (end - start) / size + if nsite > runtimeFuncInfoCount*16 || nsite > 1<<20 { + return frames, false + } + used := false + for i := uintptr(0); i < nsite; i++ { + site := (*runtimeFuncInfoEntryRecord)(unsafe.Pointer(start + i*size)) + if site == nil || site.pc == 0 || site.symbolID == 0 { + continue + } + funcIndex := funcInfoIndexForSymbolID(site.symbolID) + if funcIndex == 0 || uintptr(funcIndex) > runtimeFuncInfoCount { + continue + } + frames = append(frames, runtimeFuncPCFrame{ + entry: site.pc, + funcIndex: funcIndex, + }) + if entries[funcIndex] == 0 || site.pc < entries[funcIndex] { + entries[funcIndex] = site.pc + } + used = true + } + return frames, used +} + +func appendRuntimeFuncInfoStubSiteFrames(frames []runtimeFuncPCFrame) ([]runtimeFuncPCFrame, bool) { + if runtimeFuncInfoStubSiteStart == nil || runtimeFuncInfoStubSiteEnd == nil { + return frames, false + } + start := uintptr(unsafe.Pointer(runtimeFuncInfoStubSiteStart)) + end := uintptr(unsafe.Pointer(runtimeFuncInfoStubSiteEnd)) + size := unsafe.Sizeof(*runtimeFuncInfoStubSiteStart) + if end <= start || size == 0 || (end-start)%size != 0 { + return frames, false + } + nsite := (end - start) / size + if nsite > runtimeFuncInfoCount*16 || nsite > 1<<20 { + return frames, false + } + used := false + for i := uintptr(0); i < nsite; i++ { + site := (*runtimeFuncInfoStubSiteRecord)(unsafe.Pointer(start + i*size)) + if site == nil || site.pc == 0 || site.symbolID == 0 { + continue + } + funcIndex := funcInfoIndexForSymbolID(site.symbolID) + if funcIndex == 0 || uintptr(funcIndex) > runtimeFuncInfoCount { + continue + } + frames = append(frames, runtimeFuncPCFrame{ + entry: site.pc, + funcIndex: funcIndex, + }) + used = true + } + return frames, used +} + +func funcInfoIndexForSymbolID(symbolID uint64) uint32 { + if symbolID == 0 || runtimeFuncInfoSymbolIndex == nil || runtimeFuncInfoSymbolIndexCount == 0 { + return 0 + } + if runtimeFuncInfoSymbolIndexCount > runtimeFuncInfoCount || runtimeFuncInfoSymbolIndexCount > 1<<20 { + return 0 + } + lo, hi := uintptr(0), runtimeFuncInfoSymbolIndexCount + size := unsafe.Sizeof(*runtimeFuncInfoSymbolIndex) + for lo < hi { + mid := (lo + hi) >> 1 + rec := (*runtimeFuncInfoSymbolIndexRecord)(unsafe.Add(unsafe.Pointer(runtimeFuncInfoSymbolIndex), mid*size)) + if rec.symbolID >= symbolID { + hi = mid + } else { + lo = mid + 1 + } + } + if lo >= runtimeFuncInfoSymbolIndexCount { + return 0 + } + rec := (*runtimeFuncInfoSymbolIndexRecord)(unsafe.Add(unsafe.Pointer(runtimeFuncInfoSymbolIndex), lo*size)) + if rec.symbolID != symbolID || rec.funcIndex == 0 || uintptr(rec.funcIndex) > runtimeFuncInfoCount { + return 0 + } + return rec.funcIndex +} + +func sortRuntimeFuncPCFrames(frames []runtimeFuncPCFrame) { + if len(frames) < 2 { + return + } + quickSortRuntimeFuncPCFrames(frames, 0, len(frames)-1) +} + +func quickSortRuntimeFuncPCFrames(frames []runtimeFuncPCFrame, lo, hi int) { + for hi-lo > 16 { + mid := int(uint(lo+hi) >> 1) + if frames[mid].entry < frames[lo].entry { + frames[mid], frames[lo] = frames[lo], frames[mid] + } + if frames[hi].entry < frames[mid].entry { + frames[hi], frames[mid] = frames[mid], frames[hi] + } + if frames[mid].entry < frames[lo].entry { + frames[mid], frames[lo] = frames[lo], frames[mid] + } + pivot := frames[mid].entry + i, j := lo, hi + for { + for frames[i].entry < pivot { + i++ + } + for frames[j].entry > pivot { + j-- + } + if i >= j { + break + } + frames[i], frames[j] = frames[j], frames[i] + i++ + j-- + } + if j-lo < hi-i { + quickSortRuntimeFuncPCFrames(frames, lo, j) + lo = i + } else { + quickSortRuntimeFuncPCFrames(frames, i, hi) + hi = j + } + } + for i := lo + 1; i <= hi; i++ { + x := frames[i] + j := i - 1 + for j >= lo && frames[j].entry > x.entry { + frames[j+1] = frames[j] + j-- + } + frames[j+1] = x + } +} + +func uniqueRuntimeFuncPCFrames(frames []runtimeFuncPCFrame) []runtimeFuncPCFrame { + if len(frames) < 2 { + return frames + } + out := frames[:1] + for i := 1; i < len(frames); i++ { + if frames[i].entry == out[len(out)-1].entry { + out[len(out)-1] = frames[i] + continue + } + out = append(out, frames[i]) + } + return out +} + +// buildRuntimeFuncPCIndex is the runtime counterpart of Go's linker-built +// findfunctab. The table shape and lookup behavior are Go-style; the build time +// differs because LLGo's final function PCs are discovered from associated +// sections after link/load instead of being sorted directly by cmd/link. +func buildRuntimeFuncPCIndex(frames []runtimeFuncPCFrame) runtimePCFindIndex { + if len(frames) == 0 { + return runtimePCFindIndex{} + } + if uintptr(len(frames)) > ^uintptr(0)>>1 { + return runtimePCFindIndex{} + } + base := frames[0].entry &^ (runtimePCFindBucketSize - 1) + last := frames[len(frames)-1].entry + if last < base { + return runtimePCFindIndex{} + } + nbuckets := (last-base)/runtimePCFindBucketSize + 1 + if nbuckets > 1<<20 && nbuckets > uintptr(len(frames))*64 { + return runtimePCFindIndex{} + } + buckets := make([]runtimePCFindBucket, nbuckets) + subSize := runtimePCFindBucketSize / runtimePCFindSubbucket + for b := range buckets { + bucketStart := base + uintptr(b)*runtimePCFindBucketSize + baseIdx := runtimeFuncPCFrameIndexBinary(frames, bucketStart) + if baseIdx < 0 { + baseIdx = 0 + } + if baseIdx > len(frames)-1 { + baseIdx = len(frames) - 1 + } + buckets[b].idx = uint32(baseIdx) + for s := 0; s < runtimePCFindSubbucket; s++ { + pc := bucketStart + uintptr(s)*subSize + subIdx := runtimeFuncPCFrameIndexBinary(frames, pc) + if subIdx < 0 { + subIdx = 0 + } + if subIdx > len(frames)-1 { + subIdx = len(frames) - 1 + } + // delta counts deduplicated PCs inside one bucket, so it is + // bounded by the bucket size and always fits in uint16. + delta := subIdx - baseIdx + if delta < 0 || delta > 0xffff { + return runtimePCFindIndex{} + } + buckets[b].subbuckets[s] = uint16(delta) + } + } + return runtimePCFindIndex{base: base, buckets: buckets} +} + +func runtimePCFindRange(index runtimePCFindIndex, n int, pc uintptr) (int, int, bool) { + if n == 0 || len(index.buckets) == 0 || pc < index.base { + return 0, 0, false + } + off := pc - index.base + bucket := off / runtimePCFindBucketSize + if bucket >= uintptr(len(index.buckets)) { + return 0, 0, false + } + subSize := runtimePCFindBucketSize / runtimePCFindSubbucket + sub := (off % runtimePCFindBucketSize) / subSize + b := index.buckets[bucket] + lo := int(b.idx) + int(b.subbuckets[sub]) + hi := n + if sub+1 < runtimePCFindSubbucket { + hi = int(b.idx) + int(b.subbuckets[sub+1]) + } else if bucket+1 < uintptr(len(index.buckets)) { + hi = int(index.buckets[bucket+1].idx) + } + if lo > 0 { + lo-- + } + if hi < lo { + hi = lo + } + hi += 2 + if hi > n { + hi = n + } + if lo > n { + lo = n + } + return lo, hi, true +} + +// runtimeFuncPCFrameIndex mirrors runtime.findfunc: use the compact bucket +// table to jump near the containing function, then scan the sorted frame table +// inside that narrow range. +func runtimeFuncPCFrameIndex(pc uintptr) int { + if runtimeFuncPCFramesPrebuilt { + return prebuiltFrameIndex(pc) + } + frames := runtimeFuncPCFrames + if len(frames) == 0 { + return -1 + } + if lo, hi, ok := runtimePCFindRange(runtimeFuncPCIndex, len(frames), pc); ok { + for lo < hi { + mid := int(uint(lo+hi) >> 1) + if frames[mid].entry > pc { + hi = mid + } else { + lo = mid + 1 + } + } + idx := lo - 1 + if idx < 0 || frames[idx].entry > pc { + return -1 + } + return idx + } + return runtimeFuncPCFrameIndexBinary(frames, pc) +} + +func runtimeFuncPCFrameIndexBinary(frames []runtimeFuncPCFrame, pc uintptr) int { + lo, hi := 0, len(frames) + for lo < hi { + mid := int(uint(lo+hi) >> 1) + if frames[mid].entry > pc { + hi = mid + } else { + lo = mid + 1 + } + } + idx := lo - 1 + if idx < 0 { + return -1 + } + return idx +} + +// prebuiltFrameIndex is runtimeFuncPCFrameIndex over the zero-copy ftab: +// bucket narrowing via the shared find index, then binary search on +// entryOff. Returns the index of the last entry with PC <= pc, or -1. +func prebuiltFrameIndex(pc uintptr) int { + n := prebuiltFrameCount() + if n <= 0 || pc < runtimePrebuiltBase { + return -1 + } + off := uint32(pc - runtimePrebuiltBase) + lo, hi := 0, n + if l, h, ok := runtimePCFindRange(runtimeFuncPCIndex, n, pc); ok { + lo, hi = l, h + } + for lo < hi { + mid := int(uint(lo+hi) >> 1) + if runtimePrebuiltFtab[mid].entryOff > off { + hi = mid + } else { + lo = mid + 1 + } + } + idx := lo - 1 + if idx < 0 || runtimePrebuiltFtab[idx].entryOff > off { + return -1 + } + return idx +} + +func funcEntryForIndex(index uint32) uintptr { + if index == 0 { + return 0 + } + initRuntimeFuncPCFrames() + if runtimeFuncPCFramesPrebuilt { + materializePrebuiltEntries() + } + if uintptr(index) >= uintptr(len(runtimeFuncPCEntries)) { + return 0 + } + return runtimeFuncPCEntries[index] +} + +// coldFuncInfoEntryLookup resolves an exact function-entry PC by scanning the +// raw entry-site and stub-site sections, without building the sorted frame +// table and without any dynamic-loader query. Function values can point at +// either a real function entry or its closure stub, so both sections are +// scanned. The scan is linear, so it is capped: for larger binaries the +// dladdr cold path is cheaper than streaming the whole section. +const coldFuncInfoEntryScanLimit = 4096 + +// coldFuncInfoScanRange scans one {pc, symbolID} record section for the +// anchor nearest at-or-after pc within the warm path's entry slack (anchors +// are emitted from LLVM IR and land after the backend prologue). It returns +// the matched funcinfo index and delta, or (0, maxDelta) on miss. +func coldFuncInfoScanRange(start, end, size, pc uintptr, bestDelta uintptr) (uint32, uintptr) { + if start == 0 || end <= start || size == 0 || (end-start)%size != 0 { + return 0, bestDelta + } + nsite := (end - start) / size + if nsite > coldFuncInfoEntryScanLimit || nsite > runtimeFuncInfoCount*16 { + return 0, bestDelta + } + bestIndex := uint32(0) + for i := uintptr(0); i < nsite; i++ { + site := (*runtimeFuncInfoEntryRecord)(unsafe.Pointer(start + i*size)) + if site.symbolID == 0 || site.pc < pc { + continue + } + delta := site.pc - pc + if delta >= bestDelta { + continue + } + funcIndex := funcInfoIndexForSymbolID(site.symbolID) + if funcIndex == 0 || uintptr(funcIndex) > runtimeFuncInfoCount { + continue + } + bestDelta = delta + bestIndex = funcIndex + if delta == 0 { + break + } + } + return bestIndex, bestDelta +} + +// coldFuncPCLookupBudget grants a small number of table-free cold lookups per +// process; past that, building the sorted table amortizes better than more +// linear scans or dladdr calls. +var coldFuncPCLookupCount uint32 + +func coldFuncPCLookupBudget() bool { + if prebuiltFuncPCTablePresent() { + // The prebuilt table makes first-use initialization cheap; skip the + // scan/dladdr path entirely and let the caller fall through to it. + return false + } + return latomic.AddUint32(&coldFuncPCLookupCount, 1) <= 8 +} + +// prebuiltFuncPCTablePresent reports whether the entry section carries the +// link-phase prebuilt table, in which case the cold scan must not interpret +// its bytes as site records — and does not need to: adopting the prebuilt +// table is itself cheap. +func prebuiltFuncPCTablePresent() bool { + if runtimeFuncInfoEntryStart == nil || runtimeFuncInfoEntryEnd == nil { + return false + } + start := uintptr(unsafe.Pointer(runtimeFuncInfoEntryStart)) + end := uintptr(unsafe.Pointer(runtimeFuncInfoEntryEnd)) + return end >= start+8 && *(*uint64)(unsafe.Pointer(start)) == runtimePrebuiltMagic +} + +// runtimeFuncInfoWarmSink keeps the warm-up loads observable. +var runtimeFuncInfoWarmSink byte + +// init pre-warms the prebuilt function table, mirroring Go: Go's pclntab +// pages are touched by the runtime itself (traceback, GC) long before user +// code queries it, so its "first" FuncForPC never pays page-in. Touching the +// pages the lookup path reads — adopted blob, funcinfo records, string +// offsets, strings — moves first-touch page-in (plus, on darwin, +// code-signature validation) from the first user lookup to process startup +// (tens of µs once, on binaries that carry funcinfo tables). Without a +// prebuilt table everything stays lazy: first-use construction allocates, +// which has no place in init, and programs that never introspect pay +// nothing. +func init() { + if !prebuiltFuncPCTablePresent() { + return + } + initRuntimeFuncPCFrames() // zero-copy adoption, sub-µs + touch := func(base unsafe.Pointer, n uintptr) { + if base == nil || n == 0 { + return + } + const pageStep = 4096 + sink := runtimeFuncInfoWarmSink + p := uintptr(base) + for off := uintptr(0); off < n; off += pageStep { + sink += *(*byte)(unsafe.Pointer(p + off)) + } + sink += *(*byte)(unsafe.Pointer(p + n - 1)) + runtimeFuncInfoWarmSink = sink + } + start := uintptr(unsafe.Pointer(runtimeFuncInfoEntryStart)) + count := *(*uint32)(unsafe.Pointer(start + 24)) + bucketCount := *(*uint32)(unsafe.Pointer(start + 28)) + need := uintptr(runtimePrebuiltHeaderSize) + uintptr(count)*8 + + uintptr(bucketCount)*unsafe.Sizeof(runtimePCFindBucket{}) + touch(unsafe.Pointer(runtimeFuncInfoEntryStart), need) + touch(unsafe.Pointer(runtimeFuncInfoTable), + runtimeFuncInfoCount*unsafe.Sizeof(runtimeFuncInfoRecord{})) + touch(unsafe.Pointer(runtimeFuncInfoStringOffsets), + runtimeFuncInfoStringCount*unsafe.Sizeof(uint32(0))) + if runtimeFuncInfoStrings != nil && runtimeFuncInfoStringCount > 0 { + last := uintptr(*(*uint32)(unsafe.Add(unsafe.Pointer(runtimeFuncInfoStringOffsets), + (runtimeFuncInfoStringCount-1)*unsafe.Sizeof(uint32(0))))) + lastStr := funcInfoCString(uint16(runtimeFuncInfoStringCount - 1)) + touch(unsafe.Pointer(runtimeFuncInfoStrings), last+uintptr(cStringLen(lastStr))+1) + } + // One synthetic lookup warms the code paths themselves (allocator size + // classes, lookup caches), not just the data pages. + if prebuiltFrameCount() > 0 { + frame := prebuiltFrame(0) + if sym, ok := pcSymbolForFuncInfoIndex(frame.entry, frame.entry, frame.funcIndex); ok { + runtimeFuncInfoWarmSink += byte(len(sym.function)) + } + } + // Write-warm the FuncForPC cache: its first stores otherwise take + // zero-fill write faults, one per page, on the first few lookups. + for i := 0; i < funcForPCCacheSets; i += 4096 / int(unsafe.Sizeof(funcForPCCache[0])) { + funcForPCCache[i][0].pc = 0 + } + funcForPCCache[funcForPCCacheSets-1][0].pc = 0 +} + +func coldFuncInfoEntryLookup(pc uintptr) (pcSymbol, bool) { + if pc == 0 || prebuiltFuncPCTablePresent() { + return pcSymbol{}, false + } + bestDelta := uintptr(runtimeFuncPCEntrySlack) + 1 + bestIndex := uint32(0) + if runtimeFuncInfoEntryStart != nil && runtimeFuncInfoEntryEnd != nil { + bestIndex, bestDelta = coldFuncInfoScanRange( + uintptr(unsafe.Pointer(runtimeFuncInfoEntryStart)), + uintptr(unsafe.Pointer(runtimeFuncInfoEntryEnd)), + unsafe.Sizeof(*runtimeFuncInfoEntryStart), pc, bestDelta) + } + if bestDelta != 0 && runtimeFuncInfoStubSiteStart != nil && runtimeFuncInfoStubSiteEnd != nil { + if idx, _ := coldFuncInfoScanRange( + uintptr(unsafe.Pointer(runtimeFuncInfoStubSiteStart)), + uintptr(unsafe.Pointer(runtimeFuncInfoStubSiteEnd)), + unsafe.Sizeof(*runtimeFuncInfoStubSiteStart), pc, bestDelta); idx != 0 { + bestIndex = idx + } + } + if bestIndex == 0 { + return pcSymbol{}, false + } + return pcSymbolForFuncInfoIndex(pc, pc, bestIndex) +} + +func funcPCFrameForPC(pc uintptr) (pcSymbol, bool) { + if pc == 0 { + return pcSymbol{}, false + } + initRuntimeFuncPCFrames() + idx := runtimeFuncPCFrameIndex(pc) + if idx < 0 { + return pcSymbol{}, false + } + var frame runtimeFuncPCFrame + if runtimeFuncPCFramesPrebuilt { + frame = prebuiltFrame(idx) + } else { + frame = runtimeFuncPCFrames[idx] + } + return pcSymbolForFuncInfoIndex(pc, frame.entry, frame.funcIndex) +} + +func funcPCFrameForEntryPC(pc uintptr) (pcSymbol, bool) { + if pc == 0 { + return pcSymbol{}, false + } + initRuntimeFuncPCFrames() + if runtimeFuncPCFramesPrebuilt { + // Prebuilt entries are true symbol starts (normalized against the + // symbol table by the link-phase tool), so no slack is needed. + idx := prebuiltFrameIndex(pc) + if idx < 0 { + return pcSymbol{}, false + } + frame := prebuiltFrame(idx) + if frame.entry != pc { + return pcSymbol{}, false + } + return pcSymbolForFuncInfoIndex(pc, pc, frame.funcIndex) + } + frames := runtimeFuncPCFrames + if len(frames) == 0 { + return pcSymbol{}, false + } + lo, hi := 0, len(frames) + for lo < hi { + mid := int(uint(lo+hi) >> 1) + if frames[mid].entry >= pc { + hi = mid + } else { + lo = mid + 1 + } + } + if lo >= len(frames) { + return pcSymbol{}, false + } + frame := frames[lo] + if frame.entry != pc && frame.entry-pc > runtimeFuncPCEntrySlack { + return pcSymbol{}, false + } + return pcSymbolForFuncInfoIndex(pc, pc, frame.funcIndex) +} + +func pcSymbolForFuncInfoIndex(pc, entry uintptr, funcIndex uint32) (pcSymbol, bool) { + if funcIndex == 0 || uintptr(funcIndex) > runtimeFuncInfoCount { + return pcSymbol{}, false + } + fn := funcInfoAt(uintptr(funcIndex) - 1) + line := int(fn.line) + return pcSymbol{ + pc: pc, + entry: entry, + function: funcInfoFunctionName(fn), + file: funcInfoFileName(fn), + line: line, + startLine: line, + ok: true, + }, true +} + +func initRuntimePCLineFrames() { + if latomic.LoadUint32(&runtimePCLineInitState) == runtimeFuncInfoInitDone { + return + } + initRuntimePCLineFramesSlow() +} + +func initRuntimePCLineFramesSlow() { + for { + state := latomic.LoadUint32(&runtimePCLineInitState) + switch state { + case runtimeFuncInfoInitDone: + return + case runtimeFuncInfoInitUninit: + if latomic.CompareAndSwapUint32(&runtimePCLineInitState, runtimeFuncInfoInitUninit, runtimeFuncInfoInitBusy) { + initRuntimePCLineFramesOnce() + latomic.StoreUint32(&runtimePCLineInitState, runtimeFuncInfoInitDone) + reportRuntimePCLineDebug() + return + } + } + c.Usleep(1) + } +} + +func initRuntimePCLineFramesOnce() { + if runtimePCLineTable == nil || + runtimePCLineCount == 0 || + runtimePCSiteStart == nil || + runtimePCSiteEnd == nil || + runtimeFuncInfoTable == nil || + runtimeFuncInfoCount == 0 || + runtimeFuncInfoStrings == nil || + runtimeFuncInfoStringOffsets == nil { + return + } + if runtimePCLineCount > 1<<20 || runtimePCLineCount > runtimeFuncInfoCount*1024 { + return + } + start := uintptr(unsafe.Pointer(runtimePCSiteStart)) + end := uintptr(unsafe.Pointer(runtimePCSiteEnd)) + size := unsafe.Sizeof(*runtimePCSiteStart) + if end <= start || size == 0 || (end-start)%size != 0 { + return + } + nsite := (end - start) / size + if nsite > runtimePCLineCount*1024 || nsite > 1<<22 { + return + } + frames := make([]runtimePCLineFrame, 0, nsite) + symbolBuf := make([]byte, 0, maxFuncInfoSymbolLen()+1) + // Sites vastly outnumber distinct functions and files, so materialize the + // per-function strings and entry PCs once and the packed file strings once + // per file ID. Building them per site used to dominate first-use latency. + type pcLineFuncInfo struct { + entry uintptr + function string + file string + line int + resolved bool + } + funcCache := make([]pcLineFuncInfo, runtimeFuncInfoCount+1) + fileCache := make(map[uint32]string) + for i := uintptr(0); i < nsite; i++ { + site := (*runtimePCSiteRecord)(unsafe.Pointer(start + i*size)) + if site == nil || site.id == 0 || site.pc == 0 { + continue + } + rec := pcLineInfoForID(site.id) + if rec == nil || rec.funcIndex == 0 || uintptr(rec.funcIndex) > runtimeFuncInfoCount { + continue + } + pc := site.pc + fn := funcInfoAt(uintptr(rec.funcIndex) - 1) + fc := &funcCache[rec.funcIndex] + if !fc.resolved { + fc.entry = funcEntryForIndex(rec.funcIndex) + if fc.entry == 0 { + fc.entry = symbolPCFuncInfoName(symbolBuf, fn.symbolPkg, fn.symbolName) + } + fc.function = publicFunctionName(funcInfoJoinName(fn.namePkg, fn.nameName)) + if fc.function == "" { + fc.function = publicFunctionName(funcInfoJoinName(fn.symbolPkg, fn.symbolName)) + } + fc.file = funcInfoJoinFile(fn.fileRoot, fn.fileName) + fc.line = int(fn.line) + fc.resolved = true + } + entry := fc.entry + if entry == 0 { + sym := addrInfoSymbol(pc) + entry = sym.entry + } + file := "" + if rec.file != 0 { + var ok bool + if file, ok = fileCache[rec.file]; !ok { + file = funcInfoPackedFile(rec.file) + fileCache[rec.file] = file + } + } + if file == "" { + file = fc.file + } + line := int(rec.line) + if line == 0 { + line = fc.line + } + frames = append(frames, runtimePCLineFrame{ + pc: pc, + entry: entry, + function: fc.function, + file: file, + line: line, + startLine: fc.line, + }) + } + sortRuntimePCLineFrames(frames) + frames = uniqueRuntimePCLineFrames(frames) + runtimePCLineFrames = frames + runtimePCLineIndex = buildRuntimePCLineIndex(frames) +} + +func pcLineInfoForID(id uint64) *runtimePCLineRecord { + lo, hi := uintptr(0), runtimePCLineCount + for lo < hi { + mid := (lo + hi) >> 1 + rec := pcLineAt(mid) + if rec.id >= id { + hi = mid + } else { + lo = mid + 1 + } + } + if lo >= runtimePCLineCount { + return nil + } + rec := pcLineAt(lo) + if rec.id != id { + return nil + } + return rec +} + +func symbolPC(symbol string) uintptr { + if symbol == "" { + return 0 + } + buf := make([]byte, len(symbol)+1) + copy(buf, symbol) + return uintptr(clitedebug.Symbol((*c.Char)(unsafe.Pointer(&buf[0])))) +} + +func sortRuntimePCLineFrames(frames []runtimePCLineFrame) { + if len(frames) < 2 { + return + } + quickSortRuntimePCLineFrames(frames, 0, len(frames)-1) +} + +func quickSortRuntimePCLineFrames(frames []runtimePCLineFrame, lo, hi int) { + for hi-lo > 16 { + mid := int(uint(lo+hi) >> 1) + if frames[mid].pc < frames[lo].pc { + frames[mid], frames[lo] = frames[lo], frames[mid] + } + if frames[hi].pc < frames[mid].pc { + frames[hi], frames[mid] = frames[mid], frames[hi] + } + if frames[mid].pc < frames[lo].pc { + frames[mid], frames[lo] = frames[lo], frames[mid] + } + pivot := frames[mid].pc + i, j := lo, hi + for { + for frames[i].pc < pivot { + i++ + } + for frames[j].pc > pivot { + j-- + } + if i >= j { + break + } + frames[i], frames[j] = frames[j], frames[i] + i++ + j-- + } + if j-lo < hi-i { + quickSortRuntimePCLineFrames(frames, lo, j) + lo = i + } else { + quickSortRuntimePCLineFrames(frames, i, hi) + hi = j + } + } + for i := lo + 1; i <= hi; i++ { + x := frames[i] + j := i - 1 + for j >= lo && frames[j].pc > x.pc { + frames[j+1] = frames[j] + j-- + } + frames[j+1] = x + } +} + +func uniqueRuntimePCLineFrames(frames []runtimePCLineFrame) []runtimePCLineFrame { + if len(frames) < 2 { + return frames + } + out := frames[:1] + for i := 1; i < len(frames); i++ { + if frames[i].pc == out[len(out)-1].pc { + out[len(out)-1] = frames[i] + continue + } + out = append(out, frames[i]) + } + return out +} + +// buildRuntimePCLineIndex reuses the same Go-style bucket geometry for +// statement PC-line sites. Go stores dense per-function pcdata; LLGo keeps +// statement sites as a separate sorted table for now, but the hot PC lookup +// follows the same bucket/subbucket narrowing. +func buildRuntimePCLineIndex(frames []runtimePCLineFrame) runtimePCFindIndex { + if len(frames) == 0 { + return runtimePCFindIndex{} + } + base := frames[0].pc &^ (runtimePCFindBucketSize - 1) + last := frames[len(frames)-1].pc + if last < base { + return runtimePCFindIndex{} + } + nbuckets := (last-base)/runtimePCFindBucketSize + 1 + if nbuckets > 1<<20 && nbuckets > uintptr(len(frames))*64 { + return runtimePCFindIndex{} + } + buckets := make([]runtimePCFindBucket, nbuckets) + subSize := runtimePCFindBucketSize / runtimePCFindSubbucket + for b := range buckets { + bucketStart := base + uintptr(b)*runtimePCFindBucketSize + baseIdx := runtimePCLineFrameIndexBinary(frames, bucketStart, false) + if baseIdx < 0 { + baseIdx = 0 + } + if baseIdx > len(frames)-1 { + baseIdx = len(frames) - 1 + } + buckets[b].idx = uint32(baseIdx) + for s := 0; s < runtimePCFindSubbucket; s++ { + pc := bucketStart + uintptr(s)*subSize + subIdx := runtimePCLineFrameIndexBinary(frames, pc, false) + if subIdx < 0 { + subIdx = 0 + } + if subIdx > len(frames)-1 { + subIdx = len(frames) - 1 + } + // delta counts deduplicated PCs inside one bucket, so it is + // bounded by the bucket size and always fits in uint16. + delta := subIdx - baseIdx + if delta < 0 || delta > 0xffff { + return runtimePCFindIndex{} + } + buckets[b].subbuckets[s] = uint16(delta) + } + } + return runtimePCFindIndex{base: base, buckets: buckets} +} + +func runtimePCLineFrameRange(pc uintptr) (int, int) { + frames := runtimePCLineFrames + if lo, hi, ok := runtimePCFindRange(runtimePCLineIndex, len(frames), pc); ok { + return lo, hi + } + return 0, len(frames) +} + +func runtimePCLineFrameIndex(pc uintptr, exact bool) int { + frames := runtimePCLineFrames + if len(frames) == 0 { + return -1 + } + lo, hi := runtimePCLineFrameRange(pc) + return runtimePCLineFrameIndexInRange(frames, pc, exact, lo, hi) +} + +func runtimePCLineFrameIndexBinary(frames []runtimePCLineFrame, pc uintptr, exact bool) int { + return runtimePCLineFrameIndexInRange(frames, pc, exact, 0, len(frames)) +} + +func runtimePCLineFrameIndexInRange(frames []runtimePCLineFrame, pc uintptr, exact bool, lo, hi int) int { + for lo < hi { + mid := int(uint(lo+hi) >> 1) + if frames[mid].pc > pc || (exact && frames[mid].pc == pc) { + hi = mid + } else { + lo = mid + 1 + } + } + if exact { + if lo >= len(frames) || frames[lo].pc != pc { + return -1 + } + return lo + } + idx := lo - 1 + if idx < 0 { + return -1 + } + return idx +} + +func pcLineFrameForPC(pc, entry uintptr) (pcSymbol, bool) { + if pc == 0 { + return pcSymbol{}, false + } + initRuntimePCLineFrames() + frames := runtimePCLineFrames + idx := runtimePCLineFrameIndex(pc, false) + if idx < 0 { + return pcSymbol{}, false + } + frame := frames[idx] + // When the caller knows the function entry, only accept a site from the + // same function. A site with an unresolved entry cannot prove it belongs + // to the queried function, so it must be rejected too — otherwise a + // nearest-below hit from a neighboring function leaks its file/line. + if entry != 0 && frame.entry != entry { + return pcSymbol{}, false + } + return pcSymbol{ + pc: pc, + entry: frame.entry, + function: frame.function, + file: frame.file, + line: frame.line, + startLine: frame.startLine, + ok: true, + }, true +} + +func pcLineFrameForExactPC(pc uintptr) (pcSymbol, bool) { + if pc == 0 { + return pcSymbol{}, false + } + initRuntimePCLineFrames() + frames := runtimePCLineFrames + idx := runtimePCLineFrameIndex(pc, true) + if idx < 0 { + return pcSymbol{}, false + } + frame := frames[idx] + return pcSymbol{ + pc: pc, + entry: frame.entry, + function: frame.function, + file: frame.file, + line: frame.line, + startLine: frame.startLine, + ok: true, + }, true +} + +func mergePCLineSymbol(base, line pcSymbol) pcSymbol { + if line.entry == 0 { + line.entry = base.entry + } + if line.function == "" { + line.function = base.function + } + if line.file == "" { + line.file = base.file + } + if line.line == 0 { + line.line = base.line + } + if line.startLine == 0 { + line.startLine = base.startLine + } + line.ok = true + return line +} + +func frameSymbol(pc uintptr) pcSymbol { + if pc&3 != 0 { + if frame, ok := rtdebug.FrameForPC(pc); ok { + return pcSymbol{ + pc: pc, + entry: frame.Entry, + function: frame.Function, + file: frame.File, + line: frame.Line, + startLine: frame.StartLine, + ok: true, + } + } + } + if pc == 0 { + sym := addrInfoSymbol(pc) + if frame, ok := rtdebug.FrameForPC(pc); ok { + return pcSymbol{ + pc: pc, + entry: frame.Entry, + function: frame.Function, + file: frame.File, + line: frame.Line, + startLine: frame.StartLine, + ok: true, + } + } + return sym + } + if lineSym, ok := pcLineFrameForExactPC(pc); ok { + return lineSym + } + if lineSym, ok := pcLineFrameForExactPC(pc - 1); ok { + lineSym.pc = pc + return lineSym + } + sym := addrInfoSymbol(pc) + if lineSym, ok := pcLineFrameForPC(pc, sym.entry); ok { + return mergePCLineSymbol(sym, lineSym) + } + if sym.entry == 0 || pc > sym.entry { + if callSym := addrInfoSymbol(pc - 1); callSym.ok { + if lineSym, ok := pcLineFrameForPC(pc-1, callSym.entry); ok { + lineSym.pc = pc + return mergePCLineSymbol(callSym, lineSym) + } + callSym.pc = pc + return callSym + } + } + if !sym.ok { + if funcSym, ok := funcPCFrameForPC(pc); ok { + return funcSym + } + } + if frame, ok := rtdebug.FrameForPC(pc); ok { + return pcSymbol{ + pc: pc, + entry: frame.Entry, + function: frame.Function, + file: frame.File, + line: frame.Line, + startLine: frame.StartLine, + ok: true, + } + } + return sym +} + func (ci *Frames) Next() (frame Frame, more bool) { for len(ci.frames) < 2 { // Find the next frame. @@ -119,8 +1990,8 @@ func (ci *Frames) Next() (frame Frame, more bool) { } else { pc, ci.callers = ci.callers[0], ci.callers[1:] } - info := &clitedebug.Info{} - if clitedebug.Addrinfo(unsafe.Pointer(pc), info) == 0 { + sym := frameSymbol(pc) + if !sym.ok { ci.frames = append(ci.frames, Frame{ PC: pc, Function: unknownFunctionName(pc), @@ -131,17 +2002,22 @@ func (ci *Frames) Next() (frame Frame, more bool) { }) continue } - fn := safeGoString(info.Sname, "") + fn := sym.function if fn == "" { fn = unknownFunctionName(pc) } + var f *Func + if sym.entry != 0 || fn != "" { + f = frameFuncForPC(pc, sym, fn) + } ci.frames = append(ci.frames, Frame{ PC: pc, + Func: f, Function: fn, - File: "", - Line: 0, - startLine: 0, - Entry: uintptr(info.Saddr), + File: sym.file, + Line: sym.line, + startLine: sym.startLine, + Entry: sym.entry, }) } @@ -176,19 +2052,33 @@ 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 + pc uintptr + file string + line int } 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) { - var info clitedebug.Info - if pc == 0 || clitedebug.Addrinfo(unsafe.Pointer(pc), &info) == 0 { - return "", 0 + if f != nil && f.pc == pc && (f.file != "" || f.line != 0) { + return f.file, f.line } - return safeGoString(info.Fname, ""), 0 + sym := frameSymbol(pc) + return sym.file, sym.line } // moduledata records information about the layout of the executable diff --git a/runtime/internal/lib/runtime/time_llgo.go b/runtime/internal/lib/runtime/time_llgo.go index 0a09c98e07..3f60f7d348 100644 --- a/runtime/internal/lib/runtime/time_llgo.go +++ b/runtime/internal/lib/runtime/time_llgo.go @@ -547,7 +547,5 @@ func timeSleepWake(arg any, _ uintptr) { } func runtimeNano() int64 { - tv := (*ct.Timespec)(c.Alloca(unsafe.Sizeof(ct.Timespec{}))) - ct.ClockGettime(ct.CLOCK_MONOTONIC, tv) - return int64(tv.Sec)*1e9 + int64(tv.Nsec) + return nanotime1() } diff --git a/runtime/internal/lib/runtime/time_llgo_go123.go b/runtime/internal/lib/runtime/time_llgo_go123.go index ceb2d73ed4..65bbff2f4d 100644 --- a/runtime/internal/lib/runtime/time_llgo_go123.go +++ b/runtime/internal/lib/runtime/time_llgo_go123.go @@ -581,7 +581,5 @@ func resetTimer(t *timeTimer, when, period int64) bool { } func runtimeNano() int64 { - tv := (*ct.Timespec)(c.Alloca(unsafe.Sizeof(ct.Timespec{}))) - ct.ClockGettime(ct.CLOCK_MONOTONIC, tv) - return int64(tv.Sec)*1e9 + int64(tv.Nsec) + return nanotime1() } diff --git a/runtime/internal/runtime/caller.go b/runtime/internal/runtime/caller.go new file mode 100644 index 0000000000..341158e825 --- /dev/null +++ b/runtime/internal/runtime/caller.go @@ -0,0 +1,493 @@ +/* + * 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 runtime + +import ( + "unsafe" + + clitedebug "github.com/goplus/llgo/runtime/internal/clite/debug" + "github.com/goplus/llgo/runtime/internal/clite/tls" +) + +type CallerFrame struct { + PC uintptr + Entry uintptr + Function string + File string + Line int + StartLine int + // captured memoizes the interned synthetic PC base (seq << 2) for this + // exact frame content. It is cleared whenever the frame's line info + // changes, so repeated Caller/Callers walks over an unchanged stack skip + // the intern hash probe entirely. Only meaningful inside shadow-stack + // slots; ignored by frame comparison and hashing. + captured uintptr +} + +const callerLocationLimit = 4096 + +const ( + callerPCMask = uintptr(3) + callerPCValue = uintptr(1) + callersPCValue = uintptr(3) + callerPCHashInit = 64 +) + +type callerLocationStore struct { + frames []CallerFrame + stack []CallerFrame + synthetic []CallerFrame + syntheticHash []uintptr + // Memoized synthetic PC bases for the static frames emitted around every + // Callers walk. Per-store because synthetic sequences are per-store. + callersPCBase uintptr + mainPCBase uintptr + goexitPCBase uintptr +} + +var callerLocationTLS = tls.Alloc[*callerLocationStore](nil) + +func PushCallerLocationFrame(entry uintptr, name, file string, startLine int) int { + store := callerLocationStoreForThread() + mark := len(store.stack) + store.stack = append(store.stack, CallerFrame{ + PC: entry, + Entry: entry, + Function: name, + File: file, + Line: startLine, + StartLine: startLine, + }) + return mark +} + +func PopCallerLocationFrame(mark int) { + store := callerLocationTLS.Get() + if store == nil { + return + } + oldLen := len(store.stack) + if mark < 0 || mark > oldLen { + return + } + var zero CallerFrame + for i := mark; i < oldLen; i++ { + store.stack[i] = zero + } + store.stack = store.stack[:mark] +} + +func RecordCallerLocation(entry uintptr, name, file string, line int) { + if entry == 0 || line <= 0 { + return + } + updateCurrentFrame(entry, name, file, line) + recordPCLocation(0, entry, name, file, line) +} + +func RecordPanicLocation(entry uintptr, name, file string, line int) { + if entry == 0 || line <= 0 { + return + } + updateCurrentFrame(entry, name, file, line) + recordPCLocation(0, entry, name, file, line) +} + +func updateCurrentFrame(entry uintptr, name, file string, line int) { + store := callerLocationTLS.Get() + if store == nil { + return + } + for i := len(store.stack) - 1; i >= 0; i-- { + frame := &store.stack[i] + if frame.Entry == entry { + frame.Function = name + frame.File = file + // For one entry the instrumented name/file operands are + // constants; only the line changes between call sites. Comparing + // just the line keeps this per-call path free of string + // comparisons while still invalidating the capture memo whenever + // the frame content can differ. + if frame.Line != line { + frame.Line = line + frame.captured = 0 + } + return + } + } +} + +func recordPCLocation(pc, entry uintptr, name, file string, line int) { + store := callerLocationStoreForThread() + for i := range store.frames { + frame := &store.frames[i] + if (pc != 0 && frame.PC == pc) || (pc == 0 && frame.PC == 0 && frame.Entry == entry) { + frame.PC = pc + frame.Entry = entry + frame.Function = name + frame.File = file + frame.Line = line + return + } + } + if len(store.frames) >= callerLocationLimit { + copy(store.frames, store.frames[1:]) + store.frames[len(store.frames)-1] = CallerFrame{} + store.frames = store.frames[:len(store.frames)-1] + } + store.frames = append(store.frames, CallerFrame{ + PC: pc, + Entry: entry, + Function: name, + File: file, + Line: line, + }) +} + +func Caller(skip int) (CallerFrame, bool) { + if skip < 0 { + return CallerFrame{}, false + } + store := callerLocationTLS.Get() + if store == nil || len(store.stack) == 0 { + return CallerFrame{}, false + } + if skip < len(store.stack) { + return store.captureFrameAt(&store.stack[len(store.stack)-1-skip], callerPCValue), true + } + switch skip - len(store.stack) { + case 0: + return store.captureFrame(runtimeMainFrame, callerPCValue), true + case 1: + return store.captureFrame(runtimeGoexitFrame, callerPCValue), true + default: + return CallerFrame{}, false + } +} + +func Callers(skip int, pcs []uintptr) int { + if len(pcs) == 0 { + return 0 + } + if skip < 0 { + skip = 0 + } + store := callerLocationTLS.Get() + if store == nil || len(store.stack) == 0 { + return 0 + } + // Unrolled emit sequence: no closure so nothing escapes, and frames the + // skip count drops are never captured at all. + n := 0 + if skip > 0 { + skip-- + } else { + pcs[n] = store.staticPC(runtimeCallersFrame, &store.callersPCBase, callersPCValue) + n++ + } + for i := len(store.stack) - 1; i >= 0; i-- { + if skip > 0 { + skip-- + continue + } + if n >= len(pcs) { + return n + } + pcs[n] = store.capturePC(&store.stack[i], callersPCValue) + n++ + } + if skip > 0 { + skip-- + } else { + if n >= len(pcs) { + return n + } + pcs[n] = store.staticPC(runtimeMainFrame, &store.mainPCBase, callersPCValue) + n++ + } + if skip <= 0 && n < len(pcs) { + pcs[n] = store.staticPC(runtimeGoexitFrame, &store.goexitPCBase, callersPCValue) + n++ + } + return n +} + +func SavePanicCallerFrames() { +} + +func BindCallerLocation(pc uintptr, rawName string) { + store := callerLocationTLS.Get() + if store == nil || pc == 0 { + return + } + if frame, ok := callerLocationByName(store, rawName); ok { + bindCallerLocationPC(pc, frame) + return + } +} + +var ( + runtimeCallersFrame = CallerFrame{Function: "runtime.Callers"} + runtimeMainFrame = CallerFrame{Function: "runtime.main"} + runtimeGoexitFrame = CallerFrame{Function: "runtime.goexit"} +) + +func callerLocationByName(store *callerLocationStore, rawName string) (CallerFrame, bool) { + if rawName == "" { + return CallerFrame{}, false + } + name := normalizeRuntimeFuncName(rawName) + for i := len(store.frames) - 1; i >= 0; i-- { + frame := store.frames[i] + if frame.PC == 0 && frame.Function == name && frame.Line != 0 { + return frame, true + } + } + return CallerFrame{}, false +} + +func bindCallerLocationPC(pc uintptr, frame CallerFrame) { + recordPCLocation(pc, frame.Entry, frame.Function, frame.File, frame.Line) + if pc > 0 { + recordPCLocation(pc-1, frame.Entry, frame.Function, frame.File, frame.Line) + } +} + +func FrameForPC(pc uintptr) (CallerFrame, bool) { + if pc&callerPCMask != 0 { + if frame, ok := syntheticFrameForPC(pc); ok { + return frame, true + } + } + store := callerLocationTLS.Get() + if store == nil || pc == 0 { + return CallerFrame{}, false + } + for i := len(store.frames) - 1; i >= 0; i-- { + frame := store.frames[i] + if frame.PC == pc { + return frame, true + } + } + entry := entryForPC(pc) + if entry == 0 { + return CallerFrame{}, false + } + var best CallerFrame + for _, frame := range store.frames { + if frame.PC == 0 || frame.PC > pc || frame.Entry != entry { + continue + } + if best.PC == 0 || frame.PC > best.PC { + best = frame + } + } + if best.PC != 0 { + best.PC = pc + return best, true + } + for i := len(store.frames) - 1; i >= 0; i-- { + frame := store.frames[i] + if frame.PC == 0 && frame.Entry == entry { + frame.PC = pc + return frame, true + } + } + return CallerFrame{}, false +} + +func syntheticFrameForPC(pc uintptr) (CallerFrame, bool) { + store := callerLocationTLS.Get() + if store == nil { + return CallerFrame{}, false + } + seq := pc >> 2 + if seq == 0 || seq > uintptr(len(store.synthetic)) { + return CallerFrame{}, false + } + frame := store.synthetic[seq-1] + if frame.PC>>2 != seq { + return CallerFrame{}, false + } + frame.PC = pc + if frame.Entry == 0 { + frame.Entry = pc + } + return frame, true +} + +func callerLocationStoreForThread() *callerLocationStore { + store := callerLocationTLS.Get() + if store == nil { + store = new(callerLocationStore) + callerLocationTLS.Set(store) + } + return store +} + +func (s *callerLocationStore) captureFrame(frame CallerFrame, pcValue uintptr) CallerFrame { + idx := s.internSyntheticFrame(frame) + rec := s.synthetic[idx] + seq := uintptr(idx + 1) + rec.PC = (seq << 2) | pcValue + if rec.Entry == 0 { + rec.Entry = rec.PC + } + return rec +} + +// capturePC returns the synthetic PC for a shadow-stack slot, memoizing the +// interned base in the slot so an unchanged frame costs two loads instead of +// a hash probe plus frame comparison. +func (s *callerLocationStore) capturePC(frame *CallerFrame, pcValue uintptr) uintptr { + if frame.captured != 0 { + return frame.captured | pcValue + } + idx := s.internSyntheticFrame(*frame) + base := uintptr(idx+1) << 2 + frame.captured = base + return base | pcValue +} + +// captureFrameAt is capturePC plus the full frame copy Caller needs. +func (s *callerLocationStore) captureFrameAt(frame *CallerFrame, pcValue uintptr) CallerFrame { + pc := s.capturePC(frame, pcValue) + rec := s.synthetic[(pc>>2)-1] + rec.PC = pc + if rec.Entry == 0 { + rec.Entry = rec.PC + } + return rec +} + +// staticPC memoizes the synthetic PC base of a process-static frame (e.g. +// runtime.main) in the per-store cache slot. +func (s *callerLocationStore) staticPC(frame CallerFrame, cache *uintptr, pcValue uintptr) uintptr { + if *cache == 0 { + *cache = uintptr(s.internSyntheticFrame(frame)+1) << 2 + } + return *cache | pcValue +} + +func (s *callerLocationStore) internSyntheticFrame(frame CallerFrame) int { + frame.captured = 0 + if len(s.syntheticHash) == 0 { + s.syntheticHash = make([]uintptr, callerPCHashInit) + } + if len(s.synthetic)*2 >= len(s.syntheticHash) { + s.rehashSyntheticFrames(len(s.syntheticHash) * 2) + } + slot := s.syntheticSlot(frame) + for { + idx := s.syntheticHash[slot] + if idx == 0 { + frame.PC = (uintptr(len(s.synthetic)+1) << 2) | callerPCValue + s.synthetic = append(s.synthetic, frame) + s.syntheticHash[slot] = uintptr(len(s.synthetic)) + return len(s.synthetic) - 1 + } + existing := s.synthetic[idx-1] + if sameSyntheticFrame(existing, frame) { + return int(idx - 1) + } + slot = (slot + 1) & (uintptr(len(s.syntheticHash)) - 1) + } +} + +func (s *callerLocationStore) rehashSyntheticFrames(size int) { + old := s.syntheticHash + s.syntheticHash = make([]uintptr, size) + for _, idx := range old { + if idx == 0 { + continue + } + frame := s.synthetic[idx-1] + slot := s.syntheticSlot(frame) + for s.syntheticHash[slot] != 0 { + slot = (slot + 1) & (uintptr(len(s.syntheticHash)) - 1) + } + s.syntheticHash[slot] = idx + } +} + +func (s *callerLocationStore) syntheticSlot(frame CallerFrame) uintptr { + h := frame.Entry ^ (uintptr(frame.Line) << 12) ^ (uintptr(frame.StartLine) << 24) + h ^= uintptr(len(frame.Function)) << 4 + h ^= uintptr(len(frame.File)) << 8 + return h & (uintptr(len(s.syntheticHash)) - 1) +} + +func sameSyntheticFrame(a, b CallerFrame) bool { + return a.Entry == b.Entry && + a.Function == b.Function && + a.File == b.File && + a.Line == b.Line && + a.StartLine == b.StartLine +} + +func entryForPC(pc uintptr) uintptr { + var info clitedebug.Info + if clitedebug.Addrinfo(unsafe.Pointer(pc), &info) == 0 { + return 0 + } + return uintptr(info.Saddr) +} + +func normalizeRuntimeFuncName(name string) string { + const commandLineArguments = "command-line-arguments." + if hasPrefix(name, commandLineArguments) { + name = "main." + name[len(commandLineArguments):] + } + if len(name) > 0 && name[0] == '_' { + name = name[1:] + } + return normalizeRuntimeAnonFuncName(name) +} + +func normalizeRuntimeAnonFuncName(name string) string { + dollar := lastIndexByte(name, '$') + if dollar < 0 || dollar == len(name)-1 { + return name + } + for i := dollar + 1; i < len(name); i++ { + if name[i] < '0' || name[i] > '9' { + return name + } + } + return name[:dollar] + ".func" + name[dollar+1:] +} + +func hasPrefix(s, prefix string) bool { + if len(s) < len(prefix) { + return false + } + for i := 0; i < len(prefix); i++ { + if s[i] != prefix[i] { + return false + } + } + return true +} + +func lastIndexByte(s string, c byte) int { + for i := len(s) - 1; i >= 0; i-- { + if s[i] == c { + return i + } + } + return -1 +} diff --git a/runtime/internal/runtime/z_rt.go b/runtime/internal/runtime/z_rt.go index 3b17c951e1..4cd79f22ac 100644 --- a/runtime/internal/runtime/z_rt.go +++ b/runtime/internal/runtime/z_rt.go @@ -49,6 +49,7 @@ func Recover() (ret any) { // Panic panics with a value. func Panic(v any) { + SavePanicCallerFrames() ptr := c.Malloc(unsafe.Sizeof(v)) *(*any)(ptr) = v excepKey.Set(ptr) diff --git a/ssa/decl.go b/ssa/decl.go index 115bf28fc2..3e17b40e9f 100644 --- a/ssa/decl.go +++ b/ssa/decl.go @@ -423,4 +423,9 @@ func (p Function) Inline(inline inlineAttr) { p.impl.AddFunctionAttr(inlineAttr) } +func (p Function) DisableTailCalls() { + attr := p.Pkg.mod.Context().CreateStringAttribute("disable-tail-calls", "true") + p.impl.AddFunctionAttr(attr) +} + // ----------------------------------------------------------------------------- diff --git a/ssa/funcinfo.go b/ssa/funcinfo.go new file mode 100644 index 0000000000..7e3979a9be --- /dev/null +++ b/ssa/funcinfo.go @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2024 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 ssa + +import "github.com/xgo-dev/llvm" + +const ( + FuncInfoMetadataName = "llgo.funcinfo" + PCLineMetadataName = "llgo.pcline" + funcInfoVersion = 1 + pcLineVersion = 1 +) + +// EnableFuncInfoMetadata controls emission of DCE-safe function source +// metadata. The metadata intentionally stores symbol names as strings instead +// of function pointer operands, so it can be consumed before materializing a +// final runtime line/func table without keeping otherwise-dead functions alive. +func (p Program) EnableFuncInfoMetadata(enable bool) { + p.enableFuncInfoMetadata = enable +} + +func (p Program) FuncInfoMetadataEnabled() bool { + return p.enableFuncInfoMetadata +} + +// EnableFuncInfoSites controls emission of the per-function site records +// (entry/stub/pc-line inline-asm fragments inside function bodies). They are +// gated separately from the funcinfo metadata tables because the +// body-embedded anchors shift instruction/scope layout enough to confuse +// debuggers; debug builds keep the tables (FuncForPC name/FileLine fidelity +// via the dlsym path) but drop the sites. +func (p Program) EnableFuncInfoSites(enable bool) { + p.enableFuncInfoSites = enable +} + +func (p Program) FuncInfoSitesEnabled() bool { + return p.enableFuncInfoSites +} + +// EmitFuncInfo records a function's linker symbol, Go name, and declaration +// source position as LLVM named metadata. The row layout is: +// +// !{i32 version, !"symbol", !"go.name", !"file", i32 line, i32 column} +func (p Package) EmitFuncInfo(symbol, name, file string, line, column int) { + if symbol == "" { + return + } + if line < 0 { + line = 0 + } + if column < 0 { + column = 0 + } + i32 := p.Prog.Int32().ll + p.mod.AddNamedMetadataOperand(FuncInfoMetadataName, + p.Prog.ctx.MDNode([]llvm.Metadata{ + llvm.ConstInt(i32, funcInfoVersion, false).ConstantAsMetadata(), + p.Prog.ctx.MDString(symbol), + p.Prog.ctx.MDString(name), + p.Prog.ctx.MDString(file), + llvm.ConstInt(i32, uint64(line), false).ConstantAsMetadata(), + llvm.ConstInt(i32, uint64(column), false).ConstantAsMetadata(), + }), + ) +} + +// EmitPCLineInfo records a PC label id and its source position. The id names a +// zero-byte label emitted in the function body; keeping the metadata string-only +// lets dead functions be removed without the line table holding address +// references to them. +func (p Package) EmitPCLineInfo(id uint64, symbol, file string, line, column int) { + if id == 0 || symbol == "" { + return + } + if line < 0 { + line = 0 + } + if column < 0 { + column = 0 + } + i32 := p.Prog.Int32().ll + i64 := p.Prog.Int64().ll + p.mod.AddNamedMetadataOperand(PCLineMetadataName, + p.Prog.ctx.MDNode([]llvm.Metadata{ + llvm.ConstInt(i32, pcLineVersion, false).ConstantAsMetadata(), + llvm.ConstInt(i64, id, false).ConstantAsMetadata(), + p.Prog.ctx.MDString(symbol), + p.Prog.ctx.MDString(file), + llvm.ConstInt(i32, uint64(line), false).ConstantAsMetadata(), + llvm.ConstInt(i32, uint64(column), false).ConstantAsMetadata(), + }), + ) +} diff --git a/ssa/memory.go b/ssa/memory.go index 565e705ba9..d5847f515b 100644 --- a/ssa/memory.go +++ b/ssa/memory.go @@ -63,6 +63,34 @@ func (b Builder) aggregateValue(t Type, flds ...llvm.Value) Expr { return Expr{aggregateValue(b.impl, t.ll, flds...), t} } +// LoadAndClearSinglePointer atomically copies a pointer-sized value out of ptr +// and clears the source slot. It handles either *P or *struct{P}. +func (b Builder) LoadAndClearSinglePointer(ptr Expr) (Expr, bool) { + elem := b.Prog.Elem(ptr.Type) + if elem.ll.TypeKind() == llvm.PointerTypeKind { + old := b.loadAndClearPointerWord(ptr.impl, elem.ll) + return Expr{old, elem}, true + } + + st, ok := types.Unalias(elem.RawType()).Underlying().(*types.Struct) + if !ok || st.NumFields() != 1 { + return Nil, false + } + field := b.Prog.rawType(st.Field(0).Type()) + if field.ll.TypeKind() != llvm.PointerTypeKind { + return Nil, false + } + fieldPtr := llvm.CreateStructGEP(b.impl, elem.ll, ptr.impl, 0) + old := b.loadAndClearPointerWord(fieldPtr, field.ll) + return b.aggregateValue(elem, old), true +} + +func (b Builder) loadAndClearPointerWord(ptr llvm.Value, typ llvm.Type) llvm.Value { + old := llvm.CreateLoad(b.impl, typ, ptr) + b.impl.CreateStore(llvm.ConstNull(typ), ptr) + return old +} + func aggregateValue(b llvm.Builder, tll llvm.Type, flds ...llvm.Value) llvm.Value { agg := llvm.Undef(tll) for i, fld := range flds { diff --git a/ssa/package.go b/ssa/package.go index 74256eddcc..9826102218 100644 --- a/ssa/package.go +++ b/ssa/package.go @@ -233,6 +233,9 @@ type aProgram struct { is32Bits bool enableGoGlobalDCE bool + + enableFuncInfoMetadata bool + enableFuncInfoSites bool } type AbiSymbol struct { diff --git a/ssa/ssa_test.go b/ssa/ssa_test.go index 74d4242286..078792e8b1 100644 --- a/ssa/ssa_test.go +++ b/ssa/ssa_test.go @@ -200,6 +200,150 @@ func TestNewFuncExLLVMUsed(t *testing.T) { } } +func TestFuncInfoMetadataDoesNotPreserveFunctions(t *testing.T) { + testFuncInfoMetadataDoesNotPreserveFunctions(t) +} + +func TestPCLineMetadataEmission(t *testing.T) { + testPCLineMetadataEmission(t) +} + +func testPCLineMetadataEmission(t *testing.T) { + t.Helper() + + prog := NewProgram(nil) + pkg := prog.NewPackage("main", "main") + + pkg.EmitPCLineInfo(0, "ignored", "ignored.go", -1, -1) + pkg.EmitPCLineInfo(0x1234, "", "ignored.go", -1, -1) + if ir := pkg.String(); strings.Contains(ir, PCLineMetadataName) { + t.Fatalf("invalid pcline rows should not emit metadata:\n%s", ir) + } + + pkg.EmitPCLineInfo(0x1234, "main.live", "call.go", 23, 5) + pkg.EmitPCLineInfo(0x5678, "main.negative", "negative.go", -7, -1) + ir := pkg.String() + for _, want := range []string{ + `!llgo.pcline = !{!`, + `i64 4660`, + `!"main.live"`, + `!"call.go"`, + `i32 23`, + `i32 5`, + `i64 22136`, + `!"main.negative"`, + `!"negative.go"`, + `i32 0`, + } { + if !strings.Contains(ir, want) { + t.Fatalf("missing pcline field %s:\n%s", want, ir) + } + } + if strings.Contains(ir, `ptr @main.live`) || strings.Contains(ir, `ptr @"main.live"`) { + t.Fatalf("pcline metadata must not contain function pointer operands:\n%s", ir) + } +} + +func testFuncInfoMetadataDoesNotPreserveFunctions(t *testing.T) { + t.Helper() + + prog := NewProgram(nil) + if prog.FuncInfoMetadataEnabled() { + t.Fatal("funcinfo metadata should be disabled by default") + } + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + if !prog.FuncInfoMetadataEnabled() { + t.Fatal("funcinfo metadata should be enabled") + } + + pkg := prog.NewPackage("main", "main") + sig := types.NewSignatureType(nil, nil, nil, nil, nil, false) + + pkg.NewFunc("main.unused", sig, InGo) + pkg.EmitFuncInfo("", "ignored", "ignored.go", -1, -1) + if ir := pkg.String(); strings.Contains(ir, FuncInfoMetadataName) { + t.Fatalf("empty symbol should not emit funcinfo metadata:\n%s", ir) + } + + pkg.EmitFuncInfo("main.unused", "main.unused", "unused.go", 7, 1) + pkg.EmitFuncInfo("main.negative", "main.negative", "negative.go", -7, -1) + ir := pkg.String() + + if !strings.Contains(ir, `!llgo.funcinfo = !{!`) { + t.Fatalf("missing %s metadata:\n%s", FuncInfoMetadataName, ir) + } + for _, want := range []string{`!"main.unused"`, `!"unused.go"`, `i32 7`, `!"main.negative"`, `!"negative.go"`, `i32 0`} { + if !strings.Contains(ir, want) { + t.Fatalf("missing funcinfo field %s:\n%s", want, ir) + } + } + if strings.Contains(ir, "llvm.compiler.used") { + t.Fatalf("funcinfo metadata must not preserve symbols with llvm.compiler.used:\n%s", ir) + } + if strings.Contains(ir, `ptr @"main.unused"`) || strings.Contains(ir, `ptr @main.unused`) { + t.Fatalf("funcinfo metadata must not contain function pointer operands:\n%s", ir) + } +} + +func TestFuncInfoMetadataDoesNotBlockGlobalDCE(t *testing.T) { + testFuncInfoMetadataDoesNotBlockGlobalDCE(t) +} + +func testFuncInfoMetadataDoesNotBlockGlobalDCE(t *testing.T) { + t.Helper() + + prog := NewProgram(nil) + pkg := prog.NewPackage("main", "main") + sig := types.NewSignatureType(nil, nil, nil, nil, nil, false) + + live := pkg.NewFunc("main.main", sig, InGo) + lb := live.MakeBody(1) + lb.Return() + lb.EndBuild() + + unused := pkg.NewFuncEx("main.unused", sig, InGo, false, true) + ub := unused.MakeBody(1) + ub.Return() + ub.EndBuild() + pkg.EmitFuncInfo(unused.Name(), unused.Name(), "unused.go", 7, 1) + + mod := pkg.Module() + if mod.NamedFunction("main.unused").IsNil() { + t.Fatal("missing main.unused before DCE") + } + mod.SetDataLayout(prog.DataLayout()) + mod.SetTarget(prog.Target().Spec().Triple) + pbo := llvm.NewPassBuilderOptions() + defer pbo.Dispose() + if err := llvm.VerifyModule(mod, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify module before DCE: %v", err) + } + if err := mod.RunPasses("globaldce", prog.TargetMachine(), pbo); err != nil { + t.Fatalf("run globaldce: %v", err) + } + if !mod.NamedFunction("main.unused").IsNil() { + t.Fatalf("funcinfo metadata kept main.unused alive:\n%s", mod.String()) + } + if mod.NamedFunction("main.main").IsNil() { + t.Fatalf("globaldce removed externally visible live function:\n%s", mod.String()) + } + if ir := mod.String(); !strings.Contains(ir, `!"main.unused"`) { + t.Fatalf("funcinfo metadata should remain available for later materialization:\n%s", ir) + } +} + +func TestDevLTOGlobalDCEFuncInfoMetadata(t *testing.T) { + requireGoGlobalDCE(t) + testFuncInfoMetadataDoesNotPreserveFunctions(t) + testFuncInfoMetadataDoesNotBlockGlobalDCE(t) +} + +func TestDevLTOGlobalDCEPCLineMetadata(t *testing.T) { + requireGoGlobalDCE(t) + testPCLineMetadataEmission(t) +} + func requireGoGlobalDCE(t *testing.T) { t.Helper() } @@ -1455,6 +1599,71 @@ func TestZeroSizedLoadEmitsNilDerefGuard(t *testing.T) { } } +func TestLoadAndClearSinglePointer(t *testing.T) { + prog := NewProgram(nil) + prog.sizes = types.SizesFor("gc", runtime.GOARCH) + pkg := prog.NewPackage("bar", "foo/bar") + + ptrToInt := types.NewPointer(types.Typ[types.Int]) + wrapStruct := types.NewStruct([]*types.Var{ + types.NewField(token.NoPos, nil, "p", ptrToInt, false), + }, nil) + + params := types.NewTuple( + types.NewVar(0, nil, "p", types.NewPointer(ptrToInt)), + types.NewVar(0, nil, "s", types.NewPointer(wrapStruct)), + ) + results := types.NewTuple( + types.NewVar(0, nil, "", ptrToInt), + types.NewVar(0, nil, "", wrapStruct), + ) + sig := types.NewSignatureType(nil, nil, nil, params, results, false) + fn := pkg.NewFunc("loadAndClear", sig, InGo) + b := fn.MakeBody(1) + pv, ok := b.LoadAndClearSinglePointer(fn.Param(0)) + if !ok { + t.Fatal("pointer slot should be load-and-clearable") + } + sv, ok := b.LoadAndClearSinglePointer(fn.Param(1)) + if !ok { + t.Fatal("single-pointer struct slot should be load-and-clearable") + } + if got, want := sv.impl.Type().String(), sv.Type.ll.String(); got != want { + t.Fatalf("single-pointer struct load-and-clear type = %s, want %s", got, want) + } + b.Return(pv, sv) + b.EndBuild() + + ir := fn.impl.String() + if got := strings.Count(ir, "store ptr null"); got != 2 { + t.Fatalf("LoadAndClearSinglePointer should clear both pointer slots, got %d stores:\n%s", got, ir) + } + if got := strings.Count(ir, "load ptr"); got < 2 { + t.Fatalf("LoadAndClearSinglePointer should load both pointer slots, got %d loads:\n%s", got, ir) + } + + noPtrStruct := types.NewStruct([]*types.Var{ + types.NewField(token.NoPos, nil, "i", types.Typ[types.Int], false), + }, nil) + multiStruct := types.NewStruct([]*types.Var{ + types.NewField(token.NoPos, nil, "p", ptrToInt, false), + types.NewField(token.NoPos, nil, "q", ptrToInt, false), + }, nil) + falseCases := []types.Type{ + types.NewPointer(types.Typ[types.Int]), + types.NewPointer(noPtrStruct), + types.NewPointer(multiStruct), + } + for i, typ := range falseCases { + fn := pkg.NewFunc(fmt.Sprintf("rejectLoadAndClear%d", i), types.NewSignatureType(nil, nil, nil, + types.NewTuple(types.NewVar(0, nil, "p", typ)), nil, false), InGo) + b := fn.MakeBody(1) + if _, ok := b.LoadAndClearSinglePointer(fn.Param(0)); ok { + t.Fatalf("LoadAndClearSinglePointer accepted %v", typ) + } + } +} + func TestTypeAssertSingleElemArrayUsesInsertValue(t *testing.T) { prog := NewProgram(nil) prog.sizes = types.SizesFor("gc", runtime.GOARCH) diff --git a/test/go/finalizer_test.go b/test/go/finalizer_test.go index aceb2d2a2c..6c3f6ffea6 100644 --- a/test/go/finalizer_test.go +++ b/test/go/finalizer_test.go @@ -17,6 +17,8 @@ package gotest import ( + "os" + "path/filepath" "runtime" "testing" "time" @@ -89,6 +91,116 @@ func TestRuntimeSetFinalizerCancel(t *testing.T) { } } +const finalizerStackLivenessProbe = `package main + +import ( + "fmt" + "runtime" +) + +type HeapObj [8]int64 + +type StkObj struct { + h *HeapObj +} + +var n int +var c int = -1 +var null StkObj +var sink *HeapObj + +func gc() { + runtime.GC() + runtime.GC() + runtime.GC() + n++ +} + +func keepAliveCase() { + c = -1 + n = 0 + f() + gc() + if c != 1 { + panic(fmt.Sprintf("keepalive collection phase = %d, want 1", c)) + } +} + +func f() { + var s StkObj + s.h = new(HeapObj) + runtime.SetFinalizer(s.h, func(h *HeapObj) { + c = n + }) + g(&s) + gc() +} + +func g(s *StkObj) { + gc() + runtime.KeepAlive(s) + gc() +} + +//go:noinline +func use(p *StkObj) { +} + +//go:noinline +func ambiguousArgCase(s StkObj, b bool) { + var p *StkObj + if b { + p = &s + } else { + p = &null + } + use(p) + gc() + sink = p.h + gc() + sink = nil + gc() +} + +func runAmbiguousArgCase(b bool, want int) { + var s StkObj + s.h = new(HeapObj) + c = -1 + n = 0 + runtime.SetFinalizer(s.h, func(h *HeapObj) { + c = n + }) + ambiguousArgCase(s, b) + if c != want { + panic(fmt.Sprintf("ambiguous arg b=%v collection phase = %d, want %d", b, c, want)) + } +} + +func main() { + keepAliveCase() + runAmbiguousArgCase(true, 2) + runAmbiguousArgCase(false, 0) +} +` + +func TestRuntimeSetFinalizerStackObjectLiveness(t *testing.T) { + dir, err := os.MkdirTemp("", "llgo-finalizer-stack-*") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + mainFile := filepath.Join(dir, "main.go") + if err := os.WriteFile(mainFile, []byte(finalizerStackLivenessProbe), 0644); err != nil { + t.Fatal(err) + } + + runGoCmd(t, dir, "run", mainFile) + + root := findLLGoRoot(t) + t.Setenv("LLGO_ROOT", root) + runGoCmd(t, root, "run", "./cmd/llgo", "run", mainFile) +} + func runGCWithTimeout(t *testing.T) { t.Helper() done := make(chan struct{}) diff --git a/test/go/runtime_lineinfo_stack_test.go b/test/go/runtime_lineinfo_stack_test.go new file mode 100644 index 0000000000..52fffcabef --- /dev/null +++ b/test/go/runtime_lineinfo_stack_test.go @@ -0,0 +1,320 @@ +/* + * 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" + "strconv" + "strings" + "testing" +) + +const runtimeLineInfoProbe = `package main + +import ( + "reflect" + "strconv" + "runtime" + "runtime/debug" + "strings" + _ "unsafe" +) + +func main() { + checkCaller() + checkCallerSkip() + checkFrames() // FRAMES_MAIN_MARK + checkFuncForPC() + checkFuncForPCFunctionValue() + checkFuncInfoRename() + checkRuntimeStack() + checkPanicStack() +} + +//go:noinline +func checkCaller() { + _, file, line, ok := runtime.Caller(0) // CALLER_MARK + if !ok || !strings.HasSuffix(file, "main.go") || line != CALLER_LINE { + panic("bad caller: " + file + ":" + strconv.Itoa(line)) + } +} + +//go:noinline +func checkCallerSkip() { + helperCallerSkip() // CALLER_SKIP_MARK +} + +//go:noinline +func helperCallerSkip() { + _, file, line, ok := runtime.Caller(1) + if !ok || !strings.HasSuffix(file, "main.go") || line != CALLER_SKIP_LINE { + panic("bad caller skip: " + file + ":" + strconv.Itoa(line)) + } +} + +//go:noinline +func checkFrames() { + var pcs [8]uintptr + n := runtime.Callers(0, pcs[:]) // FRAMES_CHECK_MARK + frames := runtime.CallersFrames(pcs[:n]) + seenCheckFrames := false + seenMain := false + for { + frame, more := frames.Next() + if frame.Function == "main.checkFrames" { + if !strings.HasSuffix(frame.File, "main.go") || frame.Line != FRAMES_CHECK_LINE { + panic("bad checkFrames frame: " + frame.File + ":" + strconv.Itoa(frame.Line)) + } + seenCheckFrames = true + } + if frame.Function == "main.main" { + if !strings.HasSuffix(frame.File, "main.go") || frame.Line != FRAMES_MAIN_LINE { + panic("bad main frame: " + frame.File + ":" + strconv.Itoa(frame.Line)) + } + seenMain = true + } + if seenCheckFrames && seenMain { + return + } + if !more { + break + } + } + panic("missing frame") +} + +//go:noinline +func checkFuncForPC() { + pc, _, _, ok := runtime.Caller(0) // FUNC_FILELINE_MARK + if !ok { + panic("missing pc") + } + fn := runtime.FuncForPC(pc) + if fn == nil { + panic("missing func") + } + if name := fn.Name(); name != "main.checkFuncForPC" { + panic("bad func: " + name) + } + if entry := fn.Entry(); entry == 0 { + panic("missing func entry") + } + file, line := fn.FileLine(pc) + if !strings.HasSuffix(file, "main.go") || line != FUNC_FILELINE_LINE { + panic("bad func fileline: " + file + ":" + strconv.Itoa(line)) + } +} + +//go:noinline +func entryPCTarget() int { + return 7 // FUNC_ENTRY_TARGET_MARK +} + +//go:noinline +func checkFuncForPCFunctionValue() { + if entryPCTarget() != 7 { + panic("bad target") + } + pc := reflect.ValueOf(entryPCTarget).Pointer() + if pc == 0 { + panic("missing function value pc") + } + fn := runtime.FuncForPC(pc) + if fn == nil { + panic("missing function value func") + } + if name := fn.Name(); name != "main.entryPCTarget" { + panic("bad function value func: " + name) + } + if entry := fn.Entry(); entry == 0 { + panic("missing function value entry") + } + file, line := fn.FileLine(pc) + if !strings.HasSuffix(file, "main.go") || line != FUNC_ENTRY_TARGET_LINE { + panic("bad function value fileline: " + file + ":" + strconv.Itoa(line)) + } +} + +//go:noinline +func checkFuncInfoRename() { + pc := renamedPC() + if name := runtime.FuncForPC(pc).Name(); name != "main.renamedPC" { + panic("bad renamed func: " + name) + } +} + +//go:linkname renamedPC main.renamedPCSymbol +//go:noinline +func renamedPC() uintptr { + pc, _, _, ok := runtime.Caller(0) + if !ok { + panic("missing renamed pc") + } + return pc +} + +//go:noinline +func checkRuntimeStack() { + var buf [4096]byte + n := runtime.Stack(buf[:], false) // RUNTIME_STACK_MARK + stack := string(buf[:n]) + if !strings.Contains(stack, "main.checkRuntimeStack") || !strings.Contains(stack, "main.go:RUNTIME_STACK_LINE") { + panic("bad runtime stack: " + stack) + } +} + +//go:noinline +func checkPanicStack() { + defer func() { // DEBUG_STACK_MARK + if recover() == nil { + panic("missing panic") + } + stack := string(debug.Stack()) // DEBUG_STACK_CALL_MARK + if !strings.Contains(stack, "main.checkPanicStack") || !strings.Contains(stack, "main.go:DEBUG_STACK_LINE") { + panic("bad stack: " + stack) + } + }() + s := []int{1, 2, 3} + _ = s[3] +} +` + +func TestRuntimeLineInfoAndStack(t *testing.T) { + source := runtimeLineInfoProbe + source = strings.ReplaceAll(source, "CALLER_LINE", strconv.Itoa(markerLine(source, "CALLER_MARK"))) + source = strings.ReplaceAll(source, "CALLER_SKIP_LINE", strconv.Itoa(markerLine(source, "CALLER_SKIP_MARK"))) + source = strings.ReplaceAll(source, "FRAMES_MAIN_LINE", strconv.Itoa(markerLine(source, "FRAMES_MAIN_MARK"))) + source = strings.ReplaceAll(source, "FRAMES_CHECK_LINE", strconv.Itoa(markerLine(source, "FRAMES_CHECK_MARK"))) + source = strings.ReplaceAll(source, "FUNC_FILELINE_LINE", strconv.Itoa(markerLine(source, "FUNC_FILELINE_MARK"))) + source = strings.ReplaceAll(source, "FUNC_ENTRY_TARGET_LINE", strconv.Itoa(markerLine(source, "FUNC_ENTRY_TARGET_MARK"))) + source = strings.ReplaceAll(source, "RUNTIME_STACK_LINE", strconv.Itoa(markerLine(source, "RUNTIME_STACK_MARK"))) + source = strings.ReplaceAll(source, "DEBUG_STACK_LINE", strconv.Itoa(markerLine(source, "DEBUG_STACK_CALL_MARK"))) + + dir := t.TempDir() + file := filepath.Join(dir, "main.go") + if err := os.WriteFile(file, []byte(source), 0644); err != nil { + t.Fatal(err) + } + + repoRoot := findStringConversionRepoRoot(t) + t.Setenv("LLGO_ROOT", repoRoot) + cmd := exec.Command("go", "run", "./cmd/llgo", "run", "-a", file) + cmd.Dir = repoRoot + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("llgo lineinfo probe failed: %v\n%s", err, out) + } +} + +const runtimeFuncInfoConcurrentFirstUseProbe = `package main + +import ( + "runtime" + "strconv" + "strings" + "sync" +) + +func main() { + const n = 32 + start := make(chan struct{}) + errc := make(chan string, n) + var wg sync.WaitGroup + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + defer wg.Done() + <-start + errc <- checkRuntimeInfo() + }() + } + close(start) + wg.Wait() + close(errc) + for err := range errc { + if err != "" { + panic(err) + } + } +} + +//go:noinline +func checkRuntimeInfo() string { + pc, file, line, ok := runtime.Caller(0) // CONCURRENT_CALLER_MARK + if !ok || !strings.HasSuffix(file, "main.go") || line != CONCURRENT_CALLER_LINE { + return "bad caller: " + file + ":" + strconv.Itoa(line) + } + fn := runtime.FuncForPC(pc) + if fn == nil || fn.Name() != "main.checkRuntimeInfo" { + name := "" + if fn != nil { + name = fn.Name() + } + return "bad func: " + name + } + file, line = fn.FileLine(pc) + if !strings.HasSuffix(file, "main.go") || line != CONCURRENT_CALLER_LINE { + return "bad fileline: " + file + ":" + strconv.Itoa(line) + } + var pcs [8]uintptr + n := runtime.Callers(0, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + for { + frame, more := frames.Next() + if frame.Function == "main.checkRuntimeInfo" { + if !strings.HasSuffix(frame.File, "main.go") || frame.Line == 0 { + return "bad frame: " + frame.File + ":" + strconv.Itoa(frame.Line) + } + return "" + } + if !more { + return "missing frame" + } + } +} +` + +func TestRuntimeFuncInfoConcurrentFirstUse(t *testing.T) { + source := runtimeFuncInfoConcurrentFirstUseProbe + source = strings.ReplaceAll(source, "CONCURRENT_CALLER_LINE", strconv.Itoa(markerLine(source, "CONCURRENT_CALLER_MARK"))) + + dir := t.TempDir() + file := filepath.Join(dir, "main.go") + if err := os.WriteFile(file, []byte(source), 0644); err != nil { + t.Fatal(err) + } + + repoRoot := findStringConversionRepoRoot(t) + t.Setenv("LLGO_ROOT", repoRoot) + cmd := exec.Command("go", "run", "./cmd/llgo", "run", "-a", file) + cmd.Dir = repoRoot + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("llgo concurrent funcinfo probe failed: %v\n%s", err, out) + } +} + +func markerLine(source, marker string) int { + line := 1 + for _, part := range strings.SplitAfter(source, "\n") { + if strings.Contains(part, marker) { + return line + } + line++ + } + panic("missing marker " + marker) +} diff --git a/test/go/runtime_statement_line_test.go b/test/go/runtime_statement_line_test.go new file mode 100644 index 0000000000..81430b49f0 --- /dev/null +++ b/test/go/runtime_statement_line_test.go @@ -0,0 +1,211 @@ +/* + * 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" + "strconv" + "strings" + "testing" +) + +const runtimeStatementLineProbe = `package main + +import ( + "runtime" + "runtime/debug" + "strconv" + "strings" +) + +type Wrapper struct { + a []int +} + +func (w Wrapper) Get(i int) int { + return w.a[i] +} + +func main() { + checkCallerStatement() + checkCallersFramesStatement() + checkInterfaceIndirectCaller() + checkClosureIndirectCaller() + checkAdjacentRuntimeStack() + checkRecoveredDebugStackBounds() +} + +//go:noinline +func checkCallerStatement() { + _, file, line, ok := runtime.Caller(0) // CALLER_STMT_MARK + if !ok || !strings.HasSuffix(file, "main.go") || line != CALLER_STMT_LINE { + panic("bad caller statement: " + file + ":" + strconv.Itoa(line)) + } +} + +//go:noinline +func checkCallersFramesStatement() { + var pcs [16]uintptr + n := runtime.Callers(0, pcs[:]) // CALLERS_STMT_MARK + frames := runtime.CallersFrames(pcs[:n]) + for { + frame, more := frames.Next() + if frame.Function == "main.checkCallersFramesStatement" { + if !strings.HasSuffix(frame.File, "main.go") || frame.Line != CALLERS_STMT_LINE { + panic("bad callers frame: " + frame.File + ":" + strconv.Itoa(frame.Line)) + } + fn := runtime.FuncForPC(frame.PC - 1) + if fn == nil || fn.Name() != "main.checkCallersFramesStatement" { + name := "" + if fn != nil { + name = fn.Name() + } + panic("bad FuncForPC(pc-1): " + name) + } + file, line := fn.FileLine(frame.PC - 1) + if !strings.HasSuffix(file, "main.go") || line != CALLERS_STMT_LINE { + panic("bad Func.FileLine(pc-1): " + file + ":" + strconv.Itoa(line)) + } + return + } + if !more { + break + } + } + panic("missing callers frame") +} + +type indirectCaller interface { + call() +} + +type indirectCallerImpl struct{} + +//go:noinline +func checkInterfaceIndirectCaller() { + var c indirectCaller = indirectCallerImpl{} + c.call() // INTERFACE_CALL_MARK +} + +//go:noinline +func (indirectCallerImpl) call() { + interfaceMiddle() +} + +//go:noinline +func interfaceMiddle() { + checkCallerLine("interface", 2, INTERFACE_CALL_LINE) +} + +//go:noinline +func checkClosureIndirectCaller() { + f := closureLayer(closureLayer(func() { + checkCallerLine("closure", 3, CLOSURE_CALL_LINE) + })) + f() // CLOSURE_CALL_MARK +} + +//go:noinline +func closureLayer(next func()) func() { + return func() { + next() + } +} + +//go:noinline +func checkCallerLine(kind string, skip, want int) { + _, file, line, ok := runtime.Caller(skip) + if !ok || !strings.HasSuffix(file, "main.go") || line != want { + panic("bad " + kind + " indirect caller line: " + file + ":" + strconv.Itoa(line)) + } +} + +//go:noinline +func checkAdjacentRuntimeStack() { + var buf1, buf2 [4096]byte + n1 := runtime.Stack(buf1[:], false) // STACK_ONE_MARK + n2 := runtime.Stack(buf2[:], false) // STACK_TWO_MARK + line1 := stackLineFor(string(buf1[:n1]), "main.checkAdjacentRuntimeStack") + line2 := stackLineFor(string(buf2[:n2]), "main.checkAdjacentRuntimeStack") + if line1 != STACK_ONE_LINE || line2 != STACK_TWO_LINE || line1+1 != line2 { + panic("bad adjacent stack lines: " + strconv.Itoa(line1) + "," + strconv.Itoa(line2)) + } +} + +//go:noinline +func checkRecoveredDebugStackBounds() { + defer func() { + if recover() == nil { + panic("missing bounds panic") + } + stack := string(debug.Stack()) + if !strings.Contains(stack, "main.go:BOUNDS_LINE") { + panic("bad recovered stack: " + stack) + } + }() + foo := Wrapper{a: []int{0, 1, 2}} + _ = foo.Get(3) // BOUNDS_MARK +} + +func stackLineFor(stack, fn string) int { + lines := strings.Split(stack, "\n") + for i := 0; i+1 < len(lines); i++ { + if strings.TrimSpace(lines[i]) == fn+"()" { + loc := strings.TrimSpace(lines[i+1]) + colon := strings.LastIndexByte(loc, ':') + if colon < 0 { + return 0 + } + rest := loc[colon+1:] + end := strings.IndexByte(rest, ' ') + if end >= 0 { + rest = rest[:end] + } + n, _ := strconv.Atoi(rest) + return n + } + } + return 0 +} +` + +func TestRuntimeStatementLineInfo(t *testing.T) { + source := runtimeStatementLineProbe + source = strings.ReplaceAll(source, "CALLER_STMT_LINE", strconv.Itoa(markerLine(source, "CALLER_STMT_MARK"))) + source = strings.ReplaceAll(source, "CALLERS_STMT_LINE", strconv.Itoa(markerLine(source, "CALLERS_STMT_MARK"))) + source = strings.ReplaceAll(source, "INTERFACE_CALL_LINE", strconv.Itoa(markerLine(source, "INTERFACE_CALL_MARK"))) + source = strings.ReplaceAll(source, "CLOSURE_CALL_LINE", strconv.Itoa(markerLine(source, "CLOSURE_CALL_MARK"))) + source = strings.ReplaceAll(source, "STACK_ONE_LINE", strconv.Itoa(markerLine(source, "STACK_ONE_MARK"))) + source = strings.ReplaceAll(source, "STACK_TWO_LINE", strconv.Itoa(markerLine(source, "STACK_TWO_MARK"))) + source = strings.ReplaceAll(source, "BOUNDS_LINE", strconv.Itoa(markerLine(source, "BOUNDS_MARK"))) + + dir := t.TempDir() + file := filepath.Join(dir, "main.go") + if err := os.WriteFile(file, []byte(source), 0644); err != nil { + t.Fatal(err) + } + + repoRoot := findStringConversionRepoRoot(t) + t.Setenv("LLGO_ROOT", repoRoot) + cmd := exec.Command("go", "run", "./cmd/llgo", "run", "-a", file) + cmd.Dir = repoRoot + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("llgo statement line probe failed: %v\n%s", err, out) + } +} diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index d16ea32788..4305c25b49 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -2071,10 +2071,6 @@ xfails: directive: runoutput case: rangegen.go reason: go1.26 goroot ci-mode runoutput failure on linux/amd64 - - platform: darwin/arm64 - directive: run - case: deferfin.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: heapsampling.go @@ -2099,14 +2095,6 @@ xfails: directive: run case: recover4.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: stackobj.go - reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: stackobj3.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: fixedbugs/bug347.go @@ -2159,10 +2147,6 @@ xfails: directive: run case: fixedbugs/issue29504.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue29735.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: fixedbugs/issue32477.go