#!/usr/bin/env python2 """Diff 2 chromiumos images by comparing each elf file. The script diffs every *ELF* files by dissembling every *executable* section, which means it is not a FULL elf differ. A simple usage example - chromiumos_image_diff.py --image1 image-path-1 --image2 image-path-2 Note that image path should be inside the chroot, if not (ie, image is downloaded from web), please specify a chromiumos checkout via "--chromeos_root". And this script should be executed outside chroot. """ from __future__ import print_function __author__ = 'shenhan@google.com (Han Shen)' import argparse import os import re import sys import tempfile import image_chromeos from cros_utils import command_executer from cros_utils import logger from cros_utils import misc class CrosImage(object): """A cros image object.""" def __init__(self, image, chromeos_root, no_unmount): self.image = image self.chromeos_root = chromeos_root self.mounted = False self._ce = command_executer.GetCommandExecuter() self.logger = logger.GetLogger() self.elf_files = [] self.no_unmount = no_unmount self.unmount_script = '' self.stateful = '' self.rootfs = '' def MountImage(self, mount_basename): """Mount/unpack the image.""" if mount_basename: self.rootfs = '/tmp/{0}.rootfs'.format(mount_basename) self.stateful = '/tmp/{0}.stateful'.format(mount_basename) self.unmount_script = '/tmp/{0}.unmount.sh'.format(mount_basename) else: self.rootfs = tempfile.mkdtemp( suffix='.rootfs', prefix='chromiumos_image_diff') ## rootfs is like /tmp/tmpxyz012.rootfs. match = re.match(r'^(.*)\.rootfs$', self.rootfs) basename = match.group(1) self.stateful = basename + '.stateful' os.mkdir(self.stateful) self.unmount_script = '{0}.unmount.sh'.format(basename) self.logger.LogOutput('Mounting "{0}" onto "{1}" and "{2}"'.format( self.image, self.rootfs, self.stateful)) ## First of all creating an unmount image self.CreateUnmountScript() command = image_chromeos.GetImageMountCommand( self.chromeos_root, self.image, self.rootfs, self.stateful) rv = self._ce.RunCommand(command, print_to_console=True) self.mounted = (rv == 0) if not self.mounted: self.logger.LogError('Failed to mount "{0}" onto "{1}" and "{2}".'.format( self.image, self.rootfs, self.stateful)) return self.mounted def CreateUnmountScript(self): command = ('sudo umount {r}/usr/local {r}/usr/share/oem ' '{r}/var {r}/mnt/stateful_partition {r}; sudo umount {s} ; ' 'rmdir {r} ; rmdir {s}\n').format( r=self.rootfs, s=self.stateful) f = open(self.unmount_script, 'w') f.write(command) f.close() self._ce.RunCommand( 'chmod +x {}'.format(self.unmount_script), print_to_console=False) self.logger.LogOutput( 'Created an unmount script - "{0}"'.format(self.unmount_script)) def UnmountImage(self): """Unmount the image and delete mount point.""" self.logger.LogOutput('Unmounting image "{0}" from "{1}" and "{2}"'.format( self.image, self.rootfs, self.stateful)) if self.mounted: command = 'bash "{0}"'.format(self.unmount_script) if self.no_unmount: self.logger.LogOutput(('Please unmount manually - \n' '\t bash "{0}"'.format(self.unmount_script))) else: if self._ce.RunCommand(command, print_to_console=True) == 0: self._ce.RunCommand('rm {0}'.format(self.unmount_script)) self.mounted = False self.rootfs = None self.stateful = None self.unmount_script = None return not self.mounted def FindElfFiles(self): """Find all elf files for the image. Returns: Always true """ self.logger.LogOutput( 'Finding all elf files in "{0}" ...'.format(self.rootfs)) # Note '\;' must be prefixed by 'r'. command = ('find "{0}" -type f -exec ' 'bash -c \'file -b "{{}}" | grep -q "ELF"\'' r' \; ' r'-exec echo "{{}}" \;').format(self.rootfs) self.logger.LogCmd(command) _, out, _ = self._ce.RunCommandWOutput(command, print_to_console=False) self.elf_files = out.splitlines() self.logger.LogOutput( 'Total {0} elf files found.'.format(len(self.elf_files))) return True class ImageComparator(object): """A class that wraps comparsion actions.""" def __init__(self, images, diff_file): self.images = images self.logger = logger.GetLogger() self.diff_file = diff_file self.tempf1 = None self.tempf2 = None def Cleanup(self): if self.tempf1 and self.tempf2: command_executer.GetCommandExecuter().RunCommand( 'rm {0} {1}'.format(self.tempf1, self.tempf2)) logger.GetLogger( 'Removed "{0}" and "{1}".'.format(self.tempf1, self.tempf2)) def CheckElfFileSetEquality(self): """Checking whether images have exactly number of elf files.""" self.logger.LogOutput('Checking elf file equality ...') i1 = self.images[0] i2 = self.images[1] t1 = i1.rootfs + '/' elfset1 = set([e.replace(t1, '') for e in i1.elf_files]) t2 = i2.rootfs + '/' elfset2 = set([e.replace(t2, '') for e in i2.elf_files]) dif1 = elfset1.difference(elfset2) msg = None if dif1: msg = 'The following files are not in "{image}" - "{rootfs}":\n'.format( image=i2.image, rootfs=i2.rootfs) for d in dif1: msg += '\t' + d + '\n' dif2 = elfset2.difference(elfset1) if dif2: msg = 'The following files are not in "{image}" - "{rootfs}":\n'.format( image=i1.image, rootfs=i1.rootfs) for d in dif2: msg += '\t' + d + '\n' if msg: self.logger.LogError(msg) return False return True def CompareImages(self): """Do the comparsion work.""" if not self.CheckElfFileSetEquality(): return False mismatch_list = [] match_count = 0 i1 = self.images[0] i2 = self.images[1] self.logger.LogOutput( 'Start comparing {0} elf file by file ...'.format(len(i1.elf_files))) ## Note - i1.elf_files and i2.elf_files have exactly the same entries here. ## Create 2 temp files to be used for all disassembed files. handle, self.tempf1 = tempfile.mkstemp() os.close(handle) # We do not need the handle handle, self.tempf2 = tempfile.mkstemp() os.close(handle) cmde = command_executer.GetCommandExecuter() for elf1 in i1.elf_files: tmp_rootfs = i1.rootfs + '/' f1 = elf1.replace(tmp_rootfs, '') full_path1 = elf1 full_path2 = elf1.replace(i1.rootfs, i2.rootfs) if full_path1 == full_path2: self.logger.LogError( 'Error: We\'re comparing the SAME file - {0}'.format(f1)) continue command = ( 'objdump -d "{f1}" > {tempf1} ; ' 'objdump -d "{f2}" > {tempf2} ; ' # Remove path string inside the dissemble 'sed -i \'s!{rootfs1}!!g\' {tempf1} ; ' 'sed -i \'s!{rootfs2}!!g\' {tempf2} ; ' 'diff {tempf1} {tempf2} 1>/dev/null 2>&1').format( f1=full_path1, f2=full_path2, rootfs1=i1.rootfs, rootfs2=i2.rootfs, tempf1=self.tempf1, tempf2=self.tempf2) ret = cmde.RunCommand(command, print_to_console=False) if ret != 0: self.logger.LogOutput( '*** Not match - "{0}" "{1}"'.format(full_path1, full_path2)) mismatch_list.append(f1) if self.diff_file: command = ('echo "Diffs of disassemble of \"{f1}\" and \"{f2}\"" ' '>> {diff_file} ; diff {tempf1} {tempf2} ' '>> {diff_file}').format( f1=full_path1, f2=full_path2, diff_file=self.diff_file, tempf1=self.tempf1, tempf2=self.tempf2) cmde.RunCommand(command, print_to_console=False) else: match_count += 1 ## End of comparing every elf files. if not mismatch_list: self.logger.LogOutput( '** COOL, ALL {0} BINARIES MATCHED!! **'.format(match_count)) return True mismatch_str = 'Found {0} mismatch:\n'.format(len(mismatch_list)) for b in mismatch_list: mismatch_str += '\t' + b + '\n' self.logger.LogOutput(mismatch_str) return False def Main(argv): """The main function.""" command_executer.InitCommandExecuter() images = [] parser = argparse.ArgumentParser() parser.add_argument( '--no_unmount', action='store_true', dest='no_unmount', default=False, help='Do not unmount after finish, this is useful for debugging.') parser.add_argument( '--chromeos_root', dest='chromeos_root', default=None, action='store', help=('[Optional] Specify a chromeos tree instead of ' 'deducing it from image path so that we can compare ' '2 images that are downloaded.')) parser.add_argument( '--mount_basename', dest='mount_basename', default=None, action='store', help=('Specify a meaningful name for the mount point. With this being ' 'set, the mount points would be "/tmp/mount_basename.x.rootfs" ' ' and "/tmp/mount_basename.x.stateful". (x is 1 or 2).')) parser.add_argument( '--diff_file', dest='diff_file', default=None, help='Dumping all the diffs (if any) to the diff file') parser.add_argument( '--image1', dest='image1', default=None, required=True, help=('Image 1 file name.')) parser.add_argument( '--image2', dest='image2', default=None, required=True, help=('Image 2 file name.')) options = parser.parse_args(argv[1:]) if options.mount_basename and options.mount_basename.find('/') >= 0: logger.GetLogger().LogError( '"--mount_basename" must be a name, not a path.') parser.print_help() return 1 result = False image_comparator = None try: for i, image_path in enumerate([options.image1, options.image2], start=1): image_path = os.path.realpath(image_path) if not os.path.isfile(image_path): logger.getLogger().LogError('"{0}" is not a file.'.format(image_path)) return 1 chromeos_root = None if options.chromeos_root: chromeos_root = options.chromeos_root else: ## Deduce chromeos root from image t = image_path while t != '/': if misc.IsChromeOsTree(t): break t = os.path.dirname(t) if misc.IsChromeOsTree(t): chromeos_root = t if not chromeos_root: logger.GetLogger().LogError( 'Please provide a valid chromeos root via --chromeos_root') return 1 image = CrosImage(image_path, chromeos_root, options.no_unmount) if options.mount_basename: mount_basename = '{basename}.{index}'.format( basename=options.mount_basename, index=i) else: mount_basename = None if image.MountImage(mount_basename): images.append(image) image.FindElfFiles() if len(images) == 2: image_comparator = ImageComparator(images, options.diff_file) result = image_comparator.CompareImages() finally: for image in images: image.UnmountImage() if image_comparator: image_comparator.Cleanup() return 0 if result else 1 if __name__ == '__main__': Main(sys.argv)