#!/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)