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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/goallc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ concurrency:
cancel-in-progress: true

env:
PINNED_LLVM_RELEASE: goallc-llvm23.1.0-20260814T092516Z
PINNED_LLVM_REVISION: 3710742685729c697378ab13a456484e5693aba9
PINNED_LLVM_RELEASE: goallc-llvm23.1.0-20260815T075241Z
PINNED_LLVM_REVISION: 9bd4c90aca1584de810fd081bc1fd6a0cce602dd

jobs:
llvm-payload:
Expand Down
37 changes: 19 additions & 18 deletions doc/goallc-llvm-goobj.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,10 @@ use 的 local 不生成 LLVM 栈槽,避免无意义地增大 frame。

GoObj stack growth、SP 恢复和当前 stack-map 路径只支持编译期固定 frame。
因此 amd64 和 arm64 GoObj target 对 variable-sized `alloca` 都确定性报错,
不能把动态栈分配交给 LLVM 通用 lowering。当前 GoObj writer 的 args/locals
pointer maps 仍为空;本实现只保证现有固定栈槽的 dominance、frame placement
和 stack-growth 约束,不代表已经支持完整 precise-GC stack maps、goroutine
defer 或 panic unwind。
不能把动态栈分配交给 LLVM 通用 lowering。当前 GoObj writer 已为所有 GoObj Go
函数生成类型推导的入口 ArgsPointerMaps,并为已支持的普通 statepoint 和固定
`alloca` 生成 LocalsPointerMaps;这仍不代表已经支持完整 precise-GC stack maps、
goroutine、defer 或 panic unwind。

对应回归包括:

Expand Down Expand Up @@ -273,9 +273,10 @@ pass plugin 默认从 `llc` 所属 LLVM payload 的
wrapper 覆盖的 compile action 重新编译。

插件的功能性源码和测试位于 Go 仓库
`src/cmd/llvmplugin`,不放入 LLVM 源码树。LLVM 只提供通用的
`-load-pass-plugin` 和 pre-codegen callback;GoALLC statepoint rewrite 及其
pass 顺序都应继续在这个 Go-owned 工程中实现。当前
`src/cmd/llvmplugin`,不放入 LLVM 源码树。LLVM 提供通用的
`-load-pass-plugin`、pre-codegen callback,以及 GoObj 对 Machine StackMaps
的对象格式适配;GoALLC statepoint rewrite 及其 pass 顺序都应继续在这个
Go-owned 工程中实现。当前
`runPreCodeGenPipeline` 调用 Go-owned statepoint pass:它对 Go ABI 函数执行
CFG 逆向数据流活跃性分析,为普通调用分配稳定 callsite ID,生成
`gc.statepoint` / `gc.relocate`,并识别 `gc-leaf-function`。受管指针分类当前
Expand All @@ -285,21 +286,21 @@ aggregate、`invoke`、`musttail` 和非 leaf inline asm fail closed。未来
compile 进程内集成 LLVM 时直接复用该 core 入口,不经过 plugin adapter。

