[go] cmd/link, cmd/go: make version info easier to extract

4 views
Skip to first unread message

Russ Cox (Gerrit)

unread,
Dec 7, 2021, 3:15:02 PM12/7/21
to Russ Cox, goph...@pubsubhelper.golang.org, golang-...@googlegroups.com, Gopher Robot, Cherry Mui, golang-co...@googlegroups.com

Russ Cox submitted this change.

View Change


Approvals: Cherry Mui: Looks good to me, approved Russ Cox: Trusted; Run TryBots Gopher Robot: TryBots succeeded
cmd/link, cmd/go: make version info easier to extract

Reading the version information to date has required evaluating
two pointers to strings (which themselves contain pointers to data),
which means applying relocations, which can be very system-dependent.

To simplify the lookup, inline the string data into the build info blob.

This makes go version work on binaries built with external linking
on darwin/arm64.

Also test that at least the very basics work on a trivial binary,
even in short mode.

Change-Id: I463088c19e837ae0ce57e1278c7b72e74a80b2c4
Reviewed-on: https://go-review.googlesource.com/c/go/+/369977
Trust: Russ Cox <r...@golang.org>
Run-TryBot: Russ Cox <r...@golang.org>
TryBot-Result: Gopher Robot <go...@golang.org>
Reviewed-by: Cherry Mui <cher...@google.com>
---
M src/cmd/go/internal/modload/build.go
M src/cmd/link/internal/ld/data.go
M src/cmd/link/internal/ld/deadcode.go
M src/cmd/link/internal/ld/ld.go
M src/cmd/go/testdata/script/version.txt
M src/debug/buildinfo/buildinfo.go
M src/cmd/go/internal/work/exec.go
7 files changed, 119 insertions(+), 60 deletions(-)

diff --git a/src/cmd/go/internal/modload/build.go b/src/cmd/go/internal/modload/build.go
index 0e0292e..bfc73cc 100644
--- a/src/cmd/go/internal/modload/build.go
+++ b/src/cmd/go/internal/modload/build.go
@@ -346,33 +346,22 @@
}

