Skip to content

Commit 39ca989

Browse files
committed
cmd/link: generate .xdata PE section
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 <[email protected]> Reviewed-by: Davis Goodin <[email protected]> Run-TryBot: Quim Muntal <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Than McIntosh <[email protected]>
1 parent 14cf82a commit 39ca989

File tree

10 files changed

+216
-19
lines changed

10 files changed

+216
-19
lines changed

src/cmd/link/internal/ld/asmb.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ func asmb(ctxt *Link) {
6363
if Segpdata.Filelen > 0 {
6464
writeParallel(&wg, pdatablk, ctxt, Segpdata.Fileoff, Segpdata.Vaddr, Segpdata.Filelen)
6565
}
66+
if Segxdata.Filelen > 0 {
67+
writeParallel(&wg, xdatablk, ctxt, Segxdata.Fileoff, Segxdata.Vaddr, Segxdata.Filelen)
68+
}
6669

6770
wg.Wait()
6871
}

src/cmd/link/internal/ld/data.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,6 +1158,10 @@ func pdatablk(ctxt *Link, out *OutBuf, addr int64, size int64) {
11581158
writeBlocks(ctxt, out, ctxt.outSem, ctxt.loader, []loader.Sym{sehp.pdata}, addr, size, zeros[:])
11591159
}
11601160

1161+
func xdatablk(ctxt *Link, out *OutBuf, addr int64, size int64) {
1162+
writeBlocks(ctxt, out, ctxt.outSem, ctxt.loader, []loader.Sym{sehp.xdata}, addr, size, zeros[:])
1163+
}
1164+
11611165
var covCounterDataStartOff, covCounterDataLen uint64
11621166

11631167
var zeros [512]byte
@@ -1686,6 +1690,10 @@ func (ctxt *Link) dodata(symGroupType []sym.SymKind) {
16861690
sect.Extnum = n
16871691
n++
16881692
}
1693+
for _, sect := range Segxdata.Sections {
1694+
sect.Extnum = n
1695+
n++
1696+
}
16891697
}
16901698

16911699
// allocateDataSectionForSym creates a new sym.Section into which a
@@ -2164,7 +2172,12 @@ func (state *dodataState) allocateSEHSections(ctxt *Link) {
21642172
if sehp.pdata > 0 {
21652173
sect := state.allocateDataSectionForSym(&Segpdata, sehp.pdata, 04)
21662174
state.assignDsymsToSection(sect, []loader.Sym{sehp.pdata}, sym.SRODATA, aligndatsize)
2167-
state.checkdatsize(sym.SPDATASECT)
2175+
state.checkdatsize(sym.SSEHSECT)
2176+
}
2177+
if sehp.xdata > 0 {
2178+
sect := state.allocateNamedDataSection(&Segxdata, ".xdata", []sym.SymKind{}, 04)
2179+
state.assignDsymsToSection(sect, []loader.Sym{sehp.xdata}, sym.SRODATA, aligndatsize)
2180+
state.checkdatsize(sym.SSEHSECT)
21682181
}
21692182
}
21702183

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

2735+
if len(Segxdata.Sections) > 0 {
2736+
va = uint64(Rnd(int64(va), int64(*FlagRound)))
2737+
order = append(order, &Segxdata)
2738+
Segxdata.Rwx = 04
2739+
Segxdata.Vaddr = va
2740+
// Segxdata.Sections is intended to contain just one section.
2741+
// Loop through the slice anyway for consistency.
2742+
for _, s := range Segxdata.Sections {
2743+
va = uint64(Rnd(int64(va), int64(s.Align)))
2744+
s.Vaddr = va
2745+
va += s.Length
2746+
}
2747+
Segxdata.Length = va - Segxdata.Vaddr
2748+
}
2749+
27222750
va = uint64(Rnd(int64(va), int64(*FlagRound)))
27232751
order = append(order, &Segdwarf)
27242752
Segdwarf.Rwx = 06
@@ -2770,8 +2798,10 @@ func (ctxt *Link) address() []*sym.Segment {
27702798
}
27712799
}
27722800

