Skip to content

Commit

Permalink
cmd/link: generate .xdata PE section
Browse files Browse the repository at this point in the history
This CL adds a .xdata section to the PE file generated by the Go linker.
It is also the first CL of the SEH chain that adds effective support
for unwinding the Go stack, as demonstrated by the newly added tests.

The .xdata section is a standard PE section that contains
an array of unwind data info structures. This structures are used to
record the effects a function has on the stack pointer,
and where the nonvolatile registers are saved on the stack [1].

Note that this CL still does not support unwinding the cgo stack.

Updates #57302

[1] https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64#struct-unwind_info

Change-Id: I6f305a51ed130b758ff9ca7b90c091e50a109a6f
Reviewed-on: https://go-review.googlesource.com/c/go/+/457455
Reviewed-by: Cherry Mui <cherryyz@google.com>
Reviewed-by: Davis Goodin <dagood@microsoft.com>
Run-TryBot: Quim Muntal <quimmuntal@gmail.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Than McIntosh <thanm@google.com>
  • Loading branch information
qmuntal committed May 2, 2023
1 parent 14cf82a commit 39ca989
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 19 deletions.
3 changes: 3 additions & 0 deletions src/cmd/link/internal/ld/asmb.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ func asmb(ctxt *Link) {
if Segpdata.Filelen > 0 {
writeParallel(&wg, pdatablk, ctxt, Segpdata.Fileoff, Segpdata.Vaddr, Segpdata.Filelen)
}
if Segxdata.Filelen > 0 {
writeParallel(&wg, xdatablk, ctxt, Segxdata.Fileoff, Segxdata.Vaddr, Segxdata.Filelen)
}

wg.Wait()
}
Expand Down
36 changes: 33 additions & 3 deletions src/cmd/link/internal/ld/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,10 @@ func pdatablk(ctxt *Link, out *OutBuf, addr int64, size int64) {
writeBlocks(ctxt, out, ctxt.outSem, ctxt.loader, []loader.Sym{sehp.pdata}, addr, size, zeros[:])
}

func xdatablk(ctxt *Link, out *OutBuf, addr int64, size int64) {
writeBlocks(ctxt, out, ctxt.outSem, ctxt.loader, []loader.Sym{sehp.xdata}, addr, size, zeros[:])
}

var covCounterDataStartOff, covCounterDataLen uint64

var zeros [512]byte
Expand Down Expand Up @@ -1686,6 +1690,10 @@ func (ctxt *Link) dodata(symGroupType []sym.SymKind) {
sect.Extnum = n
n++
}
for _, sect := range Segxdata.Sections {
sect.Extnum = n
n++
}
}

// allocateDataSectionForSym creates a new sym.Section into which a
Expand Down Expand Up @@ -2164,7 +2172,12 @@ func (state *dodataState) allocateSEHSections(ctxt *Link) {
if sehp.pdata > 0 {
sect := state.allocateDataSectionForSym(&Segpdata, sehp.pdata, 04)
state.assignDsymsToSection(sect, []loader.Sym{sehp.pdata}, sym.SRODATA, aligndatsize)
state.checkdatsize(sym.SPDATASECT)
state.checkdatsize(sym.SSEHSECT)
}
if sehp.xdata > 0 {
sect := state.allocateNamedDataSection(&Segxdata, ".xdata", []sym.SymKind{}, 04)
state.assignDsymsToSection(sect, []loader.Sym{sehp.xdata}, sym.SRODATA, aligndatsize)
state.checkdatsize(sym.SSEHSECT)
}
}

Expand Down Expand Up @@ -2719,6 +2732,21 @@ func (ctxt *Link) address() []*sym.Segment {
Segpdata.Length = va - Segpdata.Vaddr
}

if len(Segxdata.Sections) > 0 {
va = uint64(Rnd(int64(va), int64(*FlagRound)))
order = append(order, &Segxdata)
Segxdata.Rwx = 04
Segxdata.Vaddr = va
// Segxdata.Sections is intended to contain just one section.
// Loop through the slice anyway for consistency.
for _, s := range Segxdata.Sections {
va = uint64(Rnd(int64(va), int64(s.Align)))
s.Vaddr = va
va += s.Length
}
Segxdata.Length = va - Segxdata.Vaddr
}

