blob: 6b455829e272d57f386511dcf2f1626808d3bf0d [file] [log] [blame]
Han Shen3dc2a882014-01-07 16:42:01 -08001#!/usr/bin/python
2"""Diff 2 chromiumos images by comparing each elf file.
3
4 The script diffs every *ELF* files by dissembling every *executable* section,
5 which means it is not a FULL elf differ.
6
7 A simple usage example -
8 chromiumos_image_diff.py --image1 image-path-1 --image2 image-path-2
9
10 Note that image path should be inside the chroot, if not (ie, image is
11 downloaded from web), please specify a chromiumos checkout via
12 "--chromeos_root".
13
14 And this script should be executed outside chroot.
15"""
16
17__author__ = 'shenhan@google.com (Han Shen)'
18
19import argparse
20import os
21import re
22import sys
23import tempfile
24
25import image_chromeos
26from utils import command_executer
27from utils import logger
28from utils import misc
29
30
31class CrosImage(object):
32 """A cros image object."""
33
34 def __init__(self, image, chromeos_root, no_unmount):
35 self.image = image
36 self.chromeos_root = chromeos_root
37 self.mounted = False
38 self._ce = command_executer.GetCommandExecuter()
39 self.logger = logger.GetLogger()
40 self.elf_files = []
41 self.no_unmount = no_unmount
42
43 def MountImage(self, mount_basename):
44 """Mount/unpack the image."""
45
46 if mount_basename:
47 self.rootfs = '/tmp/{0}.rootfs'.format(mount_basename)
48 self.stateful = '/tmp/{0}.stateful'.format(mount_basename)
49 self.unmount_script = '/tmp/{0}.unmount.sh'.format(mount_basename)
50 else:
51 self.rootfs = tempfile.mkdtemp(suffix='.rootfs',
52 prefix='chromiumos_image_diff')
53 ## rootfs is like /tmp/tmpxyz012.rootfs.
54 match = re.match(r'^(.*)\.rootfs$', self.rootfs)
55 basename = match.group(1)
56 self.stateful = basename + '.stateful'
57 os.mkdir(self.stateful)
58 self.unmount_script = '{0}.unmount.sh'.format(basename)
59
60 self.logger.LogOutput('Mounting "{0}" onto "{1}" and "{2}"'.format(
61 self.image, self.rootfs, self.stateful))
62 ## First of all creating an unmount image
63 self.CreateUnmountScript()
64 command = image_chromeos.GetImageMountCommand(
65 self.chromeos_root, self.image, self.rootfs, self.stateful)
66 rv = self._ce.RunCommand(
67 command, return_output=False, print_to_console=True)
68 self.mounted = (rv == 0)
69 if not self.mounted:
70 self.logger.LogError('Failed to mount "{0}" onto "{1}" and "{2}".'.format(
71 self.image, self.rootfs, self.stateful))
72 return self.mounted
73
74 def CreateUnmountScript(self):
75 command = ('sudo umount {r}/usr/local {r}/usr/share/oem '
76 '{r}/var {r}/mnt/stateful_partition {r}; sudo umount {s} ; '
77 'rmdir {r} ; rmdir {s}\n').format(r=self.rootfs, s=self.stateful)
78 f = open(self.unmount_script, 'w')
79 f.write(command)
80 f.close()
81 self._ce.RunCommand('chmod +x {}'.format(self.unmount_script),
82 print_to_console=False, return_output=False)
83 self.logger.LogOutput('Created an unmount script - "{0}"'.format(
84 self.unmount_script))
85
86 def UnmountImage(self):
87 """Unmount the image and delete mount point."""
88
89 self.logger.LogOutput('Unmounting image "{0}" from "{1}" and "{2}"'.format(
90 self.image, self.rootfs, self.stateful))
91 if self.mounted:
92 command = 'bash "{0}"'.format(self.unmount_script)
93 if self.no_unmount:
94 self.logger.LogOutput(('Please unmount manually - \n'
95 '\t bash "{0}"'.format(self.unmount_script)))
96 else:
97 if self._ce.RunCommand(
98 command, return_output=False, print_to_console=True) == 0:
99 self._ce.RunCommand('rm {0}'.format(self.unmount_script))
100 self.mounted = False
101 self.rootfs = None
102 self.stateful = None
103 self.unmount_script = None
104
105 return not self.mounted
106
107 def FindElfFiles(self):
108 """Find all elf files for the image.
109
110 Returns:
111 Always true
112 """
113
114 self.logger.LogOutput('Finding all elf files in "{0}" ...'.format(
115 self.rootfs))
116 # Note '\;' must be prefixed by 'r'.
117 command = ('find "{0}" -type f -exec '
118 'bash -c \'file -b "{{}}" | grep -q "ELF"\'' r' \; '
119 r'-exec echo "{{}}" \;').format(self.rootfs)
120 self.logger.LogCmd(command)
121 _, out, _ = self._ce.RunCommand(
122 command, return_output=True, print_to_console=False)
123 self.elf_files = out.splitlines()
124 self.logger.LogOutput(
125 'Total {0} elf files found.'.format(len(self.elf_files)))
126 return True
127
128
129class ImageComparator(object):
130 """A class that wraps comparsion actions."""
131
132 def __init__(self, images, diff_file):
133 self.images = images
134 self.logger = logger.GetLogger()
135 self.diff_file = diff_file
136 self.tempf1 = None
137 self.tempf2 = None
138
139 def Cleanup(self):
140 if self.tempf1 and self.tempf2:
141 command_executer.GetCommandExecuter().RunCommand(
142 'rm {0} {1}'.format(self.tempf1, self.tempf2))
143 logger.GetLogger('Removed "{0}" and "{1}".'.format(
144 self.tempf1, self.tempf2))
145
146 def CheckElfFileSetEquality(self):
147 """Checking whether images have exactly number of elf files."""
148
149 self.logger.LogOutput('Checking elf file equality ...')
150 i1 = self.images[0]
151 i2 = self.images[1]
152 t1 = i1.rootfs + '/'
153 elfset1 = set([e.replace(t1, '') for e in i1.elf_files])
154 t2 = i2.rootfs + '/'
155 elfset2 = set([e.replace(t2, '') for e in i2.elf_files])
156 dif1 = elfset1.difference(elfset2)
157 msg = None
158 if dif1:
159 msg = 'The following files are not in "{image}" - "{rootfs}":\n'.format(
160 image=i2.image, rootfs=i2.rootfs)
161 for d in dif1:
162 msg += '\t' + d + '\n'
163 dif2 = elfset2.difference(elfset1)
164 if dif2:
165 msg = 'The following files are not in "{image}" - "{rootfs}":\n'.format(
166 image=i1.image, rootfs=i1.rootfs)
167 for d in dif2:
168 msg += '\t' + d + '\n'
169 if msg:
170 self.logger.LogError(msg)
171 return False
172 return True
173
174 def CompareImages(self):
175 """Do the comparsion work."""
176
177 if not self.CheckElfFileSetEquality():
178 return False
179
180 mismatch_list = []
181 match_count = 0
182 i1 = self.images[0]
183 i2 = self.images[1]
184 self.logger.LogOutput('Start comparing {0} elf file by file ...'.format(
185 len(i1.elf_files)))
186 ## Note - i1.elf_files and i2.elf_files have exactly the same entries here.
187
188 ## Create 2 temp files to be used for all disassembed files.
189 handle, self.tempf1 = tempfile.mkstemp()
190 os.close(handle) # We do not need the handle
191 handle, self.tempf2 = tempfile.mkstemp()
192 os.close(handle)
193
194 cmde = command_executer.GetCommandExecuter()
195 for elf1 in i1.elf_files:
196 tmp_rootfs = i1.rootfs + '/'
197 f1 = elf1.replace(tmp_rootfs, '')
198 full_path1 = elf1
199 full_path2 = elf1.replace(i1.rootfs, i2.rootfs)
200
201 if full_path1 == full_path2:
202 self.logger.LogError(
203 'Error: We\'re comparing the SAME file - {0}'.format(f1))
204 continue
205
206 command = ('objdump -d "{f1}" > {tempf1} ; '
207 'objdump -d "{f2}" > {tempf2} ; '
208 # Remove path string inside the dissemble
209 'sed -i \'s!{rootfs1}!!g\' {tempf1} ; '
210 'sed -i \'s!{rootfs2}!!g\' {tempf2} ; '
211 'diff {tempf1} {tempf2} 1>/dev/null 2>&1').format(
212 f1=full_path1, f2=full_path2,
213 rootfs1=i1.rootfs, rootfs2=i2.rootfs,
214 tempf1=self.tempf1, tempf2=self.tempf2)
215 ret = cmde.RunCommand(command, return_output=False,
216 print_to_console=False)
217 if ret != 0:
218 self.logger.LogOutput('*** Not match - "{0}" "{1}"'.format(
219 full_path1, full_path2))
220 mismatch_list.append(f1)
221 if self.diff_file:
222 command = (
223 'echo "Diffs of disassemble of \"{f1}\" and \"{f2}\"" '
224 '>> {diff_file} ; diff {tempf1} {tempf2} '
225 '>> {diff_file}').format(
226 f1=full_path1, f2=full_path2, diff_file=self.diff_file,
227 tempf1=self.tempf1, tempf2=self.tempf2)
228 cmde.RunCommand(command, return_output=False, print_to_console=False)
229 else:
230 match_count += 1
231 ## End of comparing every elf files.
232
233 if not mismatch_list:
234 self.logger.LogOutput('** COOL, ALL {0} BINARIES MATCHED!! **'.format(
235 match_count))
236 return True
237
238 mismatch_str = 'Found {0} mismatch:\n'.format(len(mismatch_list))
239 for b in mismatch_list:
240 mismatch_str += '\t' + b + '\n'
241
242 self.logger.LogOutput(mismatch_str)
243 return False
244
245
246def Main(argv):
247 """The main function."""
248
249 command_executer.InitCommandExecuter()
250 images = []
251
252 parser = argparse.ArgumentParser()
253 parser.add_argument(
254 '--no_unmount', action='store_true', dest='no_unmount', default=False,
255 help='Do not unmount after finish, this is useful for debugging.')
256 parser.add_argument(
257 '--chromeos_root', dest='chromeos_root', default=None, action='store',
258 help=('[Optional] Specify a chromeos tree instead of '
259 'deducing it from image path so that we can compare '
260 '2 images that are downloaded.'))
261 parser.add_argument(
262 '--mount_basename', dest='mount_basename', default=None, action='store',
263 help=('Specify a meaningful name for the mount point. With this being '
264 'set, the mount points would be "/tmp/mount_basename.x.rootfs" '
265 ' and "/tmp/mount_basename.x.stateful". (x is 1 or 2).'))
266 parser.add_argument('--diff_file', dest='diff_file', default=None,
267 help='Dumping all the diffs (if any) to the diff file')
268 parser.add_argument('--image1', dest='image1', default=None,
269 required=True, help=('Image 1 file name.'))
270 parser.add_argument('--image2', dest='image2', default=None,
271 required=True, help=('Image 2 file name.'))
272 options = parser.parse_args(argv[1:])
273
274 if options.mount_basename and options.mount_basename.find('/') >= 0:
275 logger.GetLogger().LogError(
276 '"--mount_basename" must be a name, not a path.')
277 parser.print_help()
278 return 1
279
280 result = False
281 image_comparator = None
282 try:
283 for image_path in [options.image1, options.image2]:
284 image_path = os.path.realpath(image_path)
285 if not os.path.isfile(image_path):
286 logger.getLogger().LogError('"{0}" is not a file.'.format(image_path))
287 return 1
288
289 chromeos_root = None
290 if options.chromeos_root:
291 chromeos_root = options.chromeos_root
292 else:
293 ## Deduce chromeos root from image
294 t = image_path
295 while t != '/':
296 if misc.IsChromeOsTree(t):
297 break
298 t = os.path.dirname(t)
299 if misc.IsChromeOsTree(t):
300 chromeos_root = t
301
302 if not chromeos_root:
303 logger.GetLogger().LogError(
304 'Please provide a valid chromeos root via --chromeos_root')
305 return 1
306
307 image = CrosImage(image_path, chromeos_root, options.no_unmount)
308
309 if options.mount_basename:
310 mount_basename = '{basename}.{index}'.format(
311 basename=options.mount_basename, index=i)
312 else:
313 mount_basename = None
314
315 if image.MountImage(mount_basename):
316 images.append(image)
317 image.FindElfFiles()
318
319 if len(images) == 2:
320 image_comparator = ImageComparator(images, options.diff_file)
321 result = image_comparator.CompareImages()
322 finally:
323 for image in images:
324 image.UnmountImage()
325 if image_comparator:
326 image_comparator.Cleanup()
327
328 return 0 if result else 1
329
330
331if __name__ == '__main__':
332 Main(sys.argv)