blob: 74906d3292a61b44c4ba8199c929a8c4242748c6 [file] [log] [blame]
Caroline Ticef6ef4392017-04-06 17:16:05 -07001#!/usr/bin/env python2
Manoj Gupta1d1de432019-04-26 11:30:14 -07002# -*- coding: utf-8 -*-
3#
4# Copyright 2019 The Chromium OS Authors. All rights reserved.
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
7
Han Shen3dc2a882014-01-07 16:42:01 -08008"""Diff 2 chromiumos images by comparing each elf file.
9
Caroline Tice88272d42016-01-13 09:48:29 -080010 The script diffs every *ELF* files by dissembling every *executable*
11 section, which means it is not a FULL elf differ.
Han Shen3dc2a882014-01-07 16:42:01 -080012
13 A simple usage example -
14 chromiumos_image_diff.py --image1 image-path-1 --image2 image-path-2
15
16 Note that image path should be inside the chroot, if not (ie, image is
17 downloaded from web), please specify a chromiumos checkout via
18 "--chromeos_root".
19
20 And this script should be executed outside chroot.
21"""
22
Caroline Tice88272d42016-01-13 09:48:29 -080023from __future__ import print_function
24
Han Shen3dc2a882014-01-07 16:42:01 -080025__author__ = 'shenhan@google.com (Han Shen)'
26
27import argparse
28import os
29import re
30import sys
31import tempfile
32
33import image_chromeos
Caroline Tice88272d42016-01-13 09:48:29 -080034from cros_utils import command_executer
35from cros_utils import logger
36from cros_utils import misc
Han Shen3dc2a882014-01-07 16:42:01 -080037
38
39class CrosImage(object):
40 """A cros image object."""
41
42 def __init__(self, image, chromeos_root, no_unmount):
43 self.image = image
44 self.chromeos_root = chromeos_root
45 self.mounted = False
46 self._ce = command_executer.GetCommandExecuter()
47 self.logger = logger.GetLogger()
48 self.elf_files = []
49 self.no_unmount = no_unmount
Caroline Tice88272d42016-01-13 09:48:29 -080050 self.unmount_script = ''
51 self.stateful = ''
52 self.rootfs = ''
Han Shen3dc2a882014-01-07 16:42:01 -080053
54 def MountImage(self, mount_basename):
55 """Mount/unpack the image."""
56
57 if mount_basename:
58 self.rootfs = '/tmp/{0}.rootfs'.format(mount_basename)
59 self.stateful = '/tmp/{0}.stateful'.format(mount_basename)
60 self.unmount_script = '/tmp/{0}.unmount.sh'.format(mount_basename)
61 else:
Caroline Ticef6ef4392017-04-06 17:16:05 -070062 self.rootfs = tempfile.mkdtemp(
63 suffix='.rootfs', prefix='chromiumos_image_diff')
Han Shen3dc2a882014-01-07 16:42:01 -080064 ## rootfs is like /tmp/tmpxyz012.rootfs.
65 match = re.match(r'^(.*)\.rootfs$', self.rootfs)
66 basename = match.group(1)
67 self.stateful = basename + '.stateful'
68 os.mkdir(self.stateful)
69 self.unmount_script = '{0}.unmount.sh'.format(basename)
70
71 self.logger.LogOutput('Mounting "{0}" onto "{1}" and "{2}"'.format(
72 self.image, self.rootfs, self.stateful))
73 ## First of all creating an unmount image
74 self.CreateUnmountScript()
Manoj Gupta1d1de432019-04-26 11:30:14 -070075 command = image_chromeos.GetImageMountCommand(self.image, self.rootfs,
76 self.stateful)
Luis Lozano036c9232015-12-10 10:47:01 -080077 rv = self._ce.RunCommand(command, print_to_console=True)
Han Shen3dc2a882014-01-07 16:42:01 -080078 self.mounted = (rv == 0)
79 if not self.mounted:
80 self.logger.LogError('Failed to mount "{0}" onto "{1}" and "{2}".'.format(
81 self.image, self.rootfs, self.stateful))
82 return self.mounted
83
84 def CreateUnmountScript(self):
85 command = ('sudo umount {r}/usr/local {r}/usr/share/oem '
86 '{r}/var {r}/mnt/stateful_partition {r}; sudo umount {s} ; '
Caroline Ticef6ef4392017-04-06 17:16:05 -070087 'rmdir {r} ; rmdir {s}\n').format(
88 r=self.rootfs, s=self.stateful)
Han Shen3dc2a882014-01-07 16:42:01 -080089 f = open(self.unmount_script, 'w')
90 f.write(command)
91 f.close()
Caroline Ticef6ef4392017-04-06 17:16:05 -070092 self._ce.RunCommand(
93 'chmod +x {}'.format(self.unmount_script), print_to_console=False)
Manoj Gupta1d1de432019-04-26 11:30:14 -070094 self.logger.LogOutput('Created an unmount script - "{0}"'.format(
95 self.unmount_script))
Han Shen3dc2a882014-01-07 16:42:01 -080096
97 def UnmountImage(self):
98 """Unmount the image and delete mount point."""
99
100 self.logger.LogOutput('Unmounting image "{0}" from "{1}" and "{2}"'.format(
101 self.image, self.rootfs, self.stateful))
102 if self.mounted:
103 command = 'bash "{0}"'.format(self.unmount_script)
104 if self.no_unmount:
105 self.logger.LogOutput(('Please unmount manually - \n'
106 '\t bash "{0}"'.format(self.unmount_script)))
107 else:
Luis Lozano036c9232015-12-10 10:47:01 -0800108 if self._ce.RunCommand(command, print_to_console=True) == 0:
Han Shen3dc2a882014-01-07 16:42:01 -0800109 self._ce.RunCommand('rm {0}'.format(self.unmount_script))
110 self.mounted = False
111 self.rootfs = None
112 self.stateful = None
113 self.unmount_script = None
114
Caroline Tice88272d42016-01-13 09:48:29 -0800115 return not self.mounted
Han Shen3dc2a882014-01-07 16:42:01 -0800116
117 def FindElfFiles(self):
118 """Find all elf files for the image.
119
120 Returns:
121 Always true
122 """
123
Manoj Gupta1d1de432019-04-26 11:30:14 -0700124 self.logger.LogOutput('Finding all elf files in "{0}" ...'.format(
125 self.rootfs))
Han Shen3dc2a882014-01-07 16:42:01 -0800126 # Note '\;' must be prefixed by 'r'.
127 command = ('find "{0}" -type f -exec '
Caroline Ticef6ef4392017-04-06 17:16:05 -0700128 'bash -c \'file -b "{{}}" | grep -q "ELF"\''
129 r' \; '
Han Shen3dc2a882014-01-07 16:42:01 -0800130 r'-exec echo "{{}}" \;').format(self.rootfs)
131 self.logger.LogCmd(command)
Luis Lozano036c9232015-12-10 10:47:01 -0800132 _, out, _ = self._ce.RunCommandWOutput(command, print_to_console=False)
Han Shen3dc2a882014-01-07 16:42:01 -0800133 self.elf_files = out.splitlines()
Manoj Gupta1d1de432019-04-26 11:30:14 -0700134 self.logger.LogOutput('Total {0} elf files found.'.format(
135 len(self.elf_files)))
Han Shen3dc2a882014-01-07 16:42:01 -0800136 return True
137
138
139class ImageComparator(object):
140 """A class that wraps comparsion actions."""
141
142 def __init__(self, images, diff_file):
143 self.images = images
144 self.logger = logger.GetLogger()
145 self.diff_file = diff_file
146 self.tempf1 = None
147 self.tempf2 = None
148
149 def Cleanup(self):
150 if self.tempf1 and self.tempf2:
Manoj Gupta1d1de432019-04-26 11:30:14 -0700151 command_executer.GetCommandExecuter().RunCommand('rm {0} {1}'.format(
152 self.tempf1, self.tempf2))
153 logger.GetLogger('Removed "{0}" and "{1}".'.format(
154 self.tempf1, self.tempf2))
Han Shen3dc2a882014-01-07 16:42:01 -0800155
156 def CheckElfFileSetEquality(self):
157 """Checking whether images have exactly number of elf files."""
158
159 self.logger.LogOutput('Checking elf file equality ...')
160 i1 = self.images[0]
161 i2 = self.images[1]
162 t1 = i1.rootfs + '/'
163 elfset1 = set([e.replace(t1, '') for e in i1.elf_files])
164 t2 = i2.rootfs + '/'
165 elfset2 = set([e.replace(t2, '') for e in i2.elf_files])
166 dif1 = elfset1.difference(elfset2)
167 msg = None
168 if dif1:
169 msg = 'The following files are not in "{image}" - "{rootfs}":\n'.format(
170 image=i2.image, rootfs=i2.rootfs)
171 for d in dif1:
172 msg += '\t' + d + '\n'
173 dif2 = elfset2.difference(elfset1)
174 if dif2:
175 msg = 'The following files are not in "{image}" - "{rootfs}":\n'.format(
176 image=i1.image, rootfs=i1.rootfs)
177 for d in dif2:
178 msg += '\t' + d + '\n'
179 if msg:
180 self.logger.LogError(msg)
181 return False
182 return True
183
184 def CompareImages(self):
185 """Do the comparsion work."""
186
187 if not self.CheckElfFileSetEquality():
188 return False
189
190 mismatch_list = []
191 match_count = 0
192 i1 = self.images[0]
193 i2 = self.images[1]
Manoj Gupta1d1de432019-04-26 11:30:14 -0700194 self.logger.LogOutput('Start comparing {0} elf file by file ...'.format(
195 len(i1.elf_files)))
Han Shen3dc2a882014-01-07 16:42:01 -0800196 ## Note - i1.elf_files and i2.elf_files have exactly the same entries here.
197
198 ## Create 2 temp files to be used for all disassembed files.
199 handle, self.tempf1 = tempfile.mkstemp()
200 os.close(handle) # We do not need the handle
201 handle, self.tempf2 = tempfile.mkstemp()
202 os.close(handle)
203
204 cmde = command_executer.GetCommandExecuter()
205 for elf1 in i1.elf_files:
206 tmp_rootfs = i1.rootfs + '/'
207 f1 = elf1.replace(tmp_rootfs, '')
208 full_path1 = elf1
209 full_path2 = elf1.replace(i1.rootfs, i2.rootfs)
210
211 if full_path1 == full_path2:
212 self.logger.LogError(
213 'Error: We\'re comparing the SAME file - {0}'.format(f1))
214 continue
215
Caroline Ticef6ef4392017-04-06 17:16:05 -0700216 command = (
217 'objdump -d "{f1}" > {tempf1} ; '
218 'objdump -d "{f2}" > {tempf2} ; '
219 # Remove path string inside the dissemble
220 'sed -i \'s!{rootfs1}!!g\' {tempf1} ; '
221 'sed -i \'s!{rootfs2}!!g\' {tempf2} ; '
222 'diff {tempf1} {tempf2} 1>/dev/null 2>&1').format(
223 f1=full_path1,
224 f2=full_path2,
225 rootfs1=i1.rootfs,
226 rootfs2=i2.rootfs,
227 tempf1=self.tempf1,
228 tempf2=self.tempf2)
Luis Lozano036c9232015-12-10 10:47:01 -0800229 ret = cmde.RunCommand(command, print_to_console=False)
Han Shen3dc2a882014-01-07 16:42:01 -0800230 if ret != 0:
Manoj Gupta1d1de432019-04-26 11:30:14 -0700231 self.logger.LogOutput('*** Not match - "{0}" "{1}"'.format(
232 full_path1, full_path2))
Han Shen3dc2a882014-01-07 16:42:01 -0800233 mismatch_list.append(f1)
234 if self.diff_file:
Caroline Ticef6ef4392017-04-06 17:16:05 -0700235 command = ('echo "Diffs of disassemble of \"{f1}\" and \"{f2}\"" '
236 '>> {diff_file} ; diff {tempf1} {tempf2} '
237 '>> {diff_file}').format(
238 f1=full_path1,
239 f2=full_path2,
240 diff_file=self.diff_file,
241 tempf1=self.tempf1,
242 tempf2=self.tempf2)
Luis Lozano036c9232015-12-10 10:47:01 -0800243 cmde.RunCommand(command, print_to_console=False)
Han Shen3dc2a882014-01-07 16:42:01 -0800244 else:
245 match_count += 1
246 ## End of comparing every elf files.
247
248 if not mismatch_list:
Caroline Ticef6ef4392017-04-06 17:16:05 -0700249 self.logger.LogOutput(
250 '** COOL, ALL {0} BINARIES MATCHED!! **'.format(match_count))
Han Shen3dc2a882014-01-07 16:42:01 -0800251 return True
252
253 mismatch_str = 'Found {0} mismatch:\n'.format(len(mismatch_list))
254 for b in mismatch_list:
255 mismatch_str += '\t' + b + '\n'
256
257 self.logger.LogOutput(mismatch_str)
258 return False
259
260
261def Main(argv):
262 """The main function."""
263
264 command_executer.InitCommandExecuter()
265 images = []
266
267 parser = argparse.ArgumentParser()
268 parser.add_argument(
Caroline Ticef6ef4392017-04-06 17:16:05 -0700269 '--no_unmount',
270 action='store_true',
271 dest='no_unmount',
272 default=False,
Han Shen3dc2a882014-01-07 16:42:01 -0800273 help='Do not unmount after finish, this is useful for debugging.')
274 parser.add_argument(
Caroline Ticef6ef4392017-04-06 17:16:05 -0700275 '--chromeos_root',
276 dest='chromeos_root',
277 default=None,
278 action='store',
Han Shen3dc2a882014-01-07 16:42:01 -0800279 help=('[Optional] Specify a chromeos tree instead of '
280 'deducing it from image path so that we can compare '
281 '2 images that are downloaded.'))
282 parser.add_argument(
Caroline Ticef6ef4392017-04-06 17:16:05 -0700283 '--mount_basename',
284 dest='mount_basename',
285 default=None,
286 action='store',
Han Shen3dc2a882014-01-07 16:42:01 -0800287 help=('Specify a meaningful name for the mount point. With this being '
288 'set, the mount points would be "/tmp/mount_basename.x.rootfs" '
289 ' and "/tmp/mount_basename.x.stateful". (x is 1 or 2).'))
Caroline Ticef6ef4392017-04-06 17:16:05 -0700290 parser.add_argument(
291 '--diff_file',
292 dest='diff_file',
293 default=None,
294 help='Dumping all the diffs (if any) to the diff file')
295 parser.add_argument(
296 '--image1',
297 dest='image1',
298 default=None,
299 required=True,
300 help=('Image 1 file name.'))
301 parser.add_argument(
302 '--image2',
303 dest='image2',
304 default=None,
305 required=True,
306 help=('Image 2 file name.'))
Han Shen3dc2a882014-01-07 16:42:01 -0800307 options = parser.parse_args(argv[1:])
308
309 if options.mount_basename and options.mount_basename.find('/') >= 0:
310 logger.GetLogger().LogError(
311 '"--mount_basename" must be a name, not a path.')
312 parser.print_help()
313 return 1
314
315 result = False
316 image_comparator = None
317 try:
Caroline Tice88272d42016-01-13 09:48:29 -0800318 for i, image_path in enumerate([options.image1, options.image2], start=1):
Han Shen3dc2a882014-01-07 16:42:01 -0800319 image_path = os.path.realpath(image_path)
320 if not os.path.isfile(image_path):
321 logger.getLogger().LogError('"{0}" is not a file.'.format(image_path))
322 return 1
323
324 chromeos_root = None
325 if options.chromeos_root:
326 chromeos_root = options.chromeos_root
327 else:
328 ## Deduce chromeos root from image
329 t = image_path
330 while t != '/':
331 if misc.IsChromeOsTree(t):
332 break
333 t = os.path.dirname(t)
334 if misc.IsChromeOsTree(t):
335 chromeos_root = t
336
337 if not chromeos_root:
338 logger.GetLogger().LogError(
339 'Please provide a valid chromeos root via --chromeos_root')
340 return 1
341
342 image = CrosImage(image_path, chromeos_root, options.no_unmount)
343
344 if options.mount_basename:
345 mount_basename = '{basename}.{index}'.format(
346 basename=options.mount_basename, index=i)
347 else:
348 mount_basename = None
349
350 if image.MountImage(mount_basename):
351 images.append(image)
352 image.FindElfFiles()
353
354 if len(images) == 2:
355 image_comparator = ImageComparator(images, options.diff_file)
356 result = image_comparator.CompareImages()
357 finally:
358 for image in images:
359 image.UnmountImage()
360 if image_comparator:
361 image_comparator.Cleanup()
362
363 return 0 if result else 1
364
365
366if __name__ == '__main__':
367 Main(sys.argv)