2773-
if sect := ldr.SymSect(sehp.pdata); sect != nil {
2774-
ldr.AddToSymValue(sehp.pdata, int64(sect.Vaddr))
2801+
for _, s := range []loader.Sym{sehp.pdata, sehp.xdata} {
2802+
if sect := ldr.SymSect(s); sect != nil {
2803+
ldr.AddToSymValue(s, int64(sect.Vaddr))
2804+
}
27752805
}
27762806

27772807
if ctxt.BuildMode == BuildModeShared {

src/cmd/link/internal/ld/lib.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,8 +329,9 @@ var (
329329
Segdata sym.Segment
330330
Segdwarf sym.Segment
331331
Segpdata sym.Segment // windows-only
332+
Segxdata sym.Segment // windows-only
332333

333-
Segments = []*sym.Segment{&Segtext, &Segrodata, &Segrelrodata, &Segdata, &Segdwarf, &Segpdata}
334+
Segments = []*sym.Segment{&Segtext, &Segrodata, &Segrelrodata, &Segdata, &Segdwarf, &Segpdata, &Segxdata}
334335
)
335336

336337
const pkgdef = "__.PKGDEF"

src/cmd/link/internal/ld/pe.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,7 @@ type peFile struct {
434434
bssSect *peSection
435435
ctorsSect *peSection
436436
pdataSect *peSection
437+
xdataSect *peSection
437438
nextSectOffset uint32
438439
nextFileOffset uint32
439440
symtabOffset int64 // offset to the start of symbol table
@@ -501,6 +502,8 @@ func (f *peFile) addDWARF() {
501502

502503
// addSEH adds SEH information to the COFF file f.
503504
func (f *peFile) addSEH(ctxt *Link) {
505+
// .pdata section can exist without the .xdata section.
506+
// .xdata section depends on the .pdata section.
504507
if Segpdata.Length == 0 {
505508
return
506509
}
@@ -512,10 +515,19 @@ func (f *peFile) addSEH(ctxt *Link) {
512515
}
513516
pefile.pdataSect = d
514517
d.checkSegment(&Segpdata)
515-
// TODO: remove extraSize once the dummy unwind info is removed from the .pdata section.
516-
const extraSize = 12
517-
pefile.dataDirectory[pe.IMAGE_DIRECTORY_ENTRY_EXCEPTION].VirtualAddress = d.virtualAddress + extraSize
518-
pefile.dataDirectory[pe.IMAGE_DIRECTORY_ENTRY_EXCEPTION].Size = d.virtualSize - extraSize
518+
pefile.dataDirectory[pe.IMAGE_DIRECTORY_ENTRY_EXCEPTION].VirtualAddress = d.virtualAddress
519+
pefile.dataDirectory[pe.IMAGE_DIRECTORY_ENTRY_EXCEPTION].Size = d.virtualSize
520+
521+
if Segxdata.Length > 0 {
522+
d = pefile.addSection(".xdata", int(Segxdata.Length), int(Segxdata.Length))
523+
d.characteristics = IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ
524+
if ctxt.LinkMode == LinkExternal {
525+
// Some gcc versions don't honor the default alignment for the .xdata section.
526+
d.characteristics |= IMAGE_SCN_ALIGN_4BYTES
527+
}
528+
pefile.xdataSect = d
529+
d.checkSegment(&Segxdata)
530+
}
519531
}
520532

521533
// addInitArray adds .ctors COFF section to the file f.
@@ -626,6 +638,9 @@ func (f *peFile) emitRelocations(ctxt *Link) {
626638
if sehp.pdata != 0 {
627639
sects = append(sects, relsect{f.pdataSect, &Segpdata, []loader.Sym{sehp.pdata}})
628640
}
641+
if sehp.xdata != 0 {
642+
sects = append(sects, relsect{f.xdataSect, &Segxdata, []loader.Sym{sehp.xdata}})
643+
}
629644
for _, s := range sects {
630645
s.peSect.emitRelocations(ctxt.Out, func() int {
631646
var n int

src/cmd/link/internal/ld/seh.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
var sehp struct {
1414
pdata loader.Sym
15+
xdata loader.Sym
1516
}
1617

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

4658
// Reference:
4759
// https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64#struct-runtime_function
4860
pdata.AddPEImageRelativeAddrPlus(ctxt.Arch, s, 0)
4961
pdata.AddPEImageRelativeAddrPlus(ctxt.Arch, s, ldr.SymSize(s))
50-
// TODO: reference the .xdata symbol.
51-
pdata.AddPEImageRelativeAddrPlus(ctxt.Arch, pdata.Sym(), 0)
62+
pdata.AddPEImageRelativeAddrPlus(ctxt.Arch, xdata.Sym(), off)
5263
}
5364
sehp.pdata = pdata.Sym()
65+
sehp.xdata = xdata.Sym()
5466
}

src/cmd/link/internal/sym/symkind.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ const (
126126

127127
// SEH symbol types
128128
SSEHUNWINDINFO
129-
SPDATASECT
129+
SSEHSECT
130130
)
131131

132132
// AbiSymKindToSymKind maps values read from object files (which are

src/cmd/link/internal/sym/symkind_string.go

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/internal/syscall/windows/syscall_windows.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,3 +401,4 @@ type FILE_ID_BOTH_DIR_INFO struct {
401401
//sys GetVolumeInformationByHandle(file syscall.Handle, volumeNameBuffer *uint16, volumeNameSize uint32, volumeNameSerialNumber *uint32, maximumComponentLength *uint32, fileSystemFlags *uint32, fileSystemNameBuffer *uint16, fileSystemNameSize uint32) (err error) = GetVolumeInformationByHandleW
402402

403403
//sys RtlLookupFunctionEntry(pc uintptr, baseAddress *uintptr, table *byte) (ret uintptr) = kernel32.RtlLookupFunctionEntry
404+
//sys RtlVirtualUnwind(handlerType uint32, baseAddress uintptr, pc uintptr, entry uintptr, ctxt uintptr, data *uintptr, frame *uintptr, ctxptrs *byte) (ret uintptr) = kernel32.RtlVirtualUnwind

src/internal/syscall/windows/zsyscall_windows.go

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/runtime/runtime-seh_windows_test.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88
"internal/abi"
99
"internal/syscall/windows"
1010
"runtime"
11+
"slices"
1112
"testing"
13+
"unsafe"
1214
)
1315

1416
func sehf1() int {
@@ -61,3 +63,129 @@ func TestSehLookupFunctionEntry(t *testing.T) {
6163
}
6264
}
6365
}
66+
67+
func sehCallers() []uintptr {
68+
// We don't need a real context,
69+
// RtlVirtualUnwind just needs a context with
70+
// valid a pc, sp and fp (aka bp).
71+
ctx := runtime.NewContextStub()
72+
73+
pcs := make([]uintptr, 15)
74+
var base, frame uintptr
75+
var n int
76+
for i := 0; i < len(pcs); i++ {
77+
fn := windows.RtlLookupFunctionEntry(ctx.GetPC(), &base, nil)
78+
if fn == 0 {
79+
break
80+
}
81+
windows.RtlVirtualUnwind(0, base, ctx.GetPC(), fn, uintptr(unsafe.Pointer(&ctx)), nil, &frame, nil)
82+
n++
83+
pcs[i] = ctx.GetPC()
84+
}
85+
return pcs[:n]
86+
}
87+
88+
// SEH unwinding does not report inlined frames.
89+
//
90+
//go:noinline
91+
func sehf3(pan bool) []uintptr {
92+
return sehf4(pan)
93+
}
94+
95+
//go:noinline
96+
func sehf4(pan bool) []uintptr {
97+
var pcs []uintptr
98+
if pan {
99+
panic("sehf4")
100+
}
101+
pcs = sehCallers()
102+
return pcs
103+
}
104+
105+
func testSehCallersEqual(t *testing.T, pcs []uintptr, want []string) {
106+
t.Helper()
107+
got := make([]string, 0, len(want))
108+
for _, pc := range pcs {
109+
fn := runtime.FuncForPC(pc)
110+
if fn == nil || len(got) >= len(want) {
111+
break
112+
}
113+
name := fn.Name()
114+
switch name {
115+
case "runtime.deferCallSave", "runtime.runOpenDeferFrame", "runtime.panicmem":
116+
// These functions are skipped as they appear inconsistently depending
117+
// whether inlining is on or off.
118+
continue
119+
}
120+
got = append(got, name)
121+
}
122+
if !slices.Equal(want, got) {
123+
t.Fatalf("wanted %v, got %v", want, got)
124+
}
125+
}
126+
127+
func TestSehUnwind(t *testing.T) {
128+
if runtime.GOARCH != "amd64" {
129+
t.Skip("skipping amd64-only test")
130+
}
131+
pcs := sehf3(false)
132+
testSehCallersEqual(t, pcs, []string{"runtime_test.sehCallers", "runtime_test.sehf4",
133+
"runtime_test.sehf3", "runtime_test.TestSehUnwind"})
134+
}
135+
136+
func TestSehUnwindPanic(t *testing.T) {
137+
if runtime.GOARCH != "amd64" {
138+
t.Skip("skipping amd64-only test")
139+
}
140+
want := []string{"runtime_test.sehCallers", "runtime_test.TestSehUnwindPanic.func1", "runtime.gopanic",
141+
"runtime_test.sehf4", "runtime_test.sehf3", "runtime_test.TestSehUnwindPanic"}
142+
defer func() {
143+
if r := recover(); r == nil {
144+
t.Fatal("did not panic")
145+
}
146+
pcs := sehCallers()
147+
testSehCallersEqual(t, pcs, want)
148+
}()
149+
sehf3(true)
150+
}
151+
152+
func TestSehUnwindDoublePanic(t *testing.T) {
153+
if runtime.GOARCH != "amd64" {
154+
t.Skip("skipping amd64-only test")
155+
}
156+
want := []string{"runtime_test.sehCallers", "runtime_test.TestSehUnwindDoublePanic.func1.1", "runtime.gopanic",
157+
"runtime_test.TestSehUnwindDoublePanic.func1", "runtime.gopanic", "runtime_test.TestSehUnwindDoublePanic"}
158+
defer func() {
159+
defer func() {
160+
if recover() == nil {
161+
t.Fatal("did not panic")
162+
}
163+
pcs := sehCallers()
164+
testSehCallersEqual(t, pcs, want)
165+
}()
166+
if recover() == nil {
167+
t.Fatal("did not panic")
168+
}
169+
panic(2)
170+
}()
171+
panic(1)
172+
}
173+
174+
func TestSehUnwindNilPointerPanic(t *testing.T) {
175+
if runtime.GOARCH != "amd64" {
176+
t.Skip("skipping amd64-only test")
177+
}
178+
want := []string{"runtime_test.sehCallers", "runtime_test.TestSehUnwindNilPointerPanic.func1", "runtime.gopanic",
179+
"runtime.sigpanic", "runtime_test.TestSehUnwindNilPointerPanic"}
180+
defer func() {
181+
if r := recover(); r == nil {
182+
t.Fatal("did not panic")
183+
}
184+
pcs := sehCallers()
185+
testSehCallersEqual(t, pcs, want)
186+
}()
187+
var p *int
188+
if *p == 3 {
189+
t.Fatal("did not see nil pointer panic")
190+
}
191+
}

0 commit comments

Comments
 (0)