Skip to content

Commit eaf2345

Browse files
committed
cmd/link: use a .def file to mark exported symbols on Windows
Binutils defaults to exporting all symbols when building a Windows DLL. To avoid that we were marking symbols with __declspec(dllexport) in the cgo-generated headers, which instructs ld to export only those symbols. However, that approach makes the headers hard to reuse when importing the resulting DLL into other projects, as imported symbols should be marked with __declspec(dllimport). A better approach is to generate a .def file listing the symbols to export, which gets the same effect without having to modify the headers. Updates golang#30674 Fixes golang#56994 Change-Id: I22bd0aa079e2be4ae43b13d893f6b804eaeddabf Reviewed-on: https://go-review.googlesource.com/c/go/+/705776 Reviewed-by: Michael Knyszek <[email protected]> Reviewed-by: Junyang Shao <[email protected]> Reviewed-by: Than McIntosh <[email protected]> Reviewed-by: Cherry Mui <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 4b77733 commit eaf2345

File tree

8 files changed

+118
-65
lines changed

8 files changed

+118
-65
lines changed

src/cmd/cgo/internal/testcshared/cshared_test.go

Lines changed: 31 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"bufio"
99
"bytes"
1010
"cmd/cgo/internal/cgotest"
11+
"cmp"
1112
"debug/elf"
1213
"debug/pe"
1314
"encoding/binary"
@@ -272,7 +273,7 @@ func createHeaders() error {
272273
// which results in the linkers output implib getting overwritten at each step. So instead build the
273274
// import library the traditional way, using a def file.
274275
err = os.WriteFile("libgo.def",
275-
[]byte("LIBRARY libgo.dll\nEXPORTS\n\tDidInitRun\n\tDidMainRun\n\tDivu\n\tFromPkg\n\t_cgo_dummy_export\n"),
276+
[]byte("LIBRARY libgo.dll\nEXPORTS\n\tDidInitRun\n\tDidMainRun\n\tDivu\n\tFromPkg\n"),
276277
0644)
277278
if err != nil {
278279
return fmt.Errorf("unable to write def file: %v", err)
@@ -375,9 +376,23 @@ func TestExportedSymbols(t *testing.T) {
375376
}
376377
}
377378

378-
func checkNumberOfExportedFunctionsWindows(t *testing.T, prog string, exportedFunctions int, wantAll bool) {
379+
func checkNumberOfExportedSymbolsWindows(t *testing.T, exportedSymbols int, wantAll bool) {
380+
t.Parallel()
379381
tmpdir := t.TempDir()
380382

383+
prog := `
384+
package main
385+
import "C"
386+
func main() {}
387+
`
388+
389+
for i := range exportedSymbols {
390+
prog += fmt.Sprintf(`
391+
//export GoFunc%d
392+
func GoFunc%d() {}
393+
`, i, i)
394+
}
395+
381396
srcfile := filepath.Join(tmpdir, "test.go")
382397
objfile := filepath.Join(tmpdir, "test.dll")
383398
if err := os.WriteFile(srcfile, []byte(prog), 0666); err != nil {
@@ -443,18 +458,19 @@ func checkNumberOfExportedFunctionsWindows(t *testing.T, prog string, exportedFu
443458
t.Fatalf("binary.Read failed: %v", err)
444459
}
445460

446-
// Only the two exported functions and _cgo_dummy_export should be exported.
461+
exportedSymbols = cmp.Or(exportedSymbols, 1) // _cgo_stub_export is exported if there are no other symbols exported
462+
447463
// NumberOfNames is the number of functions exported with a unique name.
448464
// NumberOfFunctions can be higher than that because it also counts
449465
// functions exported only by ordinal, a unique number asigned by the linker,
450466
// and linkers might add an unknown number of their own ordinal-only functions.
451467
if wantAll {
452-
if e.NumberOfNames <= uint32(exportedFunctions) {
453-
t.Errorf("got %d exported names, want > %d", e.NumberOfNames, exportedFunctions)
468+
if e.NumberOfNames <= uint32(exportedSymbols) {
469+
t.Errorf("got %d exported names, want > %d", e.NumberOfNames, exportedSymbols)
454470
}
455471
} else {
456-
if e.NumberOfNames > uint32(exportedFunctions) {
457-
t.Errorf("got %d exported names, want <= %d", e.NumberOfNames, exportedFunctions)
472+
if e.NumberOfNames != uint32(exportedSymbols) {
473+
t.Errorf("got %d exported names, want %d", e.NumberOfNames, exportedSymbols)
458474
}
459475
}
460476
}
@@ -470,43 +486,14 @@ func TestNumberOfExportedFunctions(t *testing.T) {
470486

471487
t.Parallel()
472488

473-
const prog0 = `
474-
package main
475-
476-
import "C"
477-
478-
func main() {
479-
}
480-
`
481-
482-
const prog2 = `
483-
package main
484-
485-
import "C"
486-
487-
//export GoFunc
488-
func GoFunc() {
489-
println(42)
490-
}
491-
492-
//export GoFunc2
493-
func GoFunc2() {
494-
println(24)
495-
}
496-
497-
func main() {
498-
}
499-
`
500-
// All programs export _cgo_dummy_export, so add 1 to the expected counts.
501-
t.Run("OnlyExported/0", func(t *testing.T) {
502-
checkNumberOfExportedFunctionsWindows(t, prog0, 0+1, false)
503-
})
504-
t.Run("OnlyExported/2", func(t *testing.T) {
505-
checkNumberOfExportedFunctionsWindows(t, prog2, 2+1, false)
506-
})
507-
t.Run("All", func(t *testing.T) {
508-
checkNumberOfExportedFunctionsWindows(t, prog2, 2+1, true)
509-
})
489+
for i := range 3 {
490+
t.Run(fmt.Sprintf("OnlyExported/%d", i), func(t *testing.T) {
491+
checkNumberOfExportedSymbolsWindows(t, i, false)
492+
})
493+
t.Run(fmt.Sprintf("All/%d", i), func(t *testing.T) {
494+
checkNumberOfExportedSymbolsWindows(t, i, true)
495+
})
496+
}
510497
}
511498

512499
// test1: shared library can be dynamically loaded and exported symbols are accessible.

src/cmd/cgo/out.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,12 +1005,8 @@ func (p *Package) writeExports(fgo2, fm, fgcc, fgcch io.Writer) {
10051005
}
10061006

10071007
// Build the wrapper function compiled by gcc.
1008-
gccExport := ""
1009-
if goos == "windows" {
1010-
gccExport = "__declspec(dllexport) "
1011-
}
10121008
var s strings.Builder
1013-
fmt.Fprintf(&s, "%s%s %s(", gccExport, gccResult, exp.ExpName)
1009+
fmt.Fprintf(&s, "%s %s(", gccResult, exp.ExpName)
10141010
if fn.Recv != nil {
10151011
s.WriteString(p.cgoType(fn.Recv.List[0].Type).C.String())
10161012
s.WriteString(" recv")

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1772,7 +1772,8 @@ func (ctxt *Link) hostlink() {
17721772
}
17731773

17741774
// Force global symbols to be exported for dlopen, etc.
1775-
if ctxt.IsELF {
1775+
switch {
1776+
case ctxt.IsELF:
17761777
if ctxt.DynlinkingGo() || ctxt.BuildMode == BuildModeCShared || !linkerFlagSupported(ctxt.Arch, argv[0], altLinker, "-Wl,--export-dynamic-symbol=main") {
17771778
argv = append(argv, "-rdynamic")
17781779
} else {
@@ -1783,10 +1784,12 @@ func (ctxt *Link) hostlink() {
17831784
sort.Strings(exports)
17841785
argv = append(argv, exports...)
17851786
}
1786-
}
1787-
if ctxt.HeadType == objabi.Haix {
1787+
case ctxt.IsAIX():
17881788
fileName := xcoffCreateExportFile(ctxt)
17891789
argv = append(argv, "-Wl,-bE:"+fileName)
1790+
case ctxt.IsWindows() && !slices.Contains(flagExtldflags, "-Wl,--export-all-symbols"):
1791+
fileName := peCreateExportFile(ctxt, filepath.Base(outopt))
1792+
argv = append(argv, fileName)
17901793
}
17911794

17921795
const unusedArguments = "-Qunused-arguments"

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package ld
99

1010
import (
11+
"bytes"
1112
"cmd/internal/objabi"
1213
"cmd/internal/sys"
1314
"cmd/link/internal/loader"
@@ -17,6 +18,8 @@ import (
1718
"fmt"
1819
"internal/buildcfg"
1920
"math"
21+
"os"
22+
"path/filepath"
2023
"slices"
2124
"sort"
2225
"strconv"
@@ -1748,3 +1751,44 @@ func asmbPe(ctxt *Link) {
17481751

17491752
pewrite(ctxt)
17501753
}
1754+
1755+
// peCreateExportFile creates a file with exported symbols for Windows .def files.
1756+
// ld will export all symbols, even those not marked for export, unless a .def file is provided.
1757+
func peCreateExportFile(ctxt *Link, libName string) (fname string) {
1758+
fname = filepath.Join(*flagTmpdir, "export_file.def")
1759+
var buf bytes.Buffer
1760+
1761+
fmt.Fprintf(&buf, "LIBRARY %s\n", libName)
1762+
buf.WriteString("EXPORTS\n")
1763+
1764+
ldr := ctxt.loader
1765+
var exports []string
1766+
for s := range ldr.ForAllCgoExportStatic() {
1767+
extname := ldr.SymExtname(s)
1768+
if !strings.HasPrefix(extname, "_cgoexp_") {
1769+
continue
1770+
}
1771+
if ldr.IsFileLocal(s) {
1772+
continue // Only export non-static symbols
1773+
}
1774+
// Retrieve the name of the initial symbol
1775+
// exported by cgo.
1776+
// The corresponding Go symbol is:
1777+
// _cgoexp_hashcode_symname.
1778+
name := strings.SplitN(extname, "_", 4)[3]
1779+
exports = append(exports, name)
1780+
}
1781+
if len(exports) == 0 {
1782+
// See runtime/cgo/windows.go for details.
1783+
exports = append(exports, "_cgo_stub_export")
1784+
}
1785+
sort.Strings(exports)
1786+
buf.WriteString(strings.Join(exports, "\n"))
1787+
1788+
err := os.WriteFile(fname, buf.Bytes(), 0666)
1789+
if err != nil {
1790+
Errorf("WriteFile %s failed: %v", fname, err)
1791+
}
1792+
1793+
return fname
1794+
}

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1779,10 +1779,7 @@ func xcoffCreateExportFile(ctxt *Link) (fname string) {
17791779
var buf bytes.Buffer
17801780

17811781
ldr := ctxt.loader
1782-
for s, nsym := loader.Sym(1), loader.Sym(ldr.NSym()); s < nsym; s++ {
1783-
if !ldr.AttrCgoExport(s) {
1784-
continue
1785-
}
1782+
for s := range ldr.ForAllCgoExportStatic() {
17861783
extname := ldr.SymExtname(s)
17871784
if !strings.HasPrefix(extname, "._cgoexp_") {
17881785
continue

src/cmd/link/internal/loader/loader.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"fmt"
1717
"internal/abi"
1818
"io"
19+
"iter"
1920
"log"
2021
"math/bits"
2122
"os"
@@ -1109,6 +1110,18 @@ func (l *Loader) SetAttrCgoExportStatic(i Sym, v bool) {
11091110
}
11101111
}
11111112

1113+
// ForAllCgoExportStatic returns an iterator over all symbols
1114+
// marked with the "cgo_export_static" compiler directive.
1115+
func (l *Loader) ForAllCgoExportStatic() iter.Seq[Sym] {
1116+
return func(yield func(Sym) bool) {
1117+
for s := range l.attrCgoExportStatic {
1118+
if !yield(s) {
1119+
break
1120+
}
1121+
}
1122+
}
1123+
}
1124+
11121125
// IsGeneratedSym returns true if a symbol's been previously marked as a
11131126
// generator symbol through the SetIsGeneratedSym. The functions for generator
11141127
// symbols are kept in the Link context.

src/runtime/cgo/gcc_libinit_windows.c

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,6 @@
1515
#include "libcgo.h"
1616
#include "libcgo_windows.h"
1717

18-
// Ensure there's one symbol marked __declspec(dllexport).
19-
// If there are no exported symbols, the unfortunate behavior of
20-
// the binutils linker is to also strip the relocations table,
21-
// resulting in non-PIE binary. The other option is the
22-
// --export-all-symbols flag, but we don't need to export all symbols
23-
// and this may overflow the export table (#40795).
24-
// See https://sourceware.org/bugzilla/show_bug.cgi?id=19011
25-
__declspec(dllexport) int _cgo_dummy_export;
26-
2718
static volatile LONG runtime_init_once_gate = 0;
2819
static volatile LONG runtime_init_once_done = 0;
2920

src/runtime/cgo/windows.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build windows
6+
7+
package cgo
8+
9+
import _ "unsafe" // for go:linkname
10+
11+
// _cgo_stub_export is only used to ensure there's at least one symbol
12+
// in the .def file passed to the external linker.
13+
// If there are no exported symbols, the unfortunate behavior of
14+
// the binutils linker is to also strip the relocations table,
15+
// resulting in non-PIE binary. The other option is the
16+
// --export-all-symbols flag, but we don't need to export all symbols
17+
// and this may overflow the export table (#40795).
18+
// See https://sourceware.org/bugzilla/show_bug.cgi?id=19011
19+
//
20+
//go:cgo_export_static _cgo_stub_export
21+
//go:linkname _cgo_stub_export _cgo_stub_export
22+
var _cgo_stub_export uintptr

0 commit comments

Comments
 (0)