机器位置不通过修改 LLVM 通用 `StackMaps.cpp` 截获。SSA→LLVM IR 前端为 Go ABI
函数声明 `gc "goallc"` 和 `go-stack-growth-statepoint`,插件只负责注册对应的
GC strategy 与 `GCMetadataPrinter::emitStackMaps`,并消费这些前端标记。在
函数声明 `gc "goallc"`;GoObj Go 函数默认采用原生 Go 的可扩栈策略,只有
`go-nosplit`、`go-systemstack` 这样的例外策略需要额外属性。插件负责注册对应的
GC strategy、执行 statepoint rewrite 并消费这些前端标记。在 LLVM GoObj
AsmPrinter 模块收尾阶段
读取标准 `FnInfos/CSInfos`,跳过 statepoint 的 CC、flags 和 deopt 前缀后,把
原始 GC locations 写入 MCContext。GoObj writer 在最终 layout 后完成 SP
校验、`Direct`/`Indirect` 解释、LocalsPointerMaps 和 PCDATA_StackMapIndex
编码。GoALLC 要求 StackMaps 记录 CALL 起点;map 从 CALL 开始。前端添加的
`go-stack-growth-statepoint` 属性使 LLVM 在 PEI 阶段把
`runtime.morestack` 调用生成为物理 MIR `STATEPOINT`。其 deopt 和 GC alloca
区为空,GC pointer 区则记录类型推导出的入口参数 home。它从 morestack CALL
起点选择入口 ArgsPointerMaps 和空 locals bitmap,因此普通调用与栈增长调用
走同一 Machine StackMaps 链路,且不依赖 return PC 反推调用范围。已有
Machine `STATEPOINT` 但缺少该前端属性时,
LLVM target lowering 会 fail closed;不再使用 slow-path reset label 兼容普通
morestack CALL。GoObj 先写索引 0 的
编码。GoALLC 要求 StackMaps 记录 CALL 起点;map 从 CALL 开始。LLVM target
formal lowering 为每个 GoObj Go 函数推导入口参数 home,并用一个独立、零字节的
`EntryArgsStackMapID` 表达函数级 ArgsPointerMaps;该记录不是调用点,不生成
PCDATA。非 nosplit 函数在 PEI 阶段额外把 `runtime.morestack` 调用生成为物理、
root-free 的 `StackGrowthStatepointID`,从 CALL 起点选择入口 ArgsPointerMaps 和
空 locals bitmap。nosplit 函数不得包含这个 statepoint。这样普通调用与栈增长
调用仍走同一 Machine StackMaps 链路,且不依赖 return PC 反推调用范围,同时
函数级入口图不再伪装成 morestack 调用。GoObj 先写索引 0 的
`PCDATA_UnsafePoint`(当前恒为 safe 的 `-1`),再写索引 1 的
`PCDATA_StackMapIndex`;不能只写后一张表,否则 linker 会把它误认成索引 0。
`Direct SP+offset` 是栈地址本身,不表示该 slot 存有 pointer,因此不会设置
Expand Down
21 changes: 16 additions & 5 deletions src/cmd/compile/internal/ssa/ssa2llvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ const goABI0SymbolSuffix = "<ABI0>"
const goResultsTupleAttr = "go_results_tuple"
const goGCStrategy = "goallc"
const goGCLeafFunctionAttr = "gc-leaf-function"
const goStackGrowthStatepointAttr = "go-stack-growth-statepoint"
const goNoSplitAttr = "go-nosplit"
const goSystemStackAttr = "go-systemstack"
const goAsyncUnsafeAttr = "go-async-unsafe"
const goWriteBarrierIntrinsic = "llvm.go.gc.write.barrier"
const goDeferEdgeIntrinsic = "llvm.go.defer.edge"
Expand Down Expand Up @@ -3087,10 +3088,20 @@ func LLVMCompile(f *Func) {
}
}
FCtxt.LF.AddFunctionAttr(GlobalCtxt.CreateStringAttribute(goAsyncUnsafeAttr, ""))
// TODO(goallc): Once LLVM lowering propagates the compiler's precise
// morestack policy, attach this only to functions whose prologue can grow
// the Go stack.
FCtxt.LF.AddFunctionAttr(GlobalCtxt.CreateStringAttribute(goStackGrowthStatepointAttr, ""))
// A //go:nosplit function must not acquire that late morestack edge. Besides
// violating the runtime's nosplit call graph, it would expose a safepoint the
// frontend deliberately prohibited. Give target frame lowering the source
// policy directly instead of asking it to infer a pragma from GoObj metadata.
if f.NoSplit {
FCtxt.LF.AddFunctionAttr(GlobalCtxt.CreateStringAttribute(goNoSplitAttr, ""))
}
// Native Go gives //go:systemstack functions a distinct stack-growth
// prologue: it checks g.stackguard1 and calls runtime.morestackc. Carry the
// source pragma through AttrCFunc so target frame lowering cannot silently
// use the ordinary goroutine stack-growth protocol for runtime code.
if f.OwnAux.Fn.CFunc() {
FCtxt.LF.AddFunctionAttr(GlobalCtxt.CreateStringAttribute(goSystemStackAttr, ""))
}
FCtxt.LF.AddFunctionAttr(GlobalCtxt.CreateStringAttribute(llvmFramePointerAttr, llvmFramePointerNonLeaf))
if sig.HasClosureContext {
FCtxt.ClosureContext = FCtxt.LF.Param(sig.ClosureContextIndex)
Expand Down
66 changes: 60 additions & 6 deletions src/cmd/internal/testdir/llvm_abi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,29 @@ import (
type llvmABIDocument struct {
Members []struct {
GoObject *struct {
Symbols []llvmABISymbol `json:"symbols"`
Symbols []llvmABISymbol `json:"symbols"`
References []llvmABISymbol `json:"references"`
} `json:"go_object"`
} `json:"members"`
}

type llvmABISymbol struct {
type llvmABIReference struct {
PkgKind string `json:"pkg_kind"`
SymIndex uint32 `json:"sym_index"`
Name string `json:"name"`
ABI uint16 `json:"abi"`
}

type llvmABISymbol struct {
Class string `json:"class"`
ClassIndex uint32 `json:"class_index"`
Name string `json:"name"`
ABI uint16 `json:"abi"`
Flags uint64 `json:"flags"`
FlagNames []string `json:"flag_names"`
Relocations []struct {
Type string `json:"type"`
Target llvmABIReference `json:"target"`
} `json:"relocations"`
Function *struct {
Info *struct {
Args uint32 `json:"args"`
Expand Down Expand Up @@ -132,12 +147,14 @@ func runLLVMAArch64ABIDifferentialTest(t *testing.T, gorootTestDir string) {
[]byte("define goabiinternal"),
[]byte(`"go_results_tuple"`),
[]byte(`gc "goallc"`),
[]byte(`"go-stack-growth-statepoint"`),
} {
if !bytes.Contains(ir, needle) {
t.Fatalf("GoALLC IR does not contain %q", needle)
}
}
if bytes.Contains(ir, []byte(`"go-stack-growth-statepoint"`)) {
t.Fatal("GoALLC IR still contains the obsolete stack-growth attribute")
}

opt := llvmToolPath(t, "opt", "GOALLC_OPT")
runLLVMABICommand(t, ir, opt, "-passes=verify", "-disable-output")
Expand Down Expand Up @@ -248,7 +265,7 @@ func runLLVMAArch64ABIDifferentialTest(t *testing.T, gorootTestDir string) {
// homes in LocalsPointerMaps through locals-only alloca records.
goallcArgsMaps: [][]int{{2, 4, 18}, {2}},
nativeStackMaps: []int32{-1, 0, -1},
goallcStackMaps: []int32{-1, 1, -1},
goallcStackMaps: []int32{-1, 1, -1, 1},
goallcQueryMaps: [][]int{{2, 4, 18}, {2}, {2}, {2}, {2}},
},
{
Expand Down Expand Up @@ -347,7 +364,8 @@ func runLLVMAArch64ABIDifferentialTest(t *testing.T, gorootTestDir string) {
nativeArgsMaps: [][]int{{4}, nil},
goallcArgsMaps: [][]int{{4}, nil, nil},
nativeStackMaps: []int32{-1, 0, 1, -1},
goallcStackMaps: []int32{-1, 1, 2, -1, 2},
goallcStackMaps: []int32{-1, 1, -1, 2},
goallcQueryMaps: [][]int{{4}, nil, nil, nil},
},
}
for _, tc := range cases {
Expand Down Expand Up @@ -955,6 +973,42 @@ func findLLVMABISymbol(t *testing.T, document llvmABIDocument, name string) llvm
return llvmABISymbol{}
}

func llvmABIRelocationTargetName(document llvmABIDocument, target llvmABIReference) string {
if target.Name != "" || target.PkgKind != "none" {
return target.Name
}
for _, member := range document.Members {
if member.GoObject == nil {
continue
}
var nonpackageDefinitions uint32
for _, symbol := range member.GoObject.Symbols {
if symbol.Class == "nonpackage" && symbol.ClassIndex >= nonpackageDefinitions {
nonpackageDefinitions = symbol.ClassIndex + 1
}
}
if target.SymIndex < nonpackageDefinitions {
continue
}
classIndex := target.SymIndex - nonpackageDefinitions
for _, reference := range member.GoObject.References {
if reference.Class == "nonpackage_reference" && reference.ClassIndex == classIndex {
return reference.Name
}
}
}
return ""
}

func llvmABIHasRelocationTo(document llvmABIDocument, symbol llvmABISymbol, name string) bool {
for _, relocation := range symbol.Relocations {
if llvmABIRelocationTargetName(document, relocation.Target) == name {
return true
}
}
return false
}

func checkLLVMABISymbol(t *testing.T, backend string, symbol llvmABISymbol, tc llvmABICase) {
t.Helper()
if symbol.ABI != 1 {
Expand Down
88 changes: 88 additions & 0 deletions src/cmd/internal/testdir/llvm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import (
"os/exec"
"path"
"path/filepath"
"regexp"
"runtime"
"slices"
"sort"
"strings"
"sync"
Expand Down Expand Up @@ -189,6 +191,7 @@ func runLLVMInfrastructureTests(t *testing.T, common testCommon) {
runLLVMCallerStateTest(t, common.gorootTestDir)
})
t.Run("getg-abi0-fail-closed", runLLVMGetGABI0FailClosedTest)
t.Run("nosplit", runLLVMNoSplitTest)
t.Run("writebarrier-helpers", runLLVMWriteBarrierHelperTest)
t.Run("compile-only-regressions", func(t *testing.T) {
for _, name := range []string{"cmp.go", "typeparam/issue47684c.go"} {
Expand All @@ -200,6 +203,91 @@ func runLLVMInfrastructureTests(t *testing.T, common testCommon) {
t.Run("writebarrier-ir", runLLVMWriteBarrierIRTests)
}

func runLLVMNoSplitTest(t *testing.T) {
t.Helper()
dir := t.TempDir()
source := filepath.Join(dir, "nosplit.go")
program := `package p

func use(*[32]uintptr)

//go:nosplit
//go:noinline
func NoSplit(pointer *int) *int {
var words [32]uintptr
use(&words)
return pointer
}

//go:noinline
func Split(pointer *int) *int {
var words [32]uintptr
use(&words)
return pointer
}
`
if err := os.WriteFile(source, []byte(program), 0o666); err != nil {
t.Fatal(err)
}
archive := filepath.Join(dir, "nosplit.a")
runLLVMABICommand(t, nil, goTool, "tool", "compile",
"-p=p", "-enablellvm", "-llvmironly", "-o", archive, source)
ir, err := os.ReadFile(archive + ".ll")
if err != nil {
t.Fatal(err)
}
attributeLine := func(name string) []byte {
definition := regexp.MustCompile(`(?m)^define goabiinternal .*@` + regexp.QuoteMeta(name) + `\([^\n]*\) #([0-9]+)`).FindSubmatch(ir)
if definition == nil {
t.Fatalf("LLVM IR has no attributed definition for %s\n%s", name, ir)
}
pattern := regexp.MustCompile(`(?m)^attributes #` + string(definition[1]) + ` = \{.*$`)
line := pattern.Find(ir)
if line == nil {
t.Fatalf("LLVM IR has no attribute group for %s\n%s", name, ir)
}
return line
}
noSplitAttrs := attributeLine("p.NoSplit")
if !bytes.Contains(noSplitAttrs, []byte(`"go-nosplit"`)) ||
!bytes.Contains(noSplitAttrs, []byte(`noinline`)) {
t.Fatalf("LLVM nosplit attributes do not select the nosplit prologue policy: %s", noSplitAttrs)
}
splitAttrs := attributeLine("p.Split")
if bytes.Contains(splitAttrs, []byte(`"go-nosplit"`)) {
t.Fatalf("LLVM split attributes do not select the stack-growth prologue policy: %s", splitAttrs)
}
if bytes.Contains(ir, []byte(`"go-stack-growth-statepoint"`)) {
t.Fatal("LLVM IR still contains the obsolete stack-growth attribute")
}

llc := llvmToolPath(t, "llc", "GOALLC_LLC")
plugin := llvmABIPassPlugin(t, llc)
object := filepath.Join(dir, "nosplit.o")
runLLVMABICommand(t, nil, llc, "-load-pass-plugin="+plugin,
"-verify-machineinstrs", "-filetype=obj", "-o", object, archive+".ll")
document := readLLVMABIObject(t, object)
noSplit := findLLVMABISymbol(t, document, "p.NoSplit")
if !slices.Contains(noSplit.FlagNames, "nosplit") {
t.Fatalf("p.NoSplit GoObj flags %v do not contain nosplit", noSplit.FlagNames)
}
if llvmABIHasRelocationTo(document, noSplit, "runtime.morestack_noctxt") {
t.Fatal("p.NoSplit unexpectedly calls runtime.morestack_noctxt")
}
noSplitArgs := llvmABIArgsPointerBitmaps(t, noSplit)
if len(noSplitArgs) == 0 || !slices.Equal(noSplitArgs[0], []int{0}) {
t.Fatalf("p.NoSplit entry ArgsPointerMaps = %v, want pointer bit 0", noSplitArgs)
}
split := findLLVMABISymbol(t, document, "p.Split")
if !llvmABIHasRelocationTo(document, split, "runtime.morestack_noctxt") {
t.Fatal("p.Split has no runtime.morestack_noctxt relocation")
}
splitArgs := llvmABIArgsPointerBitmaps(t, split)
if len(splitArgs) == 0 || !slices.Equal(splitArgs[0], []int{0}) {
t.Fatalf("p.Split entry ArgsPointerMaps = %v, want pointer bit 0", splitArgs)
}
}

func runLLVMGetGABI0FailClosedTest(t *testing.T) {
t.Helper()
dir := t.TempDir()
Expand Down
15 changes: 13 additions & 2 deletions src/cmd/llvmplugin/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ add_definitions(${LLVM_DEFINITIONS})
include_directories(SYSTEM ${LLVM_INCLUDE_DIRS})

add_llvm_pass_plugin(GoALLCStatepoints
GoALLCStackMapPrinter.cpp
GoALLCInlineAnchors.cpp
GoALLCPreCodeGen.cpp
GoALLCStackMapPrinter.cpp
GoALLCStatepoints.cpp
GoALLCStatepointPlugin.cpp
)
Expand Down Expand Up @@ -252,7 +252,7 @@ if(BUILD_TESTING)
)
set_tests_properties(GoALLCStatepoints.FrontendMarkersPreserved PROPERTIES
PASS_REGULAR_EXPRESSION
"go-stack-growth-statepoint"
"gc \"goallc\""
)

foreach(GOALLC_DEBUG_TARGET IN ITEMS X86 AArch64)
Expand Down Expand Up @@ -505,6 +505,17 @@ if(BUILD_TESTING)
DEPENDS GoALLCStatepoints.DebugInline${GOALLC_DEBUG_TARGET}GoObj
)
endforeach()
goallc_add_filecheck_test(
GoALLCStatepoints.GoObjStackMaps
"${CMAKE_CURRENT_SOURCE_DIR}/testdata/statepoint.ll"
OBJVIEW-TEXT
"${GOALLC_OBJVIEW_EXECUTABLE}" -format=text
"${CMAKE_CURRENT_BINARY_DIR}/statepoint.goobj"
)
set_tests_properties(GoALLCStatepoints.GoObjStackMaps PROPERTIES
DEPENDS GoALLCStatepoints.GoObj
)

goallc_add_filecheck_test(
GoALLCStatepoints.DeferEdgeObjView
"${CMAKE_CURRENT_SOURCE_DIR}/testdata/defer-edge.ll"
Expand Down
Loading
Loading