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