github.com/grailbio/base@v0.0.11/fatbin/fatbin.go (about) 1 // Copyright 2019 GRAIL, Inc. All rights reserved. 2 // Use of this source code is governed by the Apache 2.0 3 // license that can be found in the LICENSE file. 4 5 // Package fatbin implements a simple fat binary format, and provides 6 // facilities for creating fat binaries and accessing its variants. 7 // 8 // A fatbin binary is a base binary with a zip archive appended, 9 // containing copies of the same binary targeted to different 10 // GOOS/GOARCH combinations. The zip archive contains one entry for 11 // each supported architecture and operating system combination. 12 // At the end of a fatbin image is a footer, storing the offset of the 13 // zip archive as well as a magic constant used to identify fatbin 14 // images: 15 // 16 // [8]offset[4]magic[8]checksum 17 // 18 // The checksum is a 64-bit xxhash checksum of the offset and 19 // magic fields. The magic value is 0x5758ba2c. 20 package fatbin 21 22 import ( 23 "archive/zip" 24 "debug/elf" 25 "debug/macho" 26 "errors" 27 "fmt" 28 "io" 29 "io/ioutil" 30 "os" 31 "runtime" 32 "strings" 33 "sync" 34 35 "github.com/grailbio/base/log" 36 ) 37 38 var ( 39 selfOnce sync.Once 40 self *Reader 41 selfErr error 42 ) 43 44 var ( 45 // ErrNoSuchImage is returned when the fatbin does not contain an 46 // image for the requested GOOS/GOARCH combination. 47 ErrNoSuchImage = errors.New("image does not exist") 48 // ErrCorruptedImage is returned when the fatbin image has been 49 // corrupted. 50 ErrCorruptedImage = errors.New("corrupted fatbin image") 51 ) 52 53 // Info provides information for an embedded binary. 54 type Info struct { 55 Goos, Goarch string 56 Size int64 57 } 58 59 func (info Info) String() string { 60 return fmt.Sprintf("%s/%s: %d", info.Goos, info.Goarch, info.Size) 61 } 62 63 // Reader reads images from a fatbin. 64 type Reader struct { 65 self io.ReaderAt 66 goos, goarch string 67 offset int64 68 69 z *zip.Reader 70 } 71 72 // Self reads the currently executing binary image as a fatbin and 73 // returns a reader to it. 74 func Self() (*Reader, error) { 75 selfOnce.Do(func() { 76 filename, err := os.Executable() 77 if err != nil { 78 selfErr = err 79 return 80 } 81 f, err := os.Open(filename) 82 if err != nil { 83 selfErr = err 84 return 85 } 86 info, err := f.Stat() 87 if err != nil { 88 selfErr = err 89 return 90 } 91 _, _, offset, err := Sniff(f, info.Size()) 92 if err != nil { 93 selfErr = err 94 return 95 } 96 self, selfErr = NewReader(f, offset, info.Size(), runtime.GOOS, runtime.GOARCH) 97 }) 98 return self, selfErr 99 } 100 101 // OpenFile parses the provided ReaderAt with the provided size. The 102 // file's contents is parsed to determine the offset of the fatbin's 103 // archive. OpenFile returns an error if the file is not a fatbin. 104 func OpenFile(r io.ReaderAt, size int64) (*Reader, error) { 105 goos, goarch, offset, err := Sniff(r, size) 106 if err != nil { 107 return nil, err 108 } 109 return NewReader(r, offset, size, goos, goarch) 110 } 111 112 // NewReader returns a new fatbin reader from the provided reader. 113 // The offset should be the offset of the fatbin archive; size is the 114 // total file size. The provided goos and goarch are that of the base 115 // binary. 116 func NewReader(r io.ReaderAt, offset, size int64, goos, goarch string) (*Reader, error) { 117 rd := &Reader{ 118 self: io.NewSectionReader(r, 0, offset), 119 goos: goos, 120 goarch: goarch, 121 offset: offset, 122 } 123 if offset == size { 124 return rd, nil 125 } 126 var err error 127 rd.z, err = zip.NewReader(io.NewSectionReader(r, offset, size-offset), size-offset) 128 if err != nil { 129 return nil, err 130 } 131 return rd, nil 132 } 133 134 // GOOS returns the base binary GOOS. 135 func (r *Reader) GOOS() string { return r.goos } 136 137 // GOARCH returns the base binary GOARCH. 138 func (r *Reader) GOARCH() string { return r.goarch } 139 140 // List returns information about embedded binary images. 141 func (r *Reader) List() []Info { 142 infos := make([]Info, len(r.z.File)) 143 for i, f := range r.z.File { 144 elems := strings.SplitN(f.Name, "/", 2) 145 if len(elems) != 2 { 146 log.Error.Printf("invalid fatbin: found name %s", f.Name) 147 continue 148 } 149 infos[i] = Info{ 150 Goos: elems[0], 151 Goarch: elems[1], 152 Size: int64(f.UncompressedSize64), 153 } 154 } 155 return infos 156 } 157 158 // Open returns a ReadCloser from which the binary with the provided 159 // goos and goarch can be read. Open returns ErrNoSuchImage if the 160 // fatbin does not contain an image for the requested goos and 161 // goarch. 162 func (r *Reader) Open(goos, goarch string) (io.ReadCloser, error) { 163 if goos == r.goos && goarch == r.goarch { 164 sr := io.NewSectionReader(r.self, 0, 1<<63-1) 165 return ioutil.NopCloser(sr), nil 166 } 167 168 if r.z == nil { 169 return nil, ErrNoSuchImage 170 } 171 172 look := goos + "/" + goarch 173 for _, f := range r.z.File { 174 if f.Name == look { 175 return f.Open() 176 } 177 } 178 return nil, ErrNoSuchImage 179 } 180 181 // Stat returns the information for the image identified by the 182 // provided GOOS and GOARCH. It returns a boolean indicating 183 // whether the requested image was found. 184 func (r *Reader) Stat(goos, goarch string) (info Info, ok bool) { 185 info.Goos = goos 186 info.Goarch = goarch 187 if goos == r.goos && goarch == r.goarch { 188 info.Size = r.offset 189 ok = true 190 return 191 } 192 look := goos + "/" + goarch 193 for _, f := range r.z.File { 194 if f.Name == look { 195 info.Size = int64(f.UncompressedSize64) 196 ok = true 197 return 198 } 199 } 200 return 201 } 202 203 func sectionEndAligned(s *elf.Section) int64 { 204 return int64(((s.Offset + s.FileSize) + (s.Addralign - 1)) & -s.Addralign) 205 } 206 207 // Sniff sniffs a binary's goos, goarch, and fatbin offset. Sniff returns errors 208 // returned by the provided reader, or ErrCorruptedImage if the binary is identified 209 // as a fatbin image with a checksum mismatch. 210 func Sniff(r io.ReaderAt, size int64) (goos, goarch string, offset int64, err error) { 211 for _, s := range sniffers { 212 var ok bool 213 goos, goarch, ok = s(r) 214 if ok { 215 break 216 } 217 } 218 if goos == "" { 219 goos = "unknown" 220 } 221 if goarch == "" { 222 goarch = "unknown" 223 } 224 offset, err = readFooter(r, size) 225 if err == errNoFooter { 226 err = nil 227 offset = size 228 } 229 return 230 } 231 232 type sniffer func(r io.ReaderAt) (goos, goarch string, ok bool) 233 234 var sniffers = []sniffer{sniffElf, sniffMacho} 235 236 func sniffElf(r io.ReaderAt) (goos, goarch string, ok bool) { 237 file, err := elf.NewFile(r) 238 if err != nil { 239 return 240 } 241 ok = true 242 switch file.OSABI { 243 default: 244 goos = "unknown" 245 case elf.ELFOSABI_NONE, elf.ELFOSABI_LINUX: 246 goos = "linux" 247 case elf.ELFOSABI_NETBSD: 248 goos = "netbsd" 249 case elf.ELFOSABI_OPENBSD: 250 goos = "openbsd" 251 } 252 switch file.Machine { 253 default: 254 goarch = "unknown" 255 case elf.EM_386: 256 goarch = "386" 257 case elf.EM_X86_64: 258 goarch = "amd64" 259 case elf.EM_ARM: 260 goarch = "arm" 261 case elf.EM_AARCH64: 262 goarch = "arm64" 263 } 264 return 265 } 266 267 func sniffMacho(r io.ReaderAt) (goos, goarch string, ok bool) { 268 file, err := macho.NewFile(r) 269 if err != nil { 270 return 271 } 272 ok = true 273 // We assume mach-o is only used in Darwin. This is not exposed 274 // by the mach-o files. 275 goos = "darwin" 276 switch file.Cpu { 277 default: 278 goarch = "unknown" 279 case macho.Cpu386: 280 goarch = "386" 281 case macho.CpuAmd64: 282 goarch = "amd64" 283 case macho.CpuArm: 284 goarch = "arm" 285 case macho.CpuArm64: 286 goarch = "arm64" 287 case macho.CpuPpc: 288 goarch = "ppc" 289 case macho.CpuPpc64: 290 goarch = "ppc64" 291 } 292 return 293 }