va = uint64(Rnd(int64(va), int64(*FlagRound)))
order = append(order, &Segdwarf)
Segdwarf.Rwx = 06
Expand Down Expand Up @@ -2770,8 +2798,10 @@ func (ctxt *Link) address() []*sym.Segment {
}
}

if sect := ldr.SymSect(sehp.pdata); sect != nil {
ldr.AddToSymValue(sehp.pdata, int64(sect.Vaddr))
for _, s := range []loader.Sym{sehp.pdata, sehp.xdata} {
if sect := ldr.SymSect(s); sect != nil {
ldr.AddToSymValue(s, int64(sect.Vaddr))
}
}

if ctxt.BuildMode == BuildModeShared {
Expand Down
3 changes: 2 additions & 1 deletion src/cmd/link/internal/ld/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,9 @@ var (
Segdata sym.Segment
Segdwarf sym.Segment
Segpdata sym.Segment // windows-only
Segxdata sym.Segment // windows-only

Segments = []*sym.Segment{&Segtext, &Segrodata, &Segrelrodata, &Segdata, &Segdwarf, &Segpdata}
Segments = []*sym.Segment{&Segtext, &Segrodata, &Segrelrodata, &Segdata, &Segdwarf, &Segpdata, &Segxdata}
)

const pkgdef = "__.PKGDEF"
Expand Down
23 changes: 19 additions & 4 deletions src/cmd/link/internal/ld/pe.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,7 @@ type peFile struct {
bssSect *peSection
ctorsSect *peSection
pdataSect *peSection
xdataSect *peSection
nextSectOffset uint32
nextFileOffset uint32
symtabOffset int64 // offset to the start of symbol table
Expand Down Expand Up @@ -501,6 +502,8 @@ func (f *peFile) addDWARF() {

// addSEH adds SEH information to the COFF file f.
func (f *peFile) addSEH(ctxt *Link) {
// .pdata section can exist without the .xdata section.
// .xdata section depends on the .pdata section.
if Segpdata.Length == 0 {
return
}
Expand All @@ -512,10 +515,19 @@ func (f *peFile) addSEH(ctxt *Link) {
}
pefile.pdataSect = d
d.checkSegment(&Segpdata)
// TODO: remove extraSize once the dummy unwind info is removed from the .pdata section.
const extraSize = 12
pefile.dataDirectory[pe.IMAGE_DIRECTORY_ENTRY_EXCEPTION].VirtualAddress = d.virtualAddress + extraSize
pefile.dataDirectory[pe.IMAGE_DIRECTORY_ENTRY_EXCEPTION].Size = d.virtualSize - extraSize
pefile.dataDirectory[pe.IMAGE_DIRECTORY_ENTRY_EXCEPTION].VirtualAddress = d.virtualAddress
pefile.dataDirectory[pe.IMAGE_DIRECTORY_ENTRY_EXCEPTION].Size = d.virtualSize

if Segxdata.Length > 0 {
d = pefile.addSection(".xdata", int(Segxdata.Length), int(Segxdata.Length))
d.characteristics = IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ
if ctxt.LinkMode == LinkExternal {
// Some gcc versions don't honor the default alignment for the .xdata section.
d.characteristics |= IMAGE_SCN_ALIGN_4BYTES
}
pefile.xdataSect = d
d.checkSegment(&Segxdata)
}
}

// addInitArray adds .ctors COFF section to the file f.
Expand Down Expand Up @@ -626,6 +638,9 @@ func (f *peFile) emitRelocations(ctxt *Link) {
if sehp.pdata != 0 {
sects = append(sects, relsect{f.pdataSect, &Segpdata, []loader.Sym{sehp.pdata}})
}
if sehp.xdata != 0 {
sects = append(sects, relsect{f.xdataSect, &Segxdata, []loader.Sym{sehp.xdata}})
}
for _, s := range sects {
s.peSect.emitRelocations(ctxt.Out, func() int {
var n int
Expand Down
26 changes: 19 additions & 7 deletions src/cmd/link/internal/ld/seh.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

var sehp struct {
pdata loader.Sym
xdata loader.Sym
}

func writeSEH(ctxt *Link) {
Expand All @@ -29,11 +30,15 @@ func writeSEHAMD64(ctxt *Link) {
s.SetAlign(4)
return s
}
pdata := mkSecSym(".pdata", sym.SPDATASECT)
// TODO: the following 12 bytes represent a dummy unwind info,
// remove once unwind infos are encoded in the .xdata section.
pdata.AddUint64(ctxt.Arch, 0)
pdata.AddUint32(ctxt.Arch, 0)
pdata := mkSecSym(".pdata", sym.SSEHSECT)
xdata := mkSecSym(".xdata", sym.SSEHSECT)
// The .xdata entries have very low cardinality
// as it only contains frame pointer operations,
// which are very similar across functions.
// These are referenced by .pdata entries using
// an RVA, so it is possible, and binary-size wise,
// to deduplicate .xdata entries.
uwcache := make(map[string]int64) // aux symbol name --> .xdata offset
for _, s := range ctxt.Textp {
if fi := ldr.FuncInfo(s); !fi.Valid() || fi.TopFrame() {
continue
Expand All @@ -42,13 +47,20 @@ func writeSEHAMD64(ctxt *Link) {
if uw == 0 {
continue
}
name := ctxt.SymName(uw)
off, cached := uwcache[name]
if !cached {
off = xdata.Size()
uwcache[name] = off
xdata.AddBytes(ldr.Data(uw))
}

// Reference:
// https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64#struct-runtime_function
pdata.AddPEImageRelativeAddrPlus(ctxt.Arch, s, 0)
pdata.AddPEImageRelativeAddrPlus(ctxt.Arch, s, ldr.SymSize(s))
// TODO: reference the .xdata symbol.
pdata.AddPEImageRelativeAddrPlus(ctxt.Arch, pdata.Sym(), 0)
pdata.AddPEImageRelativeAddrPlus(ctxt.Arch, xdata.Sym(), off)
}
sehp.pdata = pdata.Sym()
sehp.xdata = xdata.Sym()
}
2 changes: 1 addition & 1 deletion src/cmd/link/internal/sym/symkind.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ const (

// SEH symbol types
SSEHUNWINDINFO
SPDATASECT
SSEHSECT
)

// AbiSymKindToSymKind maps values read from object files (which are
Expand Down
6 changes: 3 additions & 3 deletions src/cmd/link/internal/sym/symkind_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/internal/syscall/windows/syscall_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,4 @@ type FILE_ID_BOTH_DIR_INFO struct {
//sys GetVolumeInformationByHandle(file syscall.Handle, volumeNameBuffer *uint16, volumeNameSize uint32, volumeNameSerialNumber *uint32, maximumComponentLength *uint32, fileSystemFlags *uint32, fileSystemNameBuffer *uint16, fileSystemNameSize uint32) (err error) = GetVolumeInformationByHandleW

//sys RtlLookupFunctionEntry(pc uintptr, baseAddress *uintptr, table *byte) (ret uintptr) = kernel32.RtlLookupFunctionEntry
//sys RtlVirtualUnwind(handlerType uint32, baseAddress uintptr, pc uintptr, entry uintptr, ctxt uintptr, data *uintptr, frame *uintptr, ctxptrs *byte) (ret uintptr) = kernel32.RtlVirtualUnwind
7 changes: 7 additions & 0 deletions src/internal/syscall/windows/zsyscall_windows.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

128 changes: 128 additions & 0 deletions src/runtime/runtime-seh_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
"internal/abi"
"internal/syscall/windows"
"runtime"
"slices"
"testing"
"unsafe"
)

func sehf1() int {
Expand Down Expand Up @@ -61,3 +63,129 @@ func TestSehLookupFunctionEntry(t *testing.T) {
}
}
}

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
}
windows.RtlVirtualUnwind(0, base, ctx.GetPC(), fn, uintptr(unsafe.Pointer(&ctx)), nil, &frame, nil)
n++
pcs[i] = ctx.GetPC()
}
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")
}
}

0 comments on commit 39ca989

Please sign in to comment.