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