diff --git a/src/cmd/go/internal/test/test.go b/src/cmd/go/internal/test/test.go index 7df6f421d6..a3f407fdae 100644 --- a/src/cmd/go/internal/test/test.go +++ b/src/cmd/go/internal/test/test.go @@ -1026,6 +1026,11 @@ func builderTest(b *work.Builder, ctx context.Context, pkgOpts load.PackageOpts, pmain.Dir = testDir pmain.Internal.OmitDebug = !testC && !testNeedBinary() + if pmain.ImportPath == "runtime.test" { + // The runtime package needs a symbolized binary for its tests. + // See runtime/unsafepoint_test.go. + pmain.Internal.OmitDebug = false + } if !cfg.BuildN { // writeTestmain writes _testmain.go, diff --git a/src/runtime/export_test.go b/src/runtime/export_test.go index a4a1fa580d..bc08b2333c 100644 --- a/src/runtime/export_test.go +++ b/src/runtime/export_test.go @@ -7,6 +7,7 @@ package runtime import ( + "internal/abi" "internal/goarch" "internal/goos" "runtime/internal/atomic" @@ -1940,3 +1941,21 @@ func MyGenericFunc[T any]() { testUintptr = 4 }) } + +func UnsafePoint(pc uintptr) bool { + fi := findfunc(pc) + v := pcdatavalue(fi, abi.PCDATA_UnsafePoint, pc, nil) + switch v { + case abi.UnsafePointUnsafe: + return true + case abi.UnsafePointSafe: + return false + case abi.UnsafePointRestart1, abi.UnsafePointRestart2, abi.UnsafePointRestartAtEntry: + // These are all interruptible, they just encode a nonstandard + // way of recovering when interrupted. + return false + default: + var buf [20]byte + panic("invalid unsafe point code " + string(itoa(buf[:], uint64(v)))) + } +} diff --git a/src/runtime/unsafepoint_test.go b/src/runtime/unsafepoint_test.go new file mode 100644 index 0000000000..2c97adead8 --- /dev/null +++ b/src/runtime/unsafepoint_test.go @@ -0,0 +1,122 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package runtime_test + +import ( + "internal/testenv" + "os" + "os/exec" + "reflect" + "runtime" + "strconv" + "strings" + "testing" +) + +// This is the function we'll be testing. +// It has a simple write barrier in it. +func setGlobalPointer() { + globalPointer = nil +} + +var globalPointer *int + +func TestUnsafePoint(t *testing.T) { + testenv.MustHaveExec(t) + switch runtime.GOARCH { + case "amd64", "arm64": + default: + t.Skipf("test not enabled for %s", runtime.GOARCH) + } + + // Get a reference we can use to ask the runtime about + // which of its instructions are unsafe preemption points. + f := runtime.FuncForPC(reflect.ValueOf(setGlobalPointer).Pointer()) + + // Disassemble the test function. + // Note that normally "go test runtime" would strip symbols + // and prevent this step from working. So there's a hack in + // cmd/go/internal/test that exempts runtime tests from + // symbol stripping. + cmd := exec.Command(testenv.GoToolPath(t), "tool", "objdump", "-s", "setGlobalPointer", os.Args[0]) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("can't objdump %v", err) + } + lines := strings.Split(string(out), "\n")[1:] + + // Walk through assembly instructions, checking preemptible flags. + var entry uint64 + var startedWB bool + var doneWB bool + instructionCount := 0 + unsafeCount := 0 + for _, line := range lines { + line = strings.TrimSpace(line) + t.Logf("%s", line) + parts := strings.Fields(line) + if len(parts) < 4 { + continue + } + if !strings.HasPrefix(parts[0], "unsafepoint_test.go:") { + continue + } + pc, err := strconv.ParseUint(parts[1][2:], 16, 64) + if err != nil { + t.Fatalf("can't parse pc %s: %v", parts[1], err) + } + if entry == 0 { + entry = pc + } + // Note that some platforms do ASLR, so the PCs in the disassembly + // don't match PCs in the address space. Only offsets from function + // entry make sense. + unsafe := runtime.UnsafePoint(f.Entry() + uintptr(pc-entry)) + t.Logf("unsafe: %v\n", unsafe) + instructionCount++ + if unsafe { + unsafeCount++ + } + + // All the instructions inside the write barrier must be unpreemptible. + if startedWB && !doneWB && !unsafe { + t.Errorf("instruction %s must be marked unsafe, but isn't", parts[1]) + } + + // Detect whether we're in the write barrier. + switch runtime.GOARCH { + case "arm64": + if parts[3] == "MOVWU" { + // The unpreemptible region starts after the + // load of runtime.writeBarrier. + startedWB = true + } + if parts[3] == "MOVD" && parts[4] == "ZR," { + // The unpreemptible region ends after the + // write of nil. + doneWB = true + } + case "amd64": + if parts[3] == "CMPL" { + startedWB = true + } + if parts[3] == "MOVQ" && parts[4] == "$0x0," { + doneWB = true + } + } + } + + if instructionCount == 0 { + t.Errorf("no instructions") + } + if unsafeCount == instructionCount { + t.Errorf("no interruptible instructions") + } + // Note that there are other instructions marked unpreemptible besides + // just the ones required by the write barrier. Those include possibly + // the preamble and postamble, as well as bleeding out from the + // write barrier proper into adjacent instructions (in both directions). + // Hopefully we can clean up the latter at some point. +}