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