| // 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 binscan contains methods for parsing Go binary files for the purpose |
| // of extracting module dependency and symbol table information. |
| package binscan |
| |
| // Code in this package is dervied from src/cmd/go/internal/version/version.go |
| // and cmd/go/internal/version/exe.go. |
| |
| import ( |
| "bytes" |
| "debug/gosym" |
| "encoding/binary" |
| "errors" |
| "fmt" |
| "net/url" |
| "runtime/debug" |
| "strings" |
| ) |
| |
| // buildInfoMagic, findVers, and readString are copied from |
| // cmd/go/internal/version |
| |
| // 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). |
| var buildInfoMagic = []byte("\xff Go buildinf:") |
| |
| // findVers finds and returns the Go version and module version information |
| // in the executable x. |
| func findVers(x exe) string { |
| // Read the first 64kB of text to find the build info blob. |
| text := x.DataStart() |
| data, err := x.ReadData(text, 64*1024) |
| if err != nil { |
| return "" |
| } |
| for ; !bytes.HasPrefix(data, buildInfoMagic); data = data[32:] { |
| if len(data) < 32 { |
| return "" |
| } |
| } |
| |
| // Decode the blob. |
| 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 "" |
| } |
| mod := readString(x, ptrSize, readPtr, readPtr(data[16+ptrSize:])) |
| if len(mod) >= 33 && mod[len(mod)-17] == '\n' { |
| // Strip module framing. |
| mod = mod[16 : len(mod)-16] |
| } else { |
| mod = "" |
| } |
| return mod |
| } |
| |
| // 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) |
| } |
| |
| // readBuildInfo is copied from runtime/debug |
| func readBuildInfo(data string) (*debug.BuildInfo, bool) { |
| if len(data) == 0 { |
| return nil, false |
| } |
| |
| const ( |
| pathLine = "path\t" |
| modLine = "mod\t" |
| depLine = "dep\t" |
| repLine = "=>\t" |
| ) |
| |
| readEntryFirstLine := func(elem []string) (debug.Module, bool) { |
| if len(elem) != 2 && len(elem) != 3 { |
| return debug.Module{}, false |
| } |
| sum := "" |
| if len(elem) == 3 { |
| sum = elem[2] |
| } |
| return debug.Module{ |
| Path: elem[0], |
| Version: elem[1], |
| Sum: sum, |
| }, true |
| } |
| |
| var ( |
| info = &debug.BuildInfo{} |
| last *debug.Module |
| line string |
| ok bool |
| ) |
| // Reverse of cmd/go/internal/modload.PackageBuildInfo |
| for len(data) > 0 { |
| i := strings.IndexByte(data, '\n') |
| if i < 0 { |
| break |
| } |
| line, data = data[:i], data[i+1:] |
| switch { |
| case strings.HasPrefix(line, pathLine): |
| elem := line[len(pathLine):] |
| info.Path = elem |
| case strings.HasPrefix(line, modLine): |
| elem := strings.Split(line[len(modLine):], "\t") |
| last = &info.Main |
| *last, ok = readEntryFirstLine(elem) |
| if !ok { |
| return nil, false |
| } |
| case strings.HasPrefix(line, depLine): |
| elem := strings.Split(line[len(depLine):], "\t") |
| last = new(debug.Module) |
| info.Deps = append(info.Deps, last) |
| *last, ok = readEntryFirstLine(elem) |
| if !ok { |
| return nil, false |
| } |
| case strings.HasPrefix(line, repLine): |
| elem := strings.Split(line[len(repLine):], "\t") |
| if len(elem) != 3 { |
| return nil, false |
| } |
| if last == nil { |
| return nil, false |
| } |
| last.Replace = &debug.Module{ |
| Path: elem[0], |
| Version: elem[1], |
| Sum: elem[2], |
| } |
| last = nil |
| } |
| } |
| return info, true |
| } |
| |
| // ExtractPackagesAndSymbols extracts the symbols, packages, and their associated module versions |
| // from a Go binary. Stripped binaries are not supported. |
| func ExtractPackagesAndSymbols(binPath string) (map[string]string, map[string][]string, error) { |
| x, err := openExe(binPath) |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| mod := findVers(x) |
| |
| bi, ok := readBuildInfo(mod) |
| if !ok { |
| return nil, nil, err |
| } |
| |
| deps := map[string]string{} |
| for _, dep := range bi.Deps { |
| if dep == nil { |
| continue |
| } |
| if dep.Replace != nil { |
| deps[dep.Replace.Path] = dep.Replace.Version |
| continue |
| } |
| deps[dep.Path] = dep.Version |
| } |
| |
| pclntab, textOffset := x.PCLNTab() |
| lineTab := gosym.NewLineTable(pclntab, textOffset) |
| if lineTab == nil { |
| return nil, nil, errors.New("invalid line table") |
| } |
| tab, err := gosym.NewTable(nil, lineTab) |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| packageSymbols := map[string][]string{} |
| for _, f := range tab.Funcs { |
| if f.Func == nil { |
| continue |
| } |
| symName := f.Func.BaseName() |
| if r := f.Func.ReceiverName(); r != "" { |
| if strings.HasPrefix(r, "(*") { |
| r = strings.Trim(r, "(*)") |
| } |
| symName = fmt.Sprintf("%s.%s", r, symName) |
| } |
| |
| pkgName := f.Func.PackageName() |
| if pkgName == "" { |
| continue |
| } |
| pkgName, err := url.PathUnescape(pkgName) |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| packageSymbols[pkgName] = append(packageSymbols[pkgName], symName) |
| } |
| |
| versionedPackages := map[string]string{} |
| // TODO: this is rather inefficient, but probably fine for most programs |
| for pkg := range packageSymbols { |
| for mod, version := range deps { |
| if strings.HasPrefix(pkg, mod) { |
| versionedPackages[pkg] = version |
| } |
| } |
| } |
| return versionedPackages, packageSymbols, nil |
| } |