You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
368 lines
12 KiB
368 lines
12 KiB
#!/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)
|