| package main |
| |
| // elfdiff compares two ELF files. Each one can be a standalone file or an archive (.a file) |
| // member. |
| import ( |
| "android/bazel/mkcompare" |
| "bytes" |
| "debug/elf" |
| "flag" |
| "fmt" |
| "io" |
| "os" |
| "sort" |
| "strconv" |
| "strings" |
| ) |
| |
| type myElf struct { |
| *elf.File |
| path string |
| sectionsByName map[string]*elf.Section |
| } |
| |
| func always(_ string) bool { |
| return true |
| } |
| |
| func processArgs() { |
| flag.Parse() |
| if len(flag.Args()) != 2 { |
| maybeQuit(fmt.Errorf("usage: %s REF-ELF OUR-ELF\n", os.Args[0])) |
| os.Exit(1) |
| } |
| } |
| |
| func maybeQuit(err error) { |
| if err == nil { |
| return |
| } |
| |
| fmt.Fprintln(os.Stderr, err) |
| os.Exit(1) |
| } |
| |
| func main() { |
| processArgs() |
| elfRef := elfRead(flag.Arg(0)) |
| elfOur := elfRead(flag.Arg(1)) |
| missing, common, extra := mkcompare.Classify(elfRef.sectionsByName, elfOur.sectionsByName, always) |
| var hasDiff bool |
| newDifference := func() { |
| if !hasDiff { |
| hasDiff = true |
| } |
| } |
| |
| if len(missing)+len(extra) > 0 { |
| newDifference() |
| } |
| if len(missing) > 0 { |
| sort.Strings(missing) |
| fmt.Print("Missing sections:\n ", strings.Join(missing, "\n "), "\n") |
| } |
| if len(extra) > 0 { |
| sort.Strings(extra) |
| fmt.Print("Extra sections:\n ", strings.Join(extra, "\n "), "\n") |
| } |
| commonDiff := false |
| newCommonDifference := func(format string, args ...interface{}) { |
| if !commonDiff { |
| fmt.Print("Sections that differ:\n") |
| commonDiff = true |
| } |
| newDifference() |
| fmt.Printf(format, args...) |
| } |
| sort.Strings(common) |
| for _, sname := range common { |
| sectionRef := elfRef.sectionsByName[sname] |
| sectionOur := elfOur.sectionsByName[sname] |
| refSize := int64(sectionRef.Size) |
| ourSize := int64(sectionOur.Size) |
| if refSize != ourSize { |
| newCommonDifference(" %s:%d%+d\n", sname, refSize, ourSize-refSize) |
| continue |
| } |
| dataOur, err := sectionOur.Data() |
| maybeQuit(err) |
| dataRef, err := sectionRef.Data() |
| maybeQuit(err) |
| if bytes.Compare(dataRef, dataOur) != 0 { |
| newCommonDifference(" %s:%d(data)\n", sname, refSize) |
| } |
| } |
| |
| if hasDiff { |
| os.Exit(1) |
| } |
| } |
| |
| const arMagic = "!<arch>\n" |
| const arExtendedEntry = "//" |
| |
| // elfRead returns ELF file reader for URI. If URI has <path>(<member>) format, |
| // <path> is an archive (usually an .a file) and <member> is an ELF file in it. |
| func elfRead(path string) *myElf { |
| var reader io.ReaderAt |
| var err error |
| n := strings.LastIndex(path, "(") |
| if n > 0 && strings.HasSuffix(path, ")") { |
| reader = newArchiveReader(path[0:n], path[n+1:len(path)-1]) |
| } else { |
| reader, err = os.Open(path) |
| maybeQuit(err) |
| } |
| res := &myElf{path: path} |
| res.File, err = elf.NewFile(reader) |
| maybeQuit(err) |
| |
| // Build ELF sections map. Only allocatable sections are considered. |
| res.sectionsByName = make(map[string]*elf.Section) |
| for _, s := range res.File.Sections { |
| if _, ok := res.sectionsByName[s.Name]; ok { |
| fmt.Fprintf(os.Stderr, "%s: duplicate section %s, ignoring\n", res.path, s.Name) |
| continue |
| } |
| if s.Flags&elf.SHF_ALLOC != 0 && s.Type != elf.SHT_NOBITS { |
| res.sectionsByName[s.Name] = s |
| } |
| } |
| return res |
| } |
| |
| type memberHeader []byte |
| |
| const headerSize = 60 |
| |
| // memberHeader represents a member in an archive. It implements os.ReaderAt interface |
| // so it can be passed to elf.NewFile |
| type memberReader struct { |
| file *os.File |
| start int64 |
| size int64 |
| } |
| |
| func (m memberReader) ReadAt(p []byte, off int64) (n int, err error) { |
| nToRead := int64(len(p)) |
| nHas := m.size - off |
| if nHas <= 0 { |
| return 0, io.EOF |
| } |
| if nToRead > nHas { |
| nToRead = nHas |
| } |
| return m.file.ReadAt(p[0:nToRead], m.start+off) |
| } |
| |
| func (h memberHeader) memberSize() int64 { |
| n, err := strconv.ParseInt(strings.TrimSpace(string(h[48:58])), 10, 64) |
| maybeQuit(err) |
| return (n + 1) & -2 // The size is always an even number |
| } |
| |
| // newArchiveReader returns a reader for an archive member. |
| // The format of the ar archive is sort of documented in Wikipedia: |
| // https://en.wikipedia.org/wiki/Ar_(Unix) |
| func newArchiveReader(path string, member string) io.ReaderAt { |
| f, err := os.Open(path) |
| maybeQuit(err) |
| fStat, err := f.Stat() |
| maybeQuit(err) |
| fileSize := fStat.Size() |
| |
| var nextHeaderPos int64 = 8 |
| var contentPos int64 |
| var header memberHeader = make([]byte, headerSize) |
| |
| // fill the buffer, reading from given position. |
| readFully := func(buf []byte, at int64) { |
| n, err := f.ReadAt(buf, at) |
| maybeQuit(err) |
| if n < len(buf) { |
| maybeQuit(fmt.Errorf("%s is corrupt, read %d bytes instead of %d\n", path, n, len(buf))) |
| } |
| } |
| // Read the header, update contents and next header pointers |
| readHeader := func() { |
| readFully(header, nextHeaderPos) |
| contentPos = nextHeaderPos + headerSize |
| nextHeaderPos = contentPos + header.memberSize() |
| } |
| |
| // Read the file header |
| buf := make([]byte, len(arMagic)) |
| readFully(buf, 0) |
| if bytes.Compare([]byte(arMagic), buf) != 0 { |
| maybeQuit(fmt.Errorf("%s is not an ar archive\n", path)) |
| } |
| |
| entry := []byte(member + "/") // `/` is member name sentinel |
| if len(entry) <= 16 { |
| // the name fits into a section header, so just scan the sections. |
| for nextHeaderPos < fileSize { |
| readHeader() |
| if bytes.Compare(entry, header[0:len(entry)]) == 0 { |
| return &memberReader{f, contentPos, header.memberSize()} |
| } |
| } |
| } else { |
| // If section's name is `/` followed by digits, these digits are an offset to |
| // its real name in the 'extended names' section. |
| // The name of the extended names section is `//`, and it should precede the |
| // sections with longer names. |
| var extendedNames []byte |
| for nextHeaderPos < fileSize { |
| readHeader() |
| if bytes.Compare(header[0:2], []byte(arExtendedEntry)) == 0 { |
| extendedNames = make([]byte, header.memberSize()) |
| readFully(extendedNames, contentPos) |
| } else if bytes.Compare(header[0:1], []byte("/")) != 0 { |
| continue |
| } |
| if off, err := strconv.ParseInt(strings.TrimSpace(string(header[1:16])), 10, 64); err == nil { |
| // A section with extended name. |
| if extendedNames == nil { |
| maybeQuit(fmt.Errorf("%s: extended names entry is missing in archive\n", path)) |
| } |
| if off+int64(len(entry)) <= int64(len(extendedNames)) && |
| bytes.Compare(entry, extendedNames[off:off+int64(len(entry))]) == 0 { |
| return &memberReader{f, contentPos, header.memberSize()} |
| } |
| } |
| } |
| } |
| maybeQuit(fmt.Errorf("%s: no such member %s", path, member)) |
| return nil |
| } |