// 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.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") } }