package buildinfo
import (
"bytes"
"debug/elf"
"debug/macho"
"debug/pe"
"debug/plan9obj"
"encoding/binary"
"errors"
"fmt"
"internal/saferio"
"internal/xcoff"
"io"
"io/fs"
"os"
"runtime/debug"
_ "unsafe"
)
type BuildInfo = debug .BuildInfo
var errUnrecognizedFormat = errors .New ("unrecognized file format" )
var errNotGoExe = errors .New ("not a Go executable" )
var buildInfoMagic = []byte ("\xff Go buildinf:" )
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 )
}
func Read (r io .ReaderAt ) (*BuildInfo , error ) {
vers , mod , err := readRawBuildInfo (r )
if err != nil {
return nil , err
}
bi , err := debug .ParseBuildInfo (mod )
if err != nil {
return nil , err
}
bi .GoVersion = vers
return bi , nil
}
type exe interface {
ReadData(addr, size uint64 ) ([]byte , error )
DataStart() (uint64 , uint64 )
}
func readRawBuildInfo(r io .ReaderAt ) (vers , mod string , err error ) {
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 ("\xCA\xFE\xBA\xBE" )) || bytes .HasPrefix (ident , []byte ("\xCA\xFE\xBA\xBF" )):
f , err := macho .NewFatFile (r )
if err != nil || len (f .Arches ) == 0 {
return "" , "" , errUnrecognizedFormat
}
x = &machoExe {f .Arches [0 ].File }
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 }
case hasPlan9Magic (ident ):
f , err := plan9obj .NewFile (r )
if err != nil {
return "" , "" , errUnrecognizedFormat
}
x = &plan9objExe {f }
default :
return "" , "" , errUnrecognizedFormat
}
dataAddr , dataSize := x .DataStart ()
if dataSize == 0 {
return "" , "" , errNotGoExe
}
data , err := x .ReadData (dataAddr , dataSize )
if err != nil {
return "" , "" , err
}
const (
buildInfoAlign = 16
buildInfoSize = 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 -1 ):]
}
ptrSize := int (data [14 ])
if data [15 ]&2 != 0 {
vers , data = decodeString (data [32 :])
mod , data = decodeString (data )
} else {
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 if ptrSize == 8 {
readPtr = bo .Uint64
} else {
return "" , "" , errNotGoExe
}
vers = readString (x , ptrSize , readPtr , readPtr (data [16 :]))
mod = readString (x , ptrSize , readPtr , readPtr (data [16 +ptrSize :]))
}
if vers == "" {
return "" , "" , errNotGoExe
}
if len (mod ) >= 33 && mod [len (mod )-17 ] == '\n' {
mod = mod [16 : len (mod )-16 ]
} else {
mod = ""
}
return vers , mod , nil
}
func hasPlan9Magic(magic []byte ) bool {
if len (magic ) >= 4 {
m := binary .BigEndian .Uint32 (magic )
switch m {
case plan9obj .Magic386 , plan9obj .MagicAMD64 , plan9obj .MagicARM :
return true
}
}
return false
}
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 :]
}
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 )
}
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
}
return saferio .ReadDataAt (prog , n , int64 (addr -prog .Vaddr ))
}
}
return nil , errUnrecognizedFormat
}
func (x *elfExe ) DataStart () (uint64 , uint64 ) {
for _ , s := range x .f .Sections {
if s .Name == ".go.buildinfo" {
return s .Addr , s .Size
}
}
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 , p .Memsz
}
}
return 0 , 0
}
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
}
return saferio .ReadDataAt (sect , n , int64 (addr -uint64 (sect .VirtualAddress )))
}
}
return nil , errUnrecognizedFormat
}
func (x *peExe ) DataStart () (uint64 , uint64 ) {
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 (), uint64 (sect .VirtualSize )
}
}
return 0 , 0
}
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
}
return saferio .ReadDataAt (seg , n , int64 (addr -seg .Addr ))
}
}
return nil , errUnrecognizedFormat
}
func (x *machoExe ) DataStart () (uint64 , uint64 ) {
for _ , sec := range x .f .Sections {
if sec .Name == "__go_buildinfo" {
return sec .Addr , sec .Size
}
}
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 , seg .Memsz
}
}
return 0 , 0
}
type xcoffExe struct {
f *xcoff .File
}
func (x *xcoffExe ) ReadData (addr , size uint64 ) ([]byte , error ) {
for _ , sect := range x .f .Sections {
if sect .VirtualAddress <= addr && addr <= sect .VirtualAddress +sect .Size -1 {
n := sect .VirtualAddress + sect .Size - addr
if n > size {
n = size
}
return saferio .ReadDataAt (sect , n , int64 (addr -sect .VirtualAddress ))
}
}
return nil , errors .New ("address not mapped" )
}
func (x *xcoffExe ) DataStart () (uint64 , uint64 ) {
if s := x .f .SectionByType (xcoff .STYP_DATA ); s != nil {
return s .VirtualAddress , s .Size
}
return 0 , 0
}
type plan9objExe struct {
f *plan9obj .File
}
func (x *plan9objExe ) DataStart () (uint64 , uint64 ) {
if s := x .f .Section ("data" ); s != nil {
return uint64 (s .Offset ), uint64 (s .Size )
}
return 0 , 0
}
func (x *plan9objExe ) ReadData (addr , size uint64 ) ([]byte , error ) {
for _ , sect := range x .f .Sections {
if uint64 (sect .Offset ) <= addr && addr <= uint64 (sect .Offset +sect .Size -1 ) {
n := uint64 (sect .Offset +sect .Size ) - addr
if n > size {
n = size
}
return saferio .ReadDataAt (sect , n , int64 (addr -uint64 (sect .Offset )))
}
}
return nil , errors .New ("address not mapped" )
}
The pages are generated with Golds v0.7.0-preview . (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 .