From 3dc2a88e73455e0d850045b3f496b589c56abc5c Mon Sep 17 00:00:00 2001 From: Han Shen Date: Tue, 7 Jan 2014 16:42:01 -0800 Subject: Added a script to compare 2 chromiumos images. This is done by comparing each ELF files of those images. TEST=manually BUG=None Change-Id: I7b19665c074645c810d974fc13aa3070d4cba6ee Reviewed-on: https://chrome-internal-review.googlesource.com/151367 Reviewed-by: Han Shen Tested-by: Han Shen Commit-Queue: Han Shen --- chromiumos_image_diff.py | 332 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100755 chromiumos_image_diff.py (limited to 'chromiumos_image_diff.py') diff --git a/chromiumos_image_diff.py b/chromiumos_image_diff.py new file mode 100755 index 00000000..6b455829 --- /dev/null +++ b/chromiumos_image_diff.py @@ -0,0 +1,332 @@ +#!/usr/bin/python +"""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. +""" + +__author__ = 'shenhan@google.com (Han Shen)' + +import argparse +import os +import re +import sys +import tempfile + +import image_chromeos +from utils import command_executer +from utils import logger +from 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 + + 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, return_output=False, 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, return_output=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, return_output=False, 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.RunCommand( + command, return_output=True, 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, return_output=False, + 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, return_output=False, 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 image_path in [options.image1, options.image2]: + 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) -- cgit v1.2.3