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