func ModInfoProg(info string, isgccgo bool) []byte {
- // Inject a variable with the debug information as runtime.modinfo,
- // but compile it in package main so that it is specific to the binary.
- // The variable must be a literal so that it will have the correct value
- // before the initializer for package main runs.
- //
- // The runtime startup code refers to the variable, which keeps it live
- // in all binaries.
- //
- // Note: we use an alternate recipe below for gccgo (based on an
- // init function) due to the fact that gccgo does not support
- // applying a "//go:linkname" directive to a variable. This has
- // drawbacks in that other packages may want to look at the module
- // info in their init functions (see issue 29628), which won't
- // work for gccgo. See also issue 30344.
-
- if !isgccgo {
- return []byte(fmt.Sprintf(`package main
-import _ "unsafe"
-//go:linkname __debug_modinfo__ runtime.modinfo
-var __debug_modinfo__ = %q
-`, string(infoStart)+info+string(infoEnd)))
- } else {
+ // Inject an init function to set runtime.modinfo.
+ // This is only used for gccgo - with gc we hand the info directly to the linker.
+ // The init function has the drawback that packages may want to
+ // look at the module info in their init functions (see issue 29628),
+ // which won't work. See also issue 30344.
+ if isgccgo {
return []byte(fmt.Sprintf(`package main
import _ "unsafe"
//go:linkname __set_debug_modinfo__ runtime.setmodinfo
func __set_debug_modinfo__(string)
func init() { __set_debug_modinfo__(%q) }
-`, string(infoStart)+info+string(infoEnd)))
+`, ModInfoData(info)))
}
+ return nil
+}
+
+func ModInfoData(info string) []byte {
+ return []byte(string(infoStart) + info + string(infoEnd))
}
diff --git a/src/cmd/go/internal/work/exec.go b/src/cmd/go/internal/work/exec.go
index 03f8866..2c040b8 100644
--- a/src/cmd/go/internal/work/exec.go
+++ b/src/cmd/go/internal/work/exec.go
@@ -794,10 +794,13 @@
}

if p.Internal.BuildInfo != "" && cfg.ModulesEnabled {
- if err := b.writeFile(objdir+"_gomod_.go", modload.ModInfoProg(p.Internal.BuildInfo, cfg.BuildToolchainName == "gccgo")); err != nil {
- return err
+ prog := modload.ModInfoProg(p.Internal.BuildInfo, cfg.BuildToolchainName == "gccgo")
+ if len(prog) > 0 {
+ if err := b.writeFile(objdir+"_gomod_.go", prog); err != nil {
+ return err
+ }
+ gofiles = append(gofiles, objdir+"_gomod_.go")
}
- gofiles = append(gofiles, objdir+"_gomod_.go")
}

// Compile Go.
@@ -1394,6 +1397,7 @@
fmt.Fprintf(&icfg, "packageshlib %s=%s\n", p1.ImportPath, p1.Shlib)
}
}
+ fmt.Fprintf(&icfg, "modinfo %q\n", modload.ModInfoData(a.Package.Internal.BuildInfo))
return b.writeFile(file, icfg.Bytes())
}

diff --git a/src/cmd/go/testdata/script/version.txt b/src/cmd/go/testdata/script/version.txt
index 8c08bae..adca7af 100644
--- a/src/cmd/go/testdata/script/version.txt
+++ b/src/cmd/go/testdata/script/version.txt
@@ -16,7 +16,14 @@
env GOFLAGS=

env GO111MODULE=on
-# Skip the builds below if we are running in short mode.
+
+# Check that very basic version lookup succeeds.
+go build empty.go
+go version empty$GOEXE
+[cgo] go build -ldflags=-linkmode=external empty.go
+[cgo] go version empty$GOEXE
+
+# Skip the remaining builds if we are running in short mode.
[short] skip

# Check that 'go version' and 'go version -m' work on a binary built in module mode.
@@ -57,3 +64,7 @@

-- go.mod --
module m
+
+-- empty.go --
+package main
+func main(){}
diff --git a/src/cmd/link/internal/ld/data.go b/src/cmd/link/internal/ld/data.go
index 4d85977..95a8e0f 100644
--- a/src/cmd/link/internal/ld/data.go
+++ b/src/cmd/link/internal/ld/data.go
@@ -2169,11 +2169,10 @@
return
}

+ // Write the buildinfo symbol, which go version looks for.
+ // The code reading this data is in package debug/buildinfo.
ldr := ctxt.loader
s := ldr.CreateSymForUpdate(".go.buildinfo", 0)
- // On AIX, .go.buildinfo must be in the symbol table as
- // it has relocations.
- s.SetNotInSymbolTable(!ctxt.IsAIX())
s.SetType(sym.SBUILDINFO)
s.SetAlign(16)
// The \xff is invalid UTF-8, meant to make it less likely
@@ -2186,16 +2185,24 @@
if ctxt.Arch.ByteOrder == binary.BigEndian {
data[len(prefix)+1] = 1
}
+ data[len(prefix)+1] |= 2 // signals new pointer-free format
+ data = appendString(data, strdata["runtime.buildVersion"])
+ data = appendString(data, strdata["runtime.modinfo"])
+ // MacOS linker gets very upset if the size os not a multiple of alignment.
+ for len(data)%16 != 0 {
+ data = append(data, 0)
+ }
s.SetData(data)
s.SetSize(int64(len(data)))
- r, _ := s.AddRel(objabi.R_ADDR)
- r.SetOff(16)
- r.SetSiz(uint8(ctxt.Arch.PtrSize))
- r.SetSym(ldr.LookupOrCreateSym("runtime.buildVersion", 0))
- r, _ = s.AddRel(objabi.R_ADDR)
- r.SetOff(16 + int32(ctxt.Arch.PtrSize))
- r.SetSiz(uint8(ctxt.Arch.PtrSize))
- r.SetSym(ldr.LookupOrCreateSym("runtime.modinfo", 0))
+}
+
+// appendString appends s to data, prefixed by its varint-encoded length.
+func appendString(data []byte, s string) []byte {
+ var v [binary.MaxVarintLen64]byte
+ n := binary.PutUvarint(v[:], uint64(len(s)))
+ data = append(data, v[:n]...)
+ data = append(data, s...)
+ return data
}

// assign addresses to text
diff --git a/src/cmd/link/internal/ld/deadcode.go b/src/cmd/link/internal/ld/deadcode.go
index 7b57a85..dba2232 100644
--- a/src/cmd/link/internal/ld/deadcode.go
+++ b/src/cmd/link/internal/ld/deadcode.go
@@ -71,12 +71,6 @@
// runtime.unreachableMethod is a function that will throw if called.
// We redirect unreachable methods to it.
names = append(names, "runtime.unreachableMethod")
- if !d.ctxt.linkShared && d.ctxt.BuildMode != BuildModePlugin {
- // runtime.buildVersion and runtime.modinfo are referenced in .go.buildinfo section
- // (see function buildinfo in data.go). They should normally be reachable from the
- // runtime. Just make it explicit, in case.
- names = append(names, "runtime.buildVersion", "runtime.modinfo")
- }
if d.ctxt.BuildMode == BuildModePlugin {
names = append(names, objabi.PathToPrefix(*flagPluginPath)+"..inittask", objabi.PathToPrefix(*flagPluginPath)+".main", "go.plugin.tabs")

diff --git a/src/cmd/link/internal/ld/ld.go b/src/cmd/link/internal/ld/ld.go
index 7ff9c41..9549218 100644
--- a/src/cmd/link/internal/ld/ld.go
+++ b/src/cmd/link/internal/ld/ld.go
@@ -85,6 +85,12 @@
log.Fatalf(`%s:%d: invalid packageshlib: syntax is "packageshlib path=filename"`, file, lineNum)
}
ctxt.PackageShlib[before] = after
+ case "modinfo":
+ s, err := strconv.Unquote(args)
+ if err != nil {
+ log.Fatalf("%s:%d: invalid modinfo: %v", file, lineNum, err)
+ }
+ addstrdata1(ctxt, "runtime.modinfo="+s)
}
}
}
diff --git a/src/debug/buildinfo/buildinfo.go b/src/debug/buildinfo/buildinfo.go
index f84429a..2c0200e 100644
--- a/src/debug/buildinfo/buildinfo.go
+++ b/src/debug/buildinfo/buildinfo.go
@@ -146,12 +146,18 @@
}
const (
buildInfoAlign = 16
- buildinfoSize = 32
+ buildInfoSize = 32
)
- for ; !bytes.HasPrefix(data, buildInfoMagic); data = data[buildInfoAlign:] {
- if len(data) < 32 {
+ for {
+ i := bytes.Index(data, buildInfoMagic)
+ if i < 0 || len(data)-i < buildInfoSize {
return "", "", errNotGoExe
}
+ if i%buildInfoAlign == 0 && len(data)-i >= buildInfoSize {
+ data = data[i:]
+ break
+ }
+ data = data[(i+buildInfoAlign-1)&^buildInfoAlign:]
}

// Decode the blob.
@@ -161,25 +167,33 @@
// Two virtual addresses to Go strings follow that: runtime.buildVersion,
// and runtime.modinfo.
// On 32-bit platforms, the last 8 bytes are unused.
+ // If the endianness has the 2 bit set, then the pointers are zero
+ // and the 32-byte header is followed by varint-prefixed string data
+ // for the two string values we care about.
ptrSize := int(data[14])
- bigEndian := data[15] != 0
- var bo binary.ByteOrder
- if bigEndian {
- bo = binary.BigEndian
+ if data[15]&2 != 0 {
+ vers, data = decodeString(data[32:])
+ mod, data = decodeString(data)
} else {
- bo = binary.LittleEndian
+ bigEndian := data[15] != 0
+ var bo binary.ByteOrder
+ if bigEndian {
+ bo = binary.BigEndian
+ } else {
+ bo = binary.LittleEndian
+ }
+ var readPtr func([]byte) uint64
+ if ptrSize == 4 {
+ readPtr = func(b []byte) uint64 { return uint64(bo.Uint32(b)) }
+ } else {
+ readPtr = bo.Uint64
+ }
+ vers = readString(x, ptrSize, readPtr, readPtr(data[16:]))
+ mod = readString(x, ptrSize, readPtr, readPtr(data[16+ptrSize:]))
}
- var readPtr func([]byte) uint64
- if ptrSize == 4 {
- readPtr = func(b []byte) uint64 { return uint64(bo.Uint32(b)) }
- } else {
- readPtr = bo.Uint64
- }
- vers = readString(x, ptrSize, readPtr, readPtr(data[16:]))
if vers == "" {
return "", "", errNotGoExe
}
- mod = readString(x, ptrSize, readPtr, readPtr(data[16+ptrSize:]))
if len(mod) >= 33 && mod[len(mod)-17] == '\n' {
// Strip module framing: sentinel strings delimiting the module info.
// These are cmd/go/internal/modload.infoStart and infoEnd.
@@ -191,6 +205,14 @@
return vers, mod, nil
}

+func decodeString(data []byte) (s string, rest []byte) {
+ u, n := binary.Uvarint(data)
+ if n <= 0 || u >= uint64(len(data)-n) {
+ return "", nil
+ }
+ return string(data[n : uint64(n)+u]), data[uint64(n)+u:]
+}
+
// readString returns the string at address addr in the executable x.
func readString(x exe, ptrSize int, readPtr func([]byte) uint64, addr uint64) string {
hdr, err := x.ReadData(addr, uint64(2*ptrSize))

To view, visit change 369977. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: go
Gerrit-Branch: master
Gerrit-Change-Id: I463088c19e837ae0ce57e1278c7b72e74a80b2c4
Gerrit-Change-Number: 369977
Gerrit-PatchSet: 3
Gerrit-Owner: Russ Cox <r...@golang.org>
Gerrit-Reviewer: Cherry Mui <cher...@google.com>
Gerrit-Reviewer: Gopher Robot <go...@golang.org>
Gerrit-Reviewer: Russ Cox <r...@golang.org>
Gerrit-MessageType: merged
Reply all
Reply to author
Forward
0 new messages