| #!/usr/bin/env python3 |
| # -*- coding: utf-8 -*- |
| # |
| # Copyright 2019 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """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.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', encoding='utf-8') |
| 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 = {e.replace(t1, '') for e in i1.elf_files} |
| t2 = i2.rootfs + '/' |
| elfset2 = {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) |