Skip to content

Commit

Permalink
debug/buildinfo: new package with Read and ReadFile
Browse files Browse the repository at this point in the history
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
Show file tree
Hide file tree
Showing 5 changed files with 645 additions and 48 deletions.
4 changes: 4 additions & 0 deletions api/next.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
pkg debug/buildinfo, func Read(io.ReaderAt) (*debug.BuildInfo, error)
pkg debug/buildinfo, func ReadFile(string) (*debug.BuildInfo, error)
pkg debug/buildinfo, type BuildInfo = debug.BuildInfo
pkg runtime/debug, method (*BuildInfo) MarshalText() ([]byte, error)
pkg runtime/debug, method (*BuildInfo) UnmarshalText() ([]byte, error)
pkg syscall (darwin-amd64), func RecvfromInet4(int, []uint8, int, *SockaddrInet4) (int, error)
pkg syscall (darwin-amd64), func RecvfromInet6(int, []uint8, int, *SockaddrInet6) (int, error)
pkg syscall (darwin-amd64), func SendtoInet4(int, []uint8, int, SockaddrInet4) error
Expand Down
374 changes: 374 additions & 0 deletions src/debug/buildinfo/buildinfo.go
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
}
Loading

0 comments on commit 434cdd0

Please sign in to comment.