1package main 2 3// elfdiff compares two ELF files. Each one can be a standalone file or an archive (.a file) 4// member. 5import ( 6 "android/bazel/mkcompare" 7 "bytes" 8 "debug/elf" 9 "flag" 10 "fmt" 11 "io" 12 "os" 13 "sort" 14 "strconv" 15 "strings" 16) 17 18type myElf struct { 19 *elf.File 20 path string 21 sectionsByName map[string]*elf.Section 22} 23 24func always(_ string) bool { 25 return true 26} 27 28func processArgs() { 29 flag.Parse() 30 if len(flag.Args()) != 2 { 31 maybeQuit(fmt.Errorf("usage: %s REF-ELF OUR-ELF\n", os.Args[0])) 32 os.Exit(1) 33 } 34} 35 36func maybeQuit(err error) { 37 if err == nil { 38 return 39 } 40 41 fmt.Fprintln(os.Stderr, err) 42 os.Exit(1) 43} 44 45func main() { 46 processArgs() 47 elfRef := elfRead(flag.Arg(0)) 48 elfOur := elfRead(flag.Arg(1)) 49 missing, common, extra := mkcompare.Classify(elfRef.sectionsByName, elfOur.sectionsByName, always) 50 var hasDiff bool 51 newDifference := func() { 52 if !hasDiff { 53 hasDiff = true 54 } 55 } 56 57 if len(missing)+len(extra) > 0 { 58 newDifference() 59 } 60 if len(missing) > 0 { 61 sort.Strings(missing) 62 fmt.Print("Missing sections:\n ", strings.Join(missing, "\n "), "\n") 63 } 64 if len(extra) > 0 { 65 sort.Strings(extra) 66 fmt.Print("Extra sections:\n ", strings.Join(extra, "\n "), "\n") 67 } 68 commonDiff := false 69 newCommonDifference := func(format string, args ...interface{}) { 70 if !commonDiff { 71 fmt.Print("Sections that differ:\n") 72 commonDiff = true 73 } 74 newDifference() 75 fmt.Printf(format, args...) 76 } 77 sort.Strings(common) 78 for _, sname := range common { 79 sectionRef := elfRef.sectionsByName[sname] 80 sectionOur := elfOur.sectionsByName[sname] 81 refSize := int64(sectionRef.Size) 82 ourSize := int64(sectionOur.Size) 83 if refSize != ourSize { 84 newCommonDifference(" %s:%d%+d\n", sname, refSize, ourSize-refSize) 85 continue 86 } 87 dataOur, err := sectionOur.Data() 88 maybeQuit(err) 89 dataRef, err := sectionRef.Data() 90 maybeQuit(err) 91 if bytes.Compare(dataRef, dataOur) != 0 { 92 newCommonDifference(" %s:%d(data)\n", sname, refSize) 93 } 94 } 95 96 if hasDiff { 97 os.Exit(1) 98 } 99} 100 101const arMagic = "!<arch>\n" 102const arExtendedEntry = "//" 103 104// elfRead returns ELF file reader for URI. If URI has <path>(<member>) format, 105// <path> is an archive (usually an .a file) and <member> is an ELF file in it. 106func elfRead(path string) *myElf { 107 var reader io.ReaderAt 108 var err error 109 n := strings.LastIndex(path, "(") 110 if n > 0 && strings.HasSuffix(path, ")") { 111 reader = newArchiveReader(path[0:n], path[n+1:len(path)-1]) 112 } else { 113 reader, err = os.Open(path) 114 maybeQuit(err) 115 } 116 res := &myElf{path: path} 117 res.File, err = elf.NewFile(reader) 118 maybeQuit(err) 119 120 // Build ELF sections map. Only allocatable sections are considered. 121 res.sectionsByName = make(map[string]*elf.Section) 122 for _, s := range res.File.Sections { 123 if _, ok := res.sectionsByName[s.Name]; ok { 124 fmt.Fprintf(os.Stderr, "%s: duplicate section %s, ignoring\n", res.path, s.Name) 125 continue 126 } 127 if s.Flags&elf.SHF_ALLOC != 0 && s.Type != elf.SHT_NOBITS { 128 res.sectionsByName[s.Name] = s 129 } 130 } 131 return res 132} 133 134type memberHeader []byte 135 136const headerSize = 60 137 138// memberHeader represents a member in an archive. It implements os.ReaderAt interface 139// so it can be passed to elf.NewFile 140type memberReader struct { 141 file *os.File 142 start int64 143 size int64 144} 145 146func (m memberReader) ReadAt(p []byte, off int64) (n int, err error) { 147 nToRead := int64(len(p)) 148 nHas := m.size - off 149 if nHas <= 0 { 150 return 0, io.EOF 151 } 152 if nToRead > nHas { 153 nToRead = nHas 154 } 155 return m.file.ReadAt(p[0:nToRead], m.start+off) 156} 157 158func (h memberHeader) memberSize() int64 { 159 n, err := strconv.ParseInt(strings.TrimSpace(string(h[48:58])), 10, 64) 160 maybeQuit(err) 161 return (n + 1) & -2 // The size is always an even number 162} 163 164// newArchiveReader returns a reader for an archive member. 165// The format of the ar archive is sort of documented in Wikipedia: 166// https://en.wikipedia.org/wiki/Ar_(Unix) 167func newArchiveReader(path string, member string) io.ReaderAt { 168 f, err := os.Open(path) 169 maybeQuit(err) 170 fStat, err := f.Stat() 171 maybeQuit(err) 172 fileSize := fStat.Size() 173 174 var nextHeaderPos int64 = 8 175 var contentPos int64 176 var header memberHeader = make([]byte, headerSize) 177 178 // fill the buffer, reading from given position. 179 readFully := func(buf []byte, at int64) { 180 n, err := f.ReadAt(buf, at) 181 maybeQuit(err) 182 if n < len(buf) { 183 maybeQuit(fmt.Errorf("%s is corrupt, read %d bytes instead of %d\n", path, n, len(buf))) 184 } 185 } 186 // Read the header, update contents and next header pointers 187 readHeader := func() { 188 readFully(header, nextHeaderPos) 189 contentPos = nextHeaderPos + headerSize 190 nextHeaderPos = contentPos + header.memberSize() 191 } 192 193 // Read the file header 194 buf := make([]byte, len(arMagic)) 195 readFully(buf, 0) 196 if bytes.Compare([]byte(arMagic), buf) != 0 { 197 maybeQuit(fmt.Errorf("%s is not an ar archive\n", path)) 198 } 199 200 entry := []byte(member + "/") // `/` is member name sentinel 201 if len(entry) <= 16 { 202 // the name fits into a section header, so just scan the sections. 203 for nextHeaderPos < fileSize { 204 readHeader() 205 if bytes.Compare(entry, header[0:len(entry)]) == 0 { 206 return &memberReader{f, contentPos, header.memberSize()} 207 } 208 } 209 } else { 210 // If section's name is `/` followed by digits, these digits are an offset to 211 // its real name in the 'extended names' section. 212 // The name of the extended names section is `//`, and it should precede the 213 // sections with longer names. 214 var extendedNames []byte 215 for nextHeaderPos < fileSize { 216 readHeader() 217 if bytes.Compare(header[0:2], []byte(arExtendedEntry)) == 0 { 218 extendedNames = make([]byte, header.memberSize()) 219 readFully(extendedNames, contentPos) 220 } else if bytes.Compare(header[0:1], []byte("/")) != 0 { 221 continue 222 } 223 if off, err := strconv.ParseInt(strings.TrimSpace(string(header[1:16])), 10, 64); err == nil { 224 // A section with extended name. 225 if extendedNames == nil { 226 maybeQuit(fmt.Errorf("%s: extended names entry is missing in archive\n", path)) 227 } 228 if off+int64(len(entry)) <= int64(len(extendedNames)) && 229 bytes.Compare(entry, extendedNames[off:off+int64(len(entry))]) == 0 { 230 return &memberReader{f, contentPos, header.memberSize()} 231 } 232 } 233 } 234 } 235 maybeQuit(fmt.Errorf("%s: no such member %s", path, member)) 236 return nil 237} 238