mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
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 #30674 Fixes #56994 Change-Id: I22bd0aa079e2be4ae43b13d893f6b804eaeddabf Reviewed-on: https://go-review.googlesource.com/c/go/+/705776 Reviewed-by: Michael Knyszek <mknyszek@google.com> Reviewed-by: Junyang Shao <shaojunyang@google.com> Reviewed-by: Than McIntosh <thanm@golang.org> Reviewed-by: Cherry Mui <cherryyz@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
parent
4b77733565
commit
eaf2345256
8 changed files with 118 additions and 65 deletions
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"cmd/cgo/internal/cgotest"
|
"cmd/cgo/internal/cgotest"
|
||||||
|
"cmp"
|
||||||
"debug/elf"
|
"debug/elf"
|
||||||
"debug/pe"
|
"debug/pe"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
|
@ -272,7 +273,7 @@ func createHeaders() error {
|
||||||
// which results in the linkers output implib getting overwritten at each step. So instead build the
|
// which results in the linkers output implib getting overwritten at each step. So instead build the
|
||||||
// import library the traditional way, using a def file.
|
// import library the traditional way, using a def file.
|
||||||
err = os.WriteFile("libgo.def",
|
err = os.WriteFile("libgo.def",
|
||||||
[]byte("LIBRARY libgo.dll\nEXPORTS\n\tDidInitRun\n\tDidMainRun\n\tDivu\n\tFromPkg\n\t_cgo_dummy_export\n"),
|
[]byte("LIBRARY libgo.dll\nEXPORTS\n\tDidInitRun\n\tDidMainRun\n\tDivu\n\tFromPkg\n"),
|
||||||
0644)
|
0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to write def file: %v", err)
|
return fmt.Errorf("unable to write def file: %v", err)
|
||||||
|
|
@ -375,9 +376,23 @@ func TestExportedSymbols(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkNumberOfExportedFunctionsWindows(t *testing.T, prog string, exportedFunctions int, wantAll bool) {
|
func checkNumberOfExportedSymbolsWindows(t *testing.T, exportedSymbols int, wantAll bool) {
|
||||||
|
t.Parallel()
|
||||||
tmpdir := t.TempDir()
|
tmpdir := t.TempDir()
|
||||||
|
|
||||||
|
prog := `
|
||||||
|
package main
|
||||||
|
import "C"
|
||||||
|
func main() {}
|
||||||
|
`
|
||||||
|
|
||||||
|
for i := range exportedSymbols {
|
||||||
|
prog += fmt.Sprintf(`
|
||||||
|
//export GoFunc%d
|
||||||
|
func GoFunc%d() {}
|
||||||
|
`, i, i)
|
||||||
|
}
|
||||||
|
|
||||||
srcfile := filepath.Join(tmpdir, "test.go")
|
srcfile := filepath.Join(tmpdir, "test.go")
|
||||||
objfile := filepath.Join(tmpdir, "test.dll")
|
objfile := filepath.Join(tmpdir, "test.dll")
|
||||||
if err := os.WriteFile(srcfile, []byte(prog), 0666); err != nil {
|
if err := os.WriteFile(srcfile, []byte(prog), 0666); err != nil {
|
||||||
|
|
@ -443,18 +458,19 @@ func checkNumberOfExportedFunctionsWindows(t *testing.T, prog string, exportedFu
|
||||||
t.Fatalf("binary.Read failed: %v", err)
|
t.Fatalf("binary.Read failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only the two exported functions and _cgo_dummy_export should be exported.
|
exportedSymbols = cmp.Or(exportedSymbols, 1) // _cgo_stub_export is exported if there are no other symbols exported
|
||||||
|
|
||||||
// NumberOfNames is the number of functions exported with a unique name.
|
// NumberOfNames is the number of functions exported with a unique name.
|
||||||
// NumberOfFunctions can be higher than that because it also counts
|
// NumberOfFunctions can be higher than that because it also counts
|
||||||
// functions exported only by ordinal, a unique number asigned by the linker,
|
// functions exported only by ordinal, a unique number asigned by the linker,
|
||||||
// and linkers might add an unknown number of their own ordinal-only functions.
|
// and linkers might add an unknown number of their own ordinal-only functions.
|
||||||
if wantAll {
|
if wantAll {
|
||||||
if e.NumberOfNames <= uint32(exportedFunctions) {
|
if e.NumberOfNames <= uint32(exportedSymbols) {
|
||||||
t.Errorf("got %d exported names, want > %d", e.NumberOfNames, exportedFunctions)
|
t.Errorf("got %d exported names, want > %d", e.NumberOfNames, exportedSymbols)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if e.NumberOfNames > uint32(exportedFunctions) {
|
if e.NumberOfNames != uint32(exportedSymbols) {
|
||||||
t.Errorf("got %d exported names, want <= %d", e.NumberOfNames, exportedFunctions)
|
t.Errorf("got %d exported names, want %d", e.NumberOfNames, exportedSymbols)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -470,43 +486,14 @@ func TestNumberOfExportedFunctions(t *testing.T) {
|
||||||
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const prog0 = `
|
for i := range 3 {
|
||||||
package main
|
t.Run(fmt.Sprintf("OnlyExported/%d", i), func(t *testing.T) {
|
||||||
|
checkNumberOfExportedSymbolsWindows(t, i, false)
|
||||||
import "C"
|
})
|
||||||
|
t.Run(fmt.Sprintf("All/%d", i), func(t *testing.T) {
|
||||||
func main() {
|
checkNumberOfExportedSymbolsWindows(t, i, true)
|
||||||
}
|
})
|
||||||
`
|
}
|
||||||
|
|
||||||
const prog2 = `
|
|
||||||
package main
|
|
||||||
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
//export GoFunc
|
|
||||||
func GoFunc() {
|
|
||||||
println(42)
|
|
||||||
}
|
|
||||||
|
|
||||||
//export GoFunc2
|
|
||||||
func GoFunc2() {
|
|
||||||
println(24)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
}
|
|
||||||
`
|
|
||||||
// All programs export _cgo_dummy_export, so add 1 to the expected counts.
|
|
||||||
t.Run("OnlyExported/0", func(t *testing.T) {
|
|
||||||
checkNumberOfExportedFunctionsWindows(t, prog0, 0+1, false)
|
|
||||||
})
|
|
||||||
t.Run("OnlyExported/2", func(t *testing.T) {
|
|
||||||
checkNumberOfExportedFunctionsWindows(t, prog2, 2+1, false)
|
|
||||||
})
|
|
||||||
t.Run("All", func(t *testing.T) {
|
|
||||||
checkNumberOfExportedFunctionsWindows(t, prog2, 2+1, true)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// test1: shared library can be dynamically loaded and exported symbols are accessible.
|
// test1: shared library can be dynamically loaded and exported symbols are accessible.
|
||||||
|
|
|
||||||
|
|
@ -1005,12 +1005,8 @@ func (p *Package) writeExports(fgo2, fm, fgcc, fgcch io.Writer) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the wrapper function compiled by gcc.
|
// Build the wrapper function compiled by gcc.
|
||||||
gccExport := ""
|
|
||||||
if goos == "windows" {
|
|
||||||
gccExport = "__declspec(dllexport) "
|
|
||||||
}
|
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
fmt.Fprintf(&s, "%s%s %s(", gccExport, gccResult, exp.ExpName)
|
fmt.Fprintf(&s, "%s %s(", gccResult, exp.ExpName)
|
||||||
if fn.Recv != nil {
|
if fn.Recv != nil {
|
||||||
s.WriteString(p.cgoType(fn.Recv.List[0].Type).C.String())
|
s.WriteString(p.cgoType(fn.Recv.List[0].Type).C.String())
|
||||||
s.WriteString(" recv")
|
s.WriteString(" recv")
|
||||||
|
|
|
||||||
|
|
@ -1772,7 +1772,8 @@ func (ctxt *Link) hostlink() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force global symbols to be exported for dlopen, etc.
|
// Force global symbols to be exported for dlopen, etc.
|
||||||
if ctxt.IsELF {
|
switch {
|
||||||
|
case ctxt.IsELF:
|
||||||
if ctxt.DynlinkingGo() || ctxt.BuildMode == BuildModeCShared || !linkerFlagSupported(ctxt.Arch, argv[0], altLinker, "-Wl,--export-dynamic-symbol=main") {
|
if ctxt.DynlinkingGo() || ctxt.BuildMode == BuildModeCShared || !linkerFlagSupported(ctxt.Arch, argv[0], altLinker, "-Wl,--export-dynamic-symbol=main") {
|
||||||
argv = append(argv, "-rdynamic")
|
argv = append(argv, "-rdynamic")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1783,10 +1784,12 @@ func (ctxt *Link) hostlink() {
|
||||||
sort.Strings(exports)
|
sort.Strings(exports)
|
||||||
argv = append(argv, exports...)
|
argv = append(argv, exports...)
|
||||||
}
|
}
|
||||||
}
|
case ctxt.IsAIX():
|
||||||
if ctxt.HeadType == objabi.Haix {
|
|
||||||
fileName := xcoffCreateExportFile(ctxt)
|
fileName := xcoffCreateExportFile(ctxt)
|
||||||
argv = append(argv, "-Wl,-bE:"+fileName)
|
argv = append(argv, "-Wl,-bE:"+fileName)
|
||||||
|
case ctxt.IsWindows() && !slices.Contains(flagExtldflags, "-Wl,--export-all-symbols"):
|
||||||
|
fileName := peCreateExportFile(ctxt, filepath.Base(outopt))
|
||||||
|
argv = append(argv, fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
const unusedArguments = "-Qunused-arguments"
|
const unusedArguments = "-Qunused-arguments"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
package ld
|
package ld
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"cmd/internal/objabi"
|
"cmd/internal/objabi"
|
||||||
"cmd/internal/sys"
|
"cmd/internal/sys"
|
||||||
"cmd/link/internal/loader"
|
"cmd/link/internal/loader"
|
||||||
|
|
@ -17,6 +18,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"internal/buildcfg"
|
"internal/buildcfg"
|
||||||
"math"
|
"math"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
@ -1748,3 +1751,44 @@ func asmbPe(ctxt *Link) {
|
||||||
|
|
||||||
pewrite(ctxt)
|
pewrite(ctxt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// peCreateExportFile creates a file with exported symbols for Windows .def files.
|
||||||
|
// ld will export all symbols, even those not marked for export, unless a .def file is provided.
|
||||||
|
func peCreateExportFile(ctxt *Link, libName string) (fname string) {
|
||||||
|
fname = filepath.Join(*flagTmpdir, "export_file.def")
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
fmt.Fprintf(&buf, "LIBRARY %s\n", libName)
|
||||||
|
buf.WriteString("EXPORTS\n")
|
||||||
|
|
||||||
|
ldr := ctxt.loader
|
||||||
|
var exports []string
|
||||||
|
for s := range ldr.ForAllCgoExportStatic() {
|
||||||
|
extname := ldr.SymExtname(s)
|
||||||
|
if !strings.HasPrefix(extname, "_cgoexp_") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ldr.IsFileLocal(s) {
|
||||||
|
continue // Only export non-static symbols
|
||||||
|
}
|
||||||
|
// Retrieve the name of the initial symbol
|
||||||
|
// exported by cgo.
|
||||||
|
// The corresponding Go symbol is:
|
||||||
|
// _cgoexp_hashcode_symname.
|
||||||
|
name := strings.SplitN(extname, "_", 4)[3]
|
||||||
|
exports = append(exports, name)
|
||||||
|
}
|
||||||
|
if len(exports) == 0 {
|
||||||
|
// See runtime/cgo/windows.go for details.
|
||||||
|
exports = append(exports, "_cgo_stub_export")
|
||||||
|
}
|
||||||
|
sort.Strings(exports)
|
||||||
|
buf.WriteString(strings.Join(exports, "\n"))
|
||||||
|
|
||||||
|
err := os.WriteFile(fname, buf.Bytes(), 0666)
|
||||||
|
if err != nil {
|
||||||
|
Errorf("WriteFile %s failed: %v", fname, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fname
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1779,10 +1779,7 @@ func xcoffCreateExportFile(ctxt *Link) (fname string) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
ldr := ctxt.loader
|
ldr := ctxt.loader
|
||||||
for s, nsym := loader.Sym(1), loader.Sym(ldr.NSym()); s < nsym; s++ {
|
for s := range ldr.ForAllCgoExportStatic() {
|
||||||
if !ldr.AttrCgoExport(s) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
extname := ldr.SymExtname(s)
|
extname := ldr.SymExtname(s)
|
||||||
if !strings.HasPrefix(extname, "._cgoexp_") {
|
if !strings.HasPrefix(extname, "._cgoexp_") {
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"internal/abi"
|
"internal/abi"
|
||||||
"io"
|
"io"
|
||||||
|
"iter"
|
||||||
"log"
|
"log"
|
||||||
"math/bits"
|
"math/bits"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -1109,6 +1110,18 @@ func (l *Loader) SetAttrCgoExportStatic(i Sym, v bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ForAllCgoExportStatic returns an iterator over all symbols
|
||||||
|
// marked with the "cgo_export_static" compiler directive.
|
||||||
|
func (l *Loader) ForAllCgoExportStatic() iter.Seq[Sym] {
|
||||||
|
return func(yield func(Sym) bool) {
|
||||||
|
for s := range l.attrCgoExportStatic {
|
||||||
|
if !yield(s) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// IsGeneratedSym returns true if a symbol's been previously marked as a
|
// IsGeneratedSym returns true if a symbol's been previously marked as a
|
||||||
// generator symbol through the SetIsGeneratedSym. The functions for generator
|
// generator symbol through the SetIsGeneratedSym. The functions for generator
|
||||||
// symbols are kept in the Link context.
|
// symbols are kept in the Link context.
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,6 @@
|
||||||
#include "libcgo.h"
|
#include "libcgo.h"
|
||||||
#include "libcgo_windows.h"
|
#include "libcgo_windows.h"
|
||||||
|
|
||||||
// Ensure there's one symbol marked __declspec(dllexport).
|
|
||||||
// If there are no exported symbols, the unfortunate behavior of
|
|
||||||
// the binutils linker is to also strip the relocations table,
|
|
||||||
// resulting in non-PIE binary. The other option is the
|
|
||||||
// --export-all-symbols flag, but we don't need to export all symbols
|
|
||||||
// and this may overflow the export table (#40795).
|
|
||||||
// See https://sourceware.org/bugzilla/show_bug.cgi?id=19011
|
|
||||||
__declspec(dllexport) int _cgo_dummy_export;
|
|
||||||
|
|
||||||
static volatile LONG runtime_init_once_gate = 0;
|
static volatile LONG runtime_init_once_gate = 0;
|
||||||
static volatile LONG runtime_init_once_done = 0;
|
static volatile LONG runtime_init_once_done = 0;
|
||||||
|
|
||||||
|
|
|
||||||
22
src/runtime/cgo/windows.go
Normal file
22
src/runtime/cgo/windows.go
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright 2025 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.
|
||||||
|
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package cgo
|
||||||
|
|
||||||
|
import _ "unsafe" // for go:linkname
|
||||||
|
|
||||||
|
// _cgo_stub_export is only used to ensure there's at least one symbol
|
||||||
|
// in the .def file passed to the external linker.
|
||||||
|
// If there are no exported symbols, the unfortunate behavior of
|
||||||
|
// the binutils linker is to also strip the relocations table,
|
||||||
|
// resulting in non-PIE binary. The other option is the
|
||||||
|
// --export-all-symbols flag, but we don't need to export all symbols
|
||||||
|
// and this may overflow the export table (#40795).
|
||||||
|
// See https://sourceware.org/bugzilla/show_bug.cgi?id=19011
|
||||||
|
//
|
||||||
|
//go:cgo_export_static _cgo_stub_export
|
||||||
|
//go:linkname _cgo_stub_export _cgo_stub_export
|
||||||
|
var _cgo_stub_export uintptr
|
||||||
Loading…
Add table
Add a link
Reference in a new issue