1#!/usr/bin/env python2 2"""Diff 2 chromiumos images by comparing each elf file. 3 4 The script diffs every *ELF* files by dissembling every *executable* 5 section, which means it is not a FULL elf differ. 6 7 A simple usage example - 8 chromiumos_image_diff.py --image1 image-path-1 --image2 image-path-2 9 10 Note that image path should be inside the chroot, if not (ie, image is 11 downloaded from web), please specify a chromiumos checkout via 12 "--chromeos_root". 13 14 And this script should be executed outside chroot. 15""" 16 17from __future__ import print_function 18 19__author__ = 'shenhan@google.com (Han Shen)' 20 21import argparse 22import os 23import re 24import sys 25import tempfile 26 27import image_chromeos 28from cros_utils import command_executer 29from cros_utils import logger 30from cros_utils import misc 31 32 33class CrosImage(object): 34 """A cros image object.""" 35 36 def __init__(self, image, chromeos_root, no_unmount): 37 self.image = image 38 self.chromeos_root = chromeos_root 39 self.mounted = False 40 self._ce = command_executer.GetCommandExecuter() 41 self.logger = logger.GetLogger() 42 self.elf_files = [] 43 self.no_unmount = no_unmount 44 self.unmount_script = '' 45 self.stateful = '' 46 self.rootfs = '' 47 48 def MountImage(self, mount_basename): 49 """Mount/unpack the image.""" 50 51 if mount_basename: 52 self.rootfs = '/tmp/{0}.rootfs'.format(mount_basename) 53 self.stateful = '/tmp/{0}.stateful'.format(mount_basename) 54 self.unmount_script = '/tmp/{0}.unmount.sh'.format(mount_basename) 55 else: 56 self.rootfs = tempfile.mkdtemp( 57 suffix='.rootfs', prefix='chromiumos_image_diff') 58 ## rootfs is like /tmp/tmpxyz012.rootfs. 59 match = re.match(r'^(.*)\.rootfs$', self.rootfs) 60 basename = match.group(1) 61 self.stateful = basename + '.stateful' 62 os.mkdir(self.stateful) 63 self.unmount_script = '{0}.unmount.sh'.format(basename) 64 65 self.logger.LogOutput('Mounting "{0}" onto "{1}" and "{2}"'.format( 66 self.image, self.rootfs, self.stateful)) 67 ## First of all creating an unmount image 68 self.CreateUnmountScript() 69 command = image_chromeos.GetImageMountCommand( 70 self.chromeos_root, self.image, self.rootfs, self.stateful) 71 rv = self._ce.RunCommand(command, print_to_console=True) 72 self.mounted = (rv == 0) 73 if not self.mounted: 74 self.logger.LogError('Failed to mount "{0}" onto "{1}" and "{2}".'.format( 75 self.image, self.rootfs, self.stateful)) 76 return self.mounted 77 78 def CreateUnmountScript(self): 79 command = ('sudo umount {r}/usr/local {r}/usr/share/oem ' 80 '{r}/var {r}/mnt/stateful_partition {r}; sudo umount {s} ; ' 81 'rmdir {r} ; rmdir {s}\n').format( 82 r=self.rootfs, s=self.stateful) 83 f = open(self.unmount_script, 'w') 84 f.write(command) 85 f.close() 86 self._ce.RunCommand( 87 'chmod +x {}'.format(self.unmount_script), print_to_console=False) 88 self.logger.LogOutput( 89 'Created an unmount script - "{0}"'.format(self.unmount_script)) 90 91 def UnmountImage(self): 92 """Unmount the image and delete mount point.""" 93 94 self.logger.LogOutput('Unmounting image "{0}" from "{1}" and "{2}"'.format( 95 self.image, self.rootfs, self.stateful)) 96 if self.mounted: 97 command = 'bash "{0}"'.format(self.unmount_script) 98 if self.no_unmount: 99 self.logger.LogOutput(('Please unmount manually - \n' 100 '\t bash "{0}"'.format(self.unmount_script))) 101 else: 102 if self._ce.RunCommand(command, print_to_console=True) == 0: 103 self._ce.RunCommand('rm {0}'.format(self.unmount_script)) 104 self.mounted = False 105 self.rootfs = None 106 self.stateful = None 107 self.unmount_script = None 108 109 return not self.mounted 110 111 def FindElfFiles(self): 112 """Find all elf files for the image. 113 114 Returns: 115 Always true 116 """ 117 118 self.logger.LogOutput( 119 'Finding all elf files in "{0}" ...'.format(self.rootfs)) 120 # Note '\;' must be prefixed by 'r'. 121 command = ('find "{0}" -type f -exec ' 122 'bash -c \'file -b "{{}}" | grep -q "ELF"\'' 123 r' \; ' 124 r'-exec echo "{{}}" \;').format(self.rootfs) 125 self.logger.LogCmd(command) 126 _, out, _ = self._ce.RunCommandWOutput(command, print_to_console=False) 127 self.elf_files = out.splitlines() 128 self.logger.LogOutput( 129 'Total {0} elf files found.'.format(len(self.elf_files))) 130 return True 131 132 133class ImageComparator(object): 134 """A class that wraps comparsion actions.""" 135 136 def __init__(self, images, diff_file): 137 self.images = images 138 self.logger = logger.GetLogger() 139 self.diff_file = diff_file 140 self.tempf1 = None 141 self.tempf2 = None 142 143 def Cleanup(self): 144 if self.tempf1 and self.tempf2: 145 command_executer.GetCommandExecuter().RunCommand( 146 'rm {0} {1}'.format(self.tempf1, self.tempf2)) 147 logger.GetLogger( 148 'Removed "{0}" and "{1}".'.format(self.tempf1, self.tempf2)) 149 150 def CheckElfFileSetEquality(self): 151 """Checking whether images have exactly number of elf files.""" 152 153 self.logger.LogOutput('Checking elf file equality ...') 154 i1 = self.images[0] 155 i2 = self.images[1] 156 t1 = i1.rootfs + '/' 157 elfset1 = set([e.replace(t1, '') for e in i1.elf_files]) 158 t2 = i2.rootfs + '/' 159 elfset2 = set([e.replace(t2, '') for e in i2.elf_files]) 160 dif1 = elfset1.difference(elfset2) 161 msg = None 162 if dif1: 163 msg = 'The following files are not in "{image}" - "{rootfs}":\n'.format( 164 image=i2.image, rootfs=i2.rootfs) 165 for d in dif1: 166 msg += '\t' + d + '\n' 167 dif2 = elfset2.difference(elfset1) 168 if dif2: 169 msg = 'The following files are not in "{image}" - "{rootfs}":\n'.format( 170 image=i1.image, rootfs=i1.rootfs) 171 for d in dif2: 172 msg += '\t' + d + '\n' 173 if msg: 174 self.logger.LogError(msg) 175 return False 176 return True 177 178 def CompareImages(self): 179 """Do the comparsion work.""" 180 181 if not self.CheckElfFileSetEquality(): 182 return False 183 184 mismatch_list = [] 185 match_count = 0 186 i1 = self.images[0] 187 i2 = self.images[1] 188 self.logger.LogOutput( 189 'Start comparing {0} elf file by file ...'.format(len(i1.elf_files))) 190 ## Note - i1.elf_files and i2.elf_files have exactly the same entries here. 191 192 ## Create 2 temp files to be used for all disassembed files. 193 handle, self.tempf1 = tempfile.mkstemp() 194 os.close(handle) # We do not need the handle 195 handle, self.tempf2 = tempfile.mkstemp() 196 os.close(handle) 197 198 cmde = command_executer.GetCommandExecuter() 199 for elf1 in i1.elf_files: 200 tmp_rootfs = i1.rootfs + '/' 201 f1 = elf1.replace(tmp_rootfs, '') 202 full_path1 = elf1 203 full_path2 = elf1.replace(i1.rootfs, i2.rootfs) 204 205 if full_path1 == full_path2: 206 self.logger.LogError( 207 'Error: We\'re comparing the SAME file - {0}'.format(f1)) 208 continue 209 210 command = ( 211 'objdump -d "{f1}" > {tempf1} ; ' 212 'objdump -d "{f2}" > {tempf2} ; ' 213 # Remove path string inside the dissemble 214 'sed -i \'s!{rootfs1}!!g\' {tempf1} ; ' 215 'sed -i \'s!{rootfs2}!!g\' {tempf2} ; ' 216 'diff {tempf1} {tempf2} 1>/dev/null 2>&1').format( 217 f1=full_path1, 218 f2=full_path2, 219 rootfs1=i1.rootfs, 220 rootfs2=i2.rootfs, 221 tempf1=self.tempf1, 222 tempf2=self.tempf2) 223 ret = cmde.RunCommand(command, print_to_console=False) 224 if ret != 0: 225 self.logger.LogOutput( 226 '*** Not match - "{0}" "{1}"'.format(full_path1, full_path2)) 227 mismatch_list.append(f1) 228 if self.diff_file: 229 command = ('echo "Diffs of disassemble of \"{f1}\" and \"{f2}\"" ' 230 '>> {diff_file} ; diff {tempf1} {tempf2} ' 231 '>> {diff_file}').format( 232 f1=full_path1, 233 f2=full_path2, 234 diff_file=self.diff_file, 235 tempf1=self.tempf1, 236 tempf2=self.tempf2) 237 cmde.RunCommand(command, print_to_console=False) 238 else: 239 match_count += 1 240 ## End of comparing every elf files. 241 242 if not mismatch_list: 243 self.logger.LogOutput( 244 '** COOL, ALL {0} BINARIES MATCHED!! **'.format(match_count)) 245 return True 246 247 mismatch_str = 'Found {0} mismatch:\n'.format(len(mismatch_list)) 248 for b in mismatch_list: 249 mismatch_str += '\t' + b + '\n' 250 251 self.logger.LogOutput(mismatch_str) 252 return False 253 254 255def Main(argv): 256 """The main function.""" 257 258 command_executer.InitCommandExecuter() 259 images = [] 260 261 parser = argparse.ArgumentParser() 262 parser.add_argument( 263 '--no_unmount', 264 action='store_true', 265 dest='no_unmount', 266 default=False, 267 help='Do not unmount after finish, this is useful for debugging.') 268 parser.add_argument( 269 '--chromeos_root', 270 dest='chromeos_root', 271 default=None, 272 action='store', 273 help=('[Optional] Specify a chromeos tree instead of ' 274 'deducing it from image path so that we can compare ' 275 '2 images that are downloaded.')) 276 parser.add_argument( 277 '--mount_basename', 278 dest='mount_basename', 279 default=None, 280 action='store', 281 help=('Specify a meaningful name for the mount point. With this being ' 282 'set, the mount points would be "/tmp/mount_basename.x.rootfs" ' 283 ' and "/tmp/mount_basename.x.stateful". (x is 1 or 2).')) 284 parser.add_argument( 285 '--diff_file', 286 dest='diff_file', 287 default=None, 288 help='Dumping all the diffs (if any) to the diff file') 289 parser.add_argument( 290 '--image1', 291 dest='image1', 292 default=None, 293 required=True, 294 help=('Image 1 file name.')) 295 parser.add_argument( 296 '--image2', 297 dest='image2', 298 default=None, 299 required=True, 300 help=('Image 2 file name.')) 301 options = parser.parse_args(argv[1:]) 302 303 if options.mount_basename and options.mount_basename.find('/') >= 0: 304 logger.GetLogger().LogError( 305 '"--mount_basename" must be a name, not a path.') 306 parser.print_help() 307 return 1 308 309 result = False 310 image_comparator = None 311 try: 312 for i, image_path in enumerate([options.image1, options.image2], start=1): 313 image_path = os.path.realpath(image_path) 314 if not os.path.isfile(image_path): 315 logger.getLogger().LogError('"{0}" is not a file.'.format(image_path)) 316 return 1 317 318 chromeos_root = None 319 if options.chromeos_root: 320 chromeos_root = options.chromeos_root 321 else: 322 ## Deduce chromeos root from image 323 t = image_path 324 while t != '/': 325 if misc.IsChromeOsTree(t): 326 break 327 t = os.path.dirname(t) 328 if misc.IsChromeOsTree(t): 329 chromeos_root = t 330 331 if not chromeos_root: 332 logger.GetLogger().LogError( 333 'Please provide a valid chromeos root via --chromeos_root') 334 return 1 335 336 image = CrosImage(image_path, chromeos_root, options.no_unmount) 337 338 if options.mount_basename: 339 mount_basename = '{basename}.{index}'.format( 340 basename=options.mount_basename, index=i) 341 else: 342 mount_basename = None 343 344 if image.MountImage(mount_basename): 345 images.append(image) 346 image.FindElfFiles() 347 348 if len(images) == 2: 349 image_comparator = ImageComparator(images, options.diff_file) 350 result = image_comparator.CompareImages() 351 finally: 352 for image in images: 353 image.UnmountImage() 354 if image_comparator: 355 image_comparator.Cleanup() 356 357 return 0 if result else 1 358 359 360if __name__ == '__main__': 361 Main(sys.argv) 362