// 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 buildinfoimport (_// for linkname)// 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.typeBuildInfo = debug.BuildInfo// 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.var errUnrecognizedFormat = errors.New("unrecognized file format")// errNotGoExe is returned when a given executable file is valid but does// not contain Go build information.//// errNotGoExe should be an internal detail,// but widely used packages access it using linkname.// Notable members of the hall of shame include:// - github.com/quay/claircore//// Do not remove or change the type signature.// See go.dev/issue/67401.////go:linkname errNotGoExevar errNotGoExe = errors.New("not a Go executable")// The build info blob left by the linker is identified by a 32-byte header,// consisting of buildInfoMagic (14 bytes), followed by version-dependent// fields.var buildInfoMagic = []byte("\xff Go buildinf:")const ( buildInfoAlign = 16 buildInfoHeaderSize = 32)// 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 ( string) ( *BuildInfo, error) {deferfunc() {if := (*fs.PathError)(nil); errors.As(, &) { = fmt.Errorf("could not read Go build info: %w", ) } elseif != nil { = fmt.Errorf("could not read Go build info from %s: %w", , ) } }() , := os.Open()if != nil {returnnil, }defer .Close()returnRead()}// 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 ( io.ReaderAt) (*BuildInfo, error) { , , := readRawBuildInfo()if != nil {returnnil, } , := debug.ParseBuildInfo()if != nil {returnnil, } .GoVersion = return , nil}type exe interface {// DataStart returns the virtual address and size 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, uint64)// DataReader returns an io.ReaderAt that reads from addr until the end // of segment or section that contains addr. DataReader(addr uint64) (io.ReaderAt, error)}// 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( io.ReaderAt) (, string, 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. := make([]byte, 16)if , := .ReadAt(, 0); < len() || != nil {return"", "", errUnrecognizedFormat }varexeswitch {casebytes.HasPrefix(, []byte("\x7FELF")): , := elf.NewFile()if != nil {return"", "", errUnrecognizedFormat } = &elfExe{}casebytes.HasPrefix(, []byte("MZ")): , := pe.NewFile()if != nil {return"", "", errUnrecognizedFormat } = &peExe{}casebytes.HasPrefix(, []byte("\xFE\xED\xFA")) || bytes.HasPrefix([1:], []byte("\xFA\xED\xFE")): , := macho.NewFile()if != nil {return"", "", errUnrecognizedFormat } = &machoExe{}casebytes.HasPrefix(, []byte("\xCA\xFE\xBA\xBE")) || bytes.HasPrefix(, []byte("\xCA\xFE\xBA\xBF")): , := macho.NewFatFile()if != nil || len(.Arches) == 0 {return"", "", errUnrecognizedFormat } = &machoExe{.Arches[0].File}casebytes.HasPrefix(, []byte{0x01, 0xDF}) || bytes.HasPrefix(, []byte{0x01, 0xF7}): , := xcoff.NewFile()if != nil {return"", "", errUnrecognizedFormat } = &xcoffExe{}casehasPlan9Magic(): , := plan9obj.NewFile()if != nil {return"", "", errUnrecognizedFormat } = &plan9objExe{}default:return"", "", errUnrecognizedFormat }// Read segment or section 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. , := .DataStart()if == 0 {return"", "", errNotGoExe } , := searchMagic(, , )if != nil {return"", "", }// Read in the full header first. , := readData(, , buildInfoHeaderSize)if == io.EOF {return"", "", errNotGoExe } elseif != nil {return"", "", }iflen() < buildInfoHeaderSize {return"", "", errNotGoExe }const ( = 14 = 15 = 16 = 0x1 = 0x0 = 0x1 = 0x2 = 0x0 = 0x2 )// Decode the blob. The blob is a 32-byte header, optionally followed // by 2 varint-prefixed string contents. // // type buildInfoHeader struct { // magic [14]byte // ptrSize uint8 // used if flagsVersionPtr // flags uint8 // versPtr targetUintptr // used if flagsVersionPtr // modPtr targetUintptr // used if flagsVersionPtr // } // // The version bit of the flags field determines the details of the format. // // Prior to 1.18, the flags version bit is flagsVersionPtr. In this // case, the header includes pointers to the version and modinfo Go // strings in the header. The ptrSize field indicates the size of the // pointers and the endian bit of the flag indicates the pointer // endianness. // // Since 1.18, the flags version bit is flagsVersionInl. In this case, // the header is followed by the string contents inline as // length-prefixed (as varint) string contents. First is the version // string, followed immediately by the modinfo string. := []if & == { , , = decodeString(, +buildInfoHeaderSize)if != nil {return"", "", } , _, = decodeString(, )if != nil {return"", "", } } else {// flagsVersionPtr (<1.18) := int([]) := & == varbinary.ByteOrderif { = binary.BigEndian } else { = binary.LittleEndian }varfunc([]byte) uint64if == 4 { = func( []byte) uint64 { returnuint64(.Uint32()) } } elseif == 8 { = .Uint64 } else {return"", "", errNotGoExe } = readString(, , , ([:])) = readString(, , , ([+:])) }if == "" {return"", "", errNotGoExe }iflen() >= 33 && [len()-17] == '\n' {// Strip module framing: sentinel strings delimiting the module info. // These are cmd/go/internal/modload.infoStart and infoEnd. = [16 : len()-16] } else { = "" }return , , nil}func hasPlan9Magic( []byte) bool {iflen() >= 4 { := binary.BigEndian.Uint32()switch {caseplan9obj.Magic386, plan9obj.MagicAMD64, plan9obj.MagicARM:returntrue } }returnfalse}func decodeString( exe, uint64) (string, uint64, error) {// varint length followed by length bytes of data.// N.B. ReadData reads _up to_ size bytes from the section containing // addr. So we don't need to check that size doesn't overflow the // section. , := readData(, , binary.MaxVarintLen64)if == io.EOF {return"", 0, errNotGoExe } elseif != nil {return"", 0, } , := binary.Uvarint()if <= 0 {return"", 0, errNotGoExe } += uint64() , = readData(, , )if == io.EOF {return"", 0, errNotGoExe } elseif == io.ErrUnexpectedEOF {// Length too large to allocate. Clearly bogus value.return"", 0, errNotGoExe } elseif != nil {return"", 0, }ifuint64(len()) < {// Section ended before we could read the full string.return"", 0, errNotGoExe }returnstring(), + , nil}// readString returns the string at address addr in the executable x.func readString( exe, int, func([]byte) uint64, uint64) string { , := readData(, , uint64(2*))if != nil || len() < 2* {return"" } := () := ([:]) , := readData(, , )if != nil || uint64(len()) < {return"" }returnstring()}const searchChunkSize = 1 << 20// 1 MB// searchMagic returns the aligned first instance of buildInfoMagic in the data// range [addr, addr+size). Returns false if not found.func searchMagic( exe, , uint64) (uint64, error) { := + if < {// Overflow.return0, errUnrecognizedFormat }// Round up start; magic can't occur in the initial unaligned portion. = ( + buildInfoAlign - 1) &^ (buildInfoAlign - 1)if >= {return0, errNotGoExe }var []bytefor < {// Read in chunks to avoid consuming too much memory if data is large. // // Normally it would be somewhat painful to handle the magic crossing a // chunk boundary, but since it must be 16-byte aligned we know it will // fall within a single chunk. := - := uint64(searchChunkSize)if > { = }if == nil { = make([]byte, ) } else {// N.B. chunkSize can only decrease, and only on the // last chunk. = [:]clear() } , := readDataInto(, , )if == io.EOF {// EOF before finding the magic; must not be a Go executable.return0, errNotGoExe } elseif != nil {return0, } := [:]forlen() > 0 { := bytes.Index(, buildInfoMagic)if < 0 {break }if -uint64() < buildInfoHeaderSize {// Found magic, but not enough space left for the full header.return0, errNotGoExe }if %buildInfoAlign != 0 {// Found magic, but misaligned. Keep searching. := ( + buildInfoAlign - 1) &^ (buildInfoAlign - 1)if > len() {// Corrupt object file: the remaining // count says there is more data, // but we didn't read it.return0, errNotGoExe } = [:]continue }// Good match!return + uint64(), nil } += }return0, errNotGoExe}func readData( exe, , uint64) ([]byte, error) { , := .DataReader()if != nil {returnnil, } , := saferio.ReadDataAt(, , 0)iflen() > 0 && == io.EOF { = nil }return , }func readDataInto( exe, uint64, []byte) (int, error) { , := .DataReader()if != nil {return0, } , := .ReadAt(, 0)if > 0 && == io.EOF { = nil }return , }// elfExe is the ELF implementation of the exe interface.type elfExe struct { f *elf.File}func ( *elfExe) ( uint64) (io.ReaderAt, error) {for , := range .f.Progs {if .Vaddr <= && <= .Vaddr+.Filesz-1 { := .Vaddr + .Filesz - returnio.NewSectionReader(, int64(-.Vaddr), int64()), nil } }returnnil, errUnrecognizedFormat}func ( *elfExe) () (uint64, uint64) {for , := range .f.Sections {if .Name == ".go.buildinfo" {return .Addr, .Size } }for , := range .f.Progs {if .Type == elf.PT_LOAD && .Flags&(elf.PF_X|elf.PF_W) == elf.PF_W {return .Vaddr, .Memsz } }return0, 0}// peExe is the PE (Windows Portable Executable) implementation of the exe interface.type peExe struct { f *pe.File}func ( *peExe) () uint64 {switch oh := .f.OptionalHeader.(type) {case *pe.OptionalHeader32:returnuint64(.ImageBase)case *pe.OptionalHeader64:return .ImageBase }return0}func ( *peExe) ( uint64) (io.ReaderAt, error) { -= .imageBase()for , := range .f.Sections {ifuint64(.VirtualAddress) <= && <= uint64(.VirtualAddress+.Size-1) { := uint64(.VirtualAddress+.Size) - returnio.NewSectionReader(, int64(-uint64(.VirtualAddress)), int64()), nil } }returnnil, errUnrecognizedFormat}func ( *peExe) () (uint64, uint64) {// Assume data is first writable section.const ( = 0x00000020 = 0x00000040 = 0x00000080 = 0x20000000 = 0x40000000 = 0x80000000 = 0x2000000 = 0x1000000 = 0x600000 )for , := range .f.Sections {if .VirtualAddress != 0 && .Size != 0 && .Characteristics&^ == || {returnuint64(.VirtualAddress) + .imageBase(), uint64(.VirtualSize) } }return0, 0}// machoExe is the Mach-O (Apple macOS/iOS) implementation of the exe interface.type machoExe struct { f *macho.File}func ( *machoExe) ( uint64) (io.ReaderAt, error) {for , := range .f.Loads { , := .(*macho.Segment)if ! {continue }if .Addr <= && <= .Addr+.Filesz-1 {if .Name == "__PAGEZERO" {continue } := .Addr + .Filesz - returnio.NewSectionReader(, int64(-.Addr), int64()), nil } }returnnil, errUnrecognizedFormat}func ( *machoExe) () (uint64, uint64) {// Look for section named "__go_buildinfo".for , := range .f.Sections {if .Name == "__go_buildinfo" {return .Addr, .Size } }// Try the first non-empty writable segment.const = 3for , := range .f.Loads { , := .(*macho.Segment)if && .Addr != 0 && .Filesz != 0 && .Prot == && .Maxprot == {return .Addr, .Memsz } }return0, 0}// xcoffExe is the XCOFF (AIX eXtended COFF) implementation of the exe interface.type xcoffExe struct { f *xcoff.File}func ( *xcoffExe) ( uint64) (io.ReaderAt, error) {for , := range .f.Sections {if .VirtualAddress <= && <= .VirtualAddress+.Size-1 { := .VirtualAddress + .Size - returnio.NewSectionReader(, int64(-.VirtualAddress), int64()), nil } }returnnil, errors.New("address not mapped")}func ( *xcoffExe) () (uint64, uint64) {if := .f.SectionByType(xcoff.STYP_DATA); != nil {return .VirtualAddress, .Size }return0, 0}// plan9objExe is the Plan 9 a.out implementation of the exe interface.type plan9objExe struct { f *plan9obj.File}func ( *plan9objExe) () (uint64, uint64) {if := .f.Section("data"); != nil {returnuint64(.Offset), uint64(.Size) }return0, 0}func ( *plan9objExe) ( uint64) (io.ReaderAt, error) {for , := range .f.Sections {ifuint64(.Offset) <= && <= uint64(.Offset+.Size-1) { := uint64(.Offset+.Size) - returnio.NewSectionReader(, int64(-uint64(.Offset)), int64()), nil } }returnnil, errors.New("address not mapped")}
The pages are generated with Goldsv0.7.3. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.