-
Notifications
You must be signed in to change notification settings - Fork 17.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
debug/buildinfo: new package with Read and ReadFile
These functions provide access to module information stamped into Go binaries. In the future, they'll provide access to other information (like VCS info). These functions are added in a new package instead of runtime/debug since they use binary parsing packages like debug/elf, which would make runtime/debug an unacceptably heavy dependency. The types in runtime/debug are still used; debug/buildinfo uses them via type aliases. This information is already available for the running binary through debug.ReadBuildInfo and for other binaries with 'go version -m', but until now, there hasn't been a way to get it for other binaries without installing cmd/go. This change copies most of the code in cmd/go/internal/version. A later CL will migrate 'go version -m' to use this package. For #37475 Fixes #39301 Change-Id: I0fbe0896e04f12ef81c6d79fb61b20daede86159 Reviewed-on: https://go-review.googlesource.com/c/go/+/353887 Trust: Jay Conrod <[email protected]> Run-TryBot: Jay Conrod <[email protected]> Reviewed-by: Bryan C. Mills <[email protected]>
- Loading branch information
Jay Conrod
committed
Oct 14, 2021
1 parent
765c911
commit 434cdd0
Showing
5 changed files
with
645 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,374 @@ | ||
// Copyright 2021 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. | ||
|
||
// Package buildinfo provides access to information embedded in a Go binary | ||
// about how it was built. This includes the Go toolchain version, and the | ||
// set of modules used (for binaries built in module mode). | ||
// | ||
// Build information is available for the currently running binary in | ||
// runtime/debug.ReadBuildInfo. | ||
package buildinfo | ||
|
||
import ( | ||
"bytes" | ||
"debug/elf" | ||
"debug/macho" | ||
"debug/pe" | ||
"encoding/binary" | ||
"errors" | ||
"fmt" | ||
"internal/xcoff" | ||
"io" | ||
"io/fs" | ||
"os" | ||
"runtime/debug" | ||
) | ||
|
||
// Type alias for build info. We cannot move the types here, since | ||
// runtime/debug would need to import this package, which would make it | ||
// a much larger dependency. | ||
type BuildInfo = debug.BuildInfo | ||
|
||
var ( | ||
// errUnrecognizedFormat is returned when a given executable file doesn't | ||
// appear to be in a known format, or it breaks the rules of that format, | ||
// or when there are I/O errors reading the file. | ||
errUnrecognizedFormat = errors.New("unrecognized file format") | ||
|
||
// errNotGoExe is returned when a given executable file is valid but does | ||
// not contain Go build information. | ||
errNotGoExe = errors.New("not a Go executable") | ||
|
||
// The build info blob left by the linker is identified by | ||
// a 16-byte header, consisting of buildInfoMagic (14 bytes), | ||
// the binary's pointer size (1 byte), | ||
// and whether the binary is big endian (1 byte). | ||
buildInfoMagic = []byte("\xff Go buildinf:") | ||
) | ||
|
||
// ReadFile returns build information embedded in a Go binary | ||
// file at the given path. Most information is only available for binaries built | ||
// with module support. | ||
func ReadFile(name string) (info *BuildInfo, err error) { | ||
defer func() { | ||
if pathErr := (*fs.PathError)(nil); errors.As(err, &pathErr) { | ||
err = fmt.Errorf("could not read Go build info: %w", err) | ||
} else if err != nil { | ||
err = fmt.Errorf("could not read Go build info from %s: %w", name, err) | ||
} | ||
}() | ||
|
||
f, err := os.Open(name) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer f.Close() | ||
return Read(f) | ||
} | ||
|
||
// Read returns build information embedded in a Go binary file | ||
// accessed through the given ReaderAt. Most information is only available for | ||
// binaries built with module support. | ||
func Read(r io.ReaderAt) (*BuildInfo, error) { | ||
_, mod, err := readRawBuildInfo(r) | ||
if err != nil { | ||
return nil, err | ||
} | ||
bi := &BuildInfo{} | ||
if err := bi.UnmarshalText([]byte(mod)); err != nil { | ||
return nil, err | ||
} | ||
return bi, nil | ||
} | ||
|
||
type exe interface { | ||
// ReadData reads and returns up to size bytes starting at virtual address addr. | ||
ReadData(addr, size uint64) ([]byte, error) | ||
|
||
// DataStart returns the virtual address of the segment or section that | ||
// should contain build information. This is either a specially named section | ||
// or the first writable non-zero data segment. | ||
DataStart() uint64 | ||
} | ||
|
||
// readRawBuildInfo extracts the Go toolchain version and module information | ||
// strings from a Go binary. On success, vers should be non-empty. mod | ||
// is empty if the binary was not built with modules enabled. | ||
func readRawBuildInfo(r io.ReaderAt) (vers, mod string, err error) { | ||
// Read the first bytes of the file to identify the format, then delegate to | ||
// a format-specific function to load segment and section headers. | ||
ident := make([]byte, 16) | ||
if n, err := r.ReadAt(ident, 0); n < len(ident) || err != nil { | ||
return "", "", errUnrecognizedFormat | ||
} | ||
|
||
var x exe | ||
switch { | ||
case bytes.HasPrefix(ident, []byte("\x7FELF")): | ||
f, err := elf.NewFile(r) | ||
if err != nil { | ||
return "", "", errUnrecognizedFormat | ||
} | ||
x = &elfExe{f} | ||
case bytes.HasPrefix(ident, []byte("MZ")): | ||
f, err := pe.NewFile(r) | ||
if err != nil { | ||
return "", "", errUnrecognizedFormat | ||
} | ||
x = &peExe{f} | ||
case bytes.HasPrefix(ident, []byte("\xFE\xED\xFA")) || bytes.HasPrefix(ident[1:], []byte("\xFA\xED\xFE")): | ||
f, err := macho.NewFile(r) | ||
if err != nil { | ||
return "", "", errUnrecognizedFormat | ||
} | ||
x = &machoExe{f} | ||
case bytes.HasPrefix(ident, []byte{0x01, 0xDF}) || bytes.HasPrefix(ident, []byte{0x01, 0xF7}): | ||
f, err := xcoff.NewFile(r) | ||
if err != nil { | ||
return "", "", errUnrecognizedFormat | ||
} | ||
x = &xcoffExe{f} | ||
default: | ||
return "", "", errUnrecognizedFormat | ||
} | ||
|
||
// Read the first 64kB of dataAddr to find the build info blob. | ||
// On some platforms, the blob will be in its own section, and DataStart | ||
// returns the address of that section. On others, it's somewhere in the | ||
// data segment; the linker puts it near the beginning. | ||
// See cmd/link/internal/ld.Link.buildinfo. | ||
dataAddr := x.DataStart() | ||
data, err := x.ReadData(dataAddr, 64*1024) | ||
if err != nil { | ||
return "", "", err | ||
} | ||
const ( | ||
buildInfoAlign = 16 | ||
buildinfoSize = 32 | ||
) | ||
for ; !bytes.HasPrefix(data, buildInfoMagic); data = data[buildInfoAlign:] { | ||
if len(data) < 32 { | ||
return "", "", errNotGoExe | ||
} | ||
} | ||
|
||
// Decode the blob. | ||
// The first 14 bytes are buildInfoMagic. | ||
// The next two bytes indicate pointer size in bytes (4 or 8) and endianness | ||
// (0 for little, 1 for big). | ||
// Two virtual addresses to Go strings follow that: runtime.buildVersion, | ||
// and runtime.modinfo. | ||
// On 32-bit platforms, the last 8 bytes are unused. | ||
ptrSize := int(data[14]) | ||
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:])) | ||
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. | ||
mod = mod[16 : len(mod)-16] | ||
} else { | ||
mod = "" | ||
} | ||
|
||
return vers, mod, nil | ||
} | ||
|
||
// 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)) | ||
if err != nil || len(hdr) < 2*ptrSize { | ||
return "" | ||
} | ||
dataAddr := readPtr(hdr) | ||
dataLen := readPtr(hdr[ptrSize:]) | ||
data, err := x.ReadData(dataAddr, dataLen) | ||
if err != nil || uint64(len(data)) < dataLen { | ||
return "" | ||
} | ||
return string(data) | ||
} | ||
|
||
// elfExe is the ELF implementation of the exe interface. | ||
type elfExe struct { | ||
f *elf.File | ||
} | ||
|
||
func (x *elfExe) ReadData(addr, size uint64) ([]byte, error) { | ||
for _, prog := range x.f.Progs { | ||
if prog.Vaddr <= addr && addr <= prog.Vaddr+prog.Filesz-1 { | ||
n := prog.Vaddr + prog.Filesz - addr | ||
if n > size { | ||
n = size | ||
} | ||
data := make([]byte, n) | ||
_, err := prog.ReadAt(data, int64(addr-prog.Vaddr)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return data, nil | ||
} | ||
} | ||
return nil, errUnrecognizedFormat | ||
} | ||
|
||
func (x *elfExe) DataStart() uint64 { | ||
for _, s := range x.f.Sections { | ||
if s.Name == ".go.buildinfo" { | ||
return s.Addr | ||
} | ||
} | ||
for _, p := range x.f.Progs { | ||
if p.Type == elf.PT_LOAD && p.Flags&(elf.PF_X|elf.PF_W) == elf.PF_W { | ||
return p.Vaddr | ||
} | ||
} | ||
return 0 | ||
} | ||
|
||
// peExe is the PE (Windows Portable Executable) implementation of the exe interface. | ||
type peExe struct { | ||
f *pe.File | ||
} | ||
|
||
func (x *peExe) imageBase() uint64 { | ||
switch oh := x.f.OptionalHeader.(type) { | ||
case *pe.OptionalHeader32: | ||
return uint64(oh.ImageBase) | ||
case *pe.OptionalHeader64: | ||
return oh.ImageBase | ||
} | ||
return 0 | ||
} | ||
|
||
func (x *peExe) ReadData(addr, size uint64) ([]byte, error) { | ||
addr -= x.imageBase() | ||
for _, sect := range x.f.Sections { | ||
if uint64(sect.VirtualAddress) <= addr && addr <= uint64(sect.VirtualAddress+sect.Size-1) { | ||
n := uint64(sect.VirtualAddress+sect.Size) - addr | ||
if n > size { | ||
n = size | ||
} | ||
data := make([]byte, n) | ||
_, err := sect.ReadAt(data, int64(addr-uint64(sect.VirtualAddress))) | ||
if err != nil { | ||
return nil, errUnrecognizedFormat | ||
} | ||
return data, nil | ||
} | ||
} | ||
return nil, errUnrecognizedFormat | ||
} | ||
|
||
func (x *peExe) DataStart() uint64 { | ||
// Assume data is first writable section. | ||
const ( | ||
IMAGE_SCN_CNT_CODE = 0x00000020 | ||
IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040 | ||
IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080 | ||
IMAGE_SCN_MEM_EXECUTE = 0x20000000 | ||
IMAGE_SCN_MEM_READ = 0x40000000 | ||
IMAGE_SCN_MEM_WRITE = 0x80000000 | ||
IMAGE_SCN_MEM_DISCARDABLE = 0x2000000 | ||
IMAGE_SCN_LNK_NRELOC_OVFL = 0x1000000 | ||
IMAGE_SCN_ALIGN_32BYTES = 0x600000 | ||
) | ||
for _, sect := range x.f.Sections { | ||
if sect.VirtualAddress != 0 && sect.Size != 0 && | ||
sect.Characteristics&^IMAGE_SCN_ALIGN_32BYTES == IMAGE_SCN_CNT_INITIALIZED_DATA|IMAGE_SCN_MEM_READ|IMAGE_SCN_MEM_WRITE { | ||
return uint64(sect.VirtualAddress) + x.imageBase() | ||
} | ||
} | ||
return 0 | ||
} | ||
|
||
// machoExe is the Mach-O (Apple macOS/iOS) implementation of the exe interface. | ||
type machoExe struct { | ||
f *macho.File | ||
} | ||
|
||
func (x *machoExe) ReadData(addr, size uint64) ([]byte, error) { | ||
for _, load := range x.f.Loads { | ||
seg, ok := load.(*macho.Segment) | ||
if !ok { | ||
continue | ||
} | ||
if seg.Addr <= addr && addr <= seg.Addr+seg.Filesz-1 { | ||
if seg.Name == "__PAGEZERO" { | ||
continue | ||
} | ||
n := seg.Addr + seg.Filesz - addr | ||
if n > size { | ||
n = size | ||
} | ||
data := make([]byte, n) | ||
_, err := seg.ReadAt(data, int64(addr-seg.Addr)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return data, nil | ||
} | ||
} | ||
return nil, errUnrecognizedFormat | ||
} | ||
|
||
func (x *machoExe) DataStart() uint64 { | ||
// Look for section named "__go_buildinfo". | ||
for _, sec := range x.f.Sections { | ||
if sec.Name == "__go_buildinfo" { | ||
return sec.Addr | ||
} | ||
} | ||
// Try the first non-empty writable segment. | ||
const RW = 3 | ||
for _, load := range x.f.Loads { | ||
seg, ok := load.(*macho.Segment) | ||
if ok && seg.Addr != 0 && seg.Filesz != 0 && seg.Prot == RW && seg.Maxprot == RW { | ||
return seg.Addr | ||
} | ||
} | ||
return 0 | ||
} | ||
|
||
// xcoffExe is the XCOFF (AIX eXtended COFF) implementation of the exe interface. | ||
type xcoffExe struct { | ||
f *xcoff.File | ||
} | ||
|
||
func (x *xcoffExe) ReadData(addr, size uint64) ([]byte, error) { | ||
for _, sect := range x.f.Sections { | ||
if uint64(sect.VirtualAddress) <= addr && addr <= uint64(sect.VirtualAddress+sect.Size-1) { | ||
n := uint64(sect.VirtualAddress+sect.Size) - addr | ||
if n > size { | ||
n = size | ||
} | ||
data := make([]byte, n) | ||
_, err := sect.ReadAt(data, int64(addr-uint64(sect.VirtualAddress))) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return data, nil | ||
} | ||
} | ||
return nil, fmt.Errorf("address not mapped") | ||
} | ||
|
||
func (x *xcoffExe) DataStart() uint64 { | ||
return x.f.SectionByType(xcoff.STYP_DATA).VirtualAddress | ||
} |
Oops, something went wrong.