blob: 27e4f4974113afc609c7cb09a970eab4886b3868 [file] [log] [blame]
// 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/abi"
"internal/syscall/windows"
"runtime"
"slices"
"testing"
"unsafe"
)
func sehf1() int {
return sehf1()
}
func sehf2() {}
func TestSehLookupFunctionEntry(t *testing.T) {
if runtime.GOARCH != "amd64" {
t.Skip("skipping amd64-only test")
}
// This test checks that Win32 is able to retrieve
// function metadata stored in the .pdata section
// by the Go linker.
// Win32 unwinding will fail if this test fails,
// as RtlUnwindEx uses RtlLookupFunctionEntry internally.
// If that's the case, don't bother investigating further,
// first fix the .pdata generation.
sehf1pc := abi.FuncPCABIInternal(sehf1)
var fnwithframe func()
fnwithframe = func() {
fnwithframe()
}
fnwithoutframe := func() {}
tests := []struct {
name string
pc uintptr
hasframe bool
}{
{"no frame func", abi.FuncPCABIInternal(sehf2), false},
{"no func", sehf1pc - 1, false},
{"func at entry", sehf1pc, true},
{"func in prologue", sehf1pc + 1, true},
{"anonymous func with frame", abi.FuncPCABIInternal(fnwithframe), true},
{"anonymous func without frame", abi.FuncPCABIInternal(fnwithoutframe), false},
{"pc at func body", runtime.NewContextStub().GetPC(), true},
}
for _, tt := range tests {
var base uintptr
fn := windows.RtlLookupFunctionEntry(tt.pc, &base, nil)
if !tt.hasframe {
if fn != 0 {
t.Errorf("%s: unexpected frame", tt.name)
}
continue
}
if fn == 0 {
t.Errorf("%s: missing frame", tt.name)
}
}
}
func sehCallers() []uintptr {
// We don't need a real context,
// RtlVirtualUnwind just needs a context with
// valid a pc, sp and fp (aka bp).
ctx := runtime.NewContextStub()
pcs := make([]uintptr, 15)
var base, frame uintptr
var n int
for i := 0; i < len(pcs); i++ {
fn := windows.RtlLookupFunctionEntry(ctx.GetPC(), &base, nil)
if fn == 0 {
break
}
pcs[i] = ctx.GetPC()
n++
windows.RtlVirtualUnwind(0, base, ctx.GetPC(), fn, uintptr(unsafe.Pointer(ctx)), nil, &frame, nil)
}
return pcs[:n]
}
// SEH unwinding does not report inlined frames.
//
//go:noinline
func sehf3(pan bool) []uintptr {
return sehf4(pan)
}
//go:noinline
func sehf4(pan bool) []uintptr {
var pcs []uintptr
if pan {
panic("sehf4")
}
pcs = sehCallers()
return pcs
}
func testSehCallersEqual(t *testing.T, pcs []uintptr, want []string) {
t.Helper()
got := make([]string, 0, len(want))
for _, pc := range pcs {
fn := runtime.FuncForPC(pc)
if fn == nil || len(got) >= len(want) {
break
}
name := fn.Name()
switch name {
case "runtime.deferCallSave", "runtime.runOpenDeferFrame", "runtime.panicmem":
// These functions are skipped as they appear inconsistently depending
// whether inlining is on or off.
continue
}
got = append(got, name)
}
if !slices.Equal(want, got) {
t.Fatalf("wanted %v, got %v", want, got)
}
}
func TestSehUnwind(t *testing.T) {
if runtime.GOARCH != "amd64" {
t.Skip("skipping amd64-only test")
}
pcs := sehf3(false)
testSehCallersEqual(t, pcs, []string{"runtime_test.sehCallers", "runtime_test.sehf4",
"runtime_test.sehf3", "runtime_test.TestSehUnwind"})
}
func TestSehUnwindPanic(t *testing.T) {
if runtime.GOARCH != "amd64" {
t.Skip("skipping amd64-only test")
}
want := []string{"runtime_test.sehCallers", "runtime_test.TestSehUnwindPanic.func1", "runtime.gopanic",
"runtime_test.sehf4", "runtime_test.sehf3", "runtime_test.TestSehUnwindPanic"}
defer func() {
if r := recover(); r == nil {
t.Fatal("did not panic")
}
pcs := sehCallers()
testSehCallersEqual(t, pcs, want)
}()
sehf3(true)
}
func TestSehUnwindDoublePanic(t *testing.T) {
if runtime.GOARCH != "amd64" {
t.Skip("skipping amd64-only test")
}
want := []string{"runtime_test.sehCallers", "runtime_test.TestSehUnwindDoublePanic.func1.1", "runtime.gopanic",
"runtime_test.TestSehUnwindDoublePanic.func1", "runtime.gopanic", "runtime_test.TestSehUnwindDoublePanic"}
defer func() {
defer func() {
if recover() == nil {
t.Fatal("did not panic")
}
pcs := sehCallers()
testSehCallersEqual(t, pcs, want)
}()
if recover() == nil {
t.Fatal("did not panic")
}
panic(2)
}()
panic(1)
}
func TestSehUnwindNilPointerPanic(t *testing.T) {
if runtime.GOARCH != "amd64" {
t.Skip("skipping amd64-only test")
}
want := []string{"runtime_test.sehCallers", "runtime_test.TestSehUnwindNilPointerPanic.func1", "runtime.gopanic",
"runtime.sigpanic", "runtime_test.TestSehUnwindNilPointerPanic"}
defer func() {
if r := recover(); r == nil {
t.Fatal("did not panic")
}
pcs := sehCallers()
testSehCallersEqual(t, pcs, want)
}()
var p *int
if *p == 3 {
t.Fatal("did not see nil pointer panic")
}
}