1#!/usr/bin/env python3 2# 3# Copyright (C) 2016 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17"""Performs bisection bug search on methods and optimizations. 18 19See README.md. 20 21Example usage: 22./bisection-search.py -cp classes.dex --expected-output output Test 23""" 24 25import abc 26import argparse 27import os 28import re 29import shlex 30import sys 31 32from subprocess import call 33from tempfile import NamedTemporaryFile 34 35sys.path.append(os.path.dirname(os.path.dirname( 36 os.path.realpath(__file__)))) 37 38from common.common import DeviceTestEnv 39from common.common import FatalError 40from common.common import GetEnvVariableOrError 41from common.common import HostTestEnv 42from common.common import LogSeverity 43from common.common import RetCode 44 45 46# Passes that are never disabled during search process because disabling them 47# would compromise correctness. 48MANDATORY_PASSES = ['dex_cache_array_fixups_arm', 49 'dex_cache_array_fixups_mips', 50 'instruction_simplifier$before_codegen', 51 'pc_relative_fixups_x86', 52 'pc_relative_fixups_mips', 53 'x86_memory_operand_generation'] 54 55# Passes that show up as optimizations in compiler verbose output but aren't 56# driven by run-passes mechanism. They are mandatory and will always run, we 57# never pass them to --run-passes. 58NON_PASSES = ['builder', 'prepare_for_register_allocation', 59 'liveness', 'register'] 60 61# If present in raw cmd, this tag will be replaced with runtime arguments 62# controlling the bisection search. Otherwise arguments will be placed on second 63# position in the command. 64RAW_CMD_RUNTIME_ARGS_TAG = '{ARGS}' 65 66# Default core image path relative to ANDROID_HOST_OUT. 67DEFAULT_IMAGE_RELATIVE_PATH = '/framework/core.art' 68 69class Dex2OatWrapperTestable(object): 70 """Class representing a testable compilation. 71 72 Accepts filters on compiled methods and optimization passes. 73 """ 74 75 def __init__(self, base_cmd, test_env, expected_retcode=None, 76 output_checker=None, verbose=False): 77 """Constructor. 78 79 Args: 80 base_cmd: list of strings, base command to run. 81 test_env: ITestEnv. 82 expected_retcode: RetCode, expected normalized return code. 83 output_checker: IOutputCheck, output checker. 84 verbose: bool, enable verbose output. 85 """ 86 self._base_cmd = base_cmd 87 self._test_env = test_env 88 self._expected_retcode = expected_retcode 89 self._output_checker = output_checker 90 self._compiled_methods_path = self._test_env.CreateFile('compiled_methods') 91 self._passes_to_run_path = self._test_env.CreateFile('run_passes') 92 self._verbose = verbose 93 if RAW_CMD_RUNTIME_ARGS_TAG in self._base_cmd: 94 self._arguments_position = self._base_cmd.index(RAW_CMD_RUNTIME_ARGS_TAG) 95 self._base_cmd.pop(self._arguments_position) 96 else: 97 self._arguments_position = 1 98 99 def Test(self, compiled_methods, passes_to_run=None): 100 """Tests compilation with compiled_methods and run_passes switches active. 101 102 If compiled_methods is None then compiles all methods. 103 If passes_to_run is None then runs default passes. 104 105 Args: 106 compiled_methods: list of strings representing methods to compile or None. 107 passes_to_run: list of strings representing passes to run or None. 108 109 Returns: 110 True if test passes with given settings. False otherwise. 111 """ 112 if self._verbose: 113 print('Testing methods: {0} passes: {1}.'.format( 114 compiled_methods, passes_to_run)) 115 cmd = self._PrepareCmd(compiled_methods=compiled_methods, 116 passes_to_run=passes_to_run) 117 (output, ret_code) = self._test_env.RunCommand( 118 cmd, LogSeverity.ERROR) 119 res = True 120 if self._expected_retcode: 121 res = self._expected_retcode == ret_code 122 if self._output_checker: 123 res = res and self._output_checker.Check(output) 124 if self._verbose: 125 print('Test passed: {0}.'.format(res)) 126 return res 127 128 def GetAllMethods(self): 129 """Get methods compiled during the test. 130 131 Returns: 132 List of strings representing methods compiled during the test. 133 134 Raises: 135 FatalError: An error occurred when retrieving methods list. 136 """ 137 cmd = self._PrepareCmd() 138 (output, _) = self._test_env.RunCommand(cmd, LogSeverity.INFO) 139 match_methods = re.findall(r'Building ([^\n]+)\n', output) 140 if not match_methods: 141 raise FatalError('Failed to retrieve methods list. ' 142 'Not recognized output format.') 143 return match_methods 144 145 def GetAllPassesForMethod(self, compiled_method): 146 """Get all optimization passes ran for a method during the test. 147 148 Args: 149 compiled_method: string representing method to compile. 150 151 Returns: 152 List of strings representing passes ran for compiled_method during test. 153 154 Raises: 155 FatalError: An error occurred when retrieving passes list. 156 """ 157 cmd = self._PrepareCmd(compiled_methods=[compiled_method]) 158 (output, _) = self._test_env.RunCommand(cmd, LogSeverity.INFO) 159 match_passes = re.findall(r'Starting pass: ([^\n]+)\n', output) 160 if not match_passes: 161 raise FatalError('Failed to retrieve passes list. ' 162 'Not recognized output format.') 163 return [p for p in match_passes if p not in NON_PASSES] 164 165 def _PrepareCmd(self, compiled_methods=None, passes_to_run=None): 166 """Prepare command to run.""" 167 cmd = self._base_cmd[0:self._arguments_position] 168 # insert additional arguments before the first argument 169 if passes_to_run is not None: 170 self._test_env.WriteLines(self._passes_to_run_path, passes_to_run) 171 cmd += ['-Xcompiler-option', '--run-passes={0}'.format( 172 self._passes_to_run_path)] 173 cmd += ['-Xcompiler-option', '--runtime-arg', '-Xcompiler-option', 174 '-verbose:compiler', '-Xcompiler-option', '-j1'] 175 cmd += self._base_cmd[self._arguments_position:] 176 return cmd 177 178 179class IOutputCheck(object): 180 """Abstract output checking class. 181 182 Checks if output is correct. 183 """ 184 __meta_class__ = abc.ABCMeta 185 186 @abc.abstractmethod 187 def Check(self, output): 188 """Check if output is correct. 189 190 Args: 191 output: string, output to check. 192 193 Returns: 194 boolean, True if output is correct, False otherwise. 195 """ 196 197 198class EqualsOutputCheck(IOutputCheck): 199 """Concrete output checking class checking for equality to expected output.""" 200 201 def __init__(self, expected_output): 202 """Constructor. 203 204 Args: 205 expected_output: string, expected output. 206 """ 207 self._expected_output = expected_output 208 209 def Check(self, output): 210 """See base class.""" 211 return self._expected_output == output 212 213 214class ExternalScriptOutputCheck(IOutputCheck): 215 """Concrete output checking class calling an external script. 216 217 The script should accept two arguments, path to expected output and path to 218 program output. It should exit with 0 return code if outputs are equivalent 219 and with different return code otherwise. 220 """ 221 222 def __init__(self, script_path, expected_output_path, logfile): 223 """Constructor. 224 225 Args: 226 script_path: string, path to checking script. 227 expected_output_path: string, path to file with expected output. 228 logfile: file handle, logfile. 229 """ 230 self._script_path = script_path 231 self._expected_output_path = expected_output_path 232 self._logfile = logfile 233 234 def Check(self, output): 235 """See base class.""" 236 ret_code = None 237 with NamedTemporaryFile(mode='w', delete=False) as temp_file: 238 temp_file.write(output) 239 temp_file.flush() 240 ret_code = call( 241 [self._script_path, self._expected_output_path, temp_file.name], 242 stdout=self._logfile, stderr=self._logfile, universal_newlines=True) 243 return ret_code == 0 244 245 246def BinarySearch(start, end, test): 247 """Binary search integers using test function to guide the process.""" 248 while start < end: 249 mid = (start + end) // 2 250 if test(mid): 251 start = mid + 1 252 else: 253 end = mid 254 return start 255 256 257def FilterPasses(passes, cutoff_idx): 258 """Filters passes list according to cutoff_idx but keeps mandatory passes.""" 259 return [opt_pass for idx, opt_pass in enumerate(passes) 260 if opt_pass in MANDATORY_PASSES or idx < cutoff_idx] 261 262 263def BugSearch(testable): 264 """Find buggy (method, optimization pass) pair for a given testable. 265 266 Args: 267 testable: Dex2OatWrapperTestable. 268 269 Returns: 270 (string, string) tuple. First element is name of method which when compiled 271 exposes test failure. Second element is name of optimization pass such that 272 for aforementioned method running all passes up to and excluding the pass 273 results in test passing but running all passes up to and including the pass 274 results in test failing. 275 276 (None, None) if test passes when compiling all methods. 277 (string, None) if a method is found which exposes the failure, but the 278 failure happens even when running just mandatory passes. 279 280 Raises: 281 FatalError: Testable fails with no methods compiled. 282 AssertionError: Method failed for all passes when bisecting methods, but 283 passed when bisecting passes. Possible sporadic failure. 284 """ 285 all_methods = testable.GetAllMethods() 286 faulty_method_idx = BinarySearch( 287 0, 288 len(all_methods) + 1, 289 lambda mid: testable.Test(all_methods[0:mid])) 290 if faulty_method_idx == len(all_methods) + 1: 291 return (None, None) 292 if faulty_method_idx == 0: 293 raise FatalError('Testable fails with no methods compiled.') 294 faulty_method = all_methods[faulty_method_idx - 1] 295 all_passes = testable.GetAllPassesForMethod(faulty_method) 296 faulty_pass_idx = BinarySearch( 297 0, 298 len(all_passes) + 1, 299 lambda mid: testable.Test([faulty_method], 300 FilterPasses(all_passes, mid))) 301 if faulty_pass_idx == 0: 302 return (faulty_method, None) 303 assert faulty_pass_idx != len(all_passes) + 1, ('Method must fail for some ' 304 'passes.') 305 faulty_pass = all_passes[faulty_pass_idx - 1] 306 return (faulty_method, faulty_pass) 307 308 309def PrepareParser(): 310 """Prepares argument parser.""" 311 parser = argparse.ArgumentParser( 312 description='Tool for finding compiler bugs. Either --raw-cmd or both ' 313 '-cp and --class are required.') 314 command_opts = parser.add_argument_group('dalvikvm command options') 315 command_opts.add_argument('-cp', '--classpath', type=str, help='classpath') 316 command_opts.add_argument('--class', dest='classname', type=str, 317 help='name of main class') 318 command_opts.add_argument('--lib', type=str, default='libart.so', 319 help='lib to use, default: libart.so') 320 command_opts.add_argument('--dalvikvm-option', dest='dalvikvm_opts', 321 metavar='OPT', nargs='*', default=[], 322 help='additional dalvikvm option') 323 command_opts.add_argument('--arg', dest='test_args', nargs='*', default=[], 324 metavar='ARG', help='argument passed to test') 325 command_opts.add_argument('--image', type=str, help='path to image') 326 command_opts.add_argument('--raw-cmd', type=str, 327 help='bisect with this command, ignore other ' 328 'command options') 329 bisection_opts = parser.add_argument_group('bisection options') 330 bisection_opts.add_argument('--64', dest='x64', action='store_true', 331 default=False, help='x64 mode') 332 bisection_opts.add_argument( 333 '--device', action='store_true', default=False, help='run on device') 334 bisection_opts.add_argument( 335 '--device-serial', help='device serial number, implies --device') 336 bisection_opts.add_argument('--expected-output', type=str, 337 help='file containing expected output') 338 bisection_opts.add_argument( 339 '--expected-retcode', type=str, help='expected normalized return code', 340 choices=[RetCode.SUCCESS.name, RetCode.TIMEOUT.name, RetCode.ERROR.name]) 341 bisection_opts.add_argument( 342 '--check-script', type=str, 343 help='script comparing output and expected output') 344 bisection_opts.add_argument( 345 '--logfile', type=str, help='custom logfile location') 346 bisection_opts.add_argument('--cleanup', action='store_true', 347 default=False, help='clean up after bisecting') 348 bisection_opts.add_argument('--timeout', type=int, default=60, 349 help='if timeout seconds pass assume test failed') 350 bisection_opts.add_argument('--verbose', action='store_true', 351 default=False, help='enable verbose output') 352 return parser 353 354 355def PrepareBaseCommand(args, classpath): 356 """Prepares base command used to run test.""" 357 if args.raw_cmd: 358 return shlex.split(args.raw_cmd) 359 else: 360 base_cmd = ['dalvikvm64'] if args.x64 else ['dalvikvm32'] 361 if not args.device: 362 base_cmd += ['-XXlib:{0}'.format(args.lib)] 363 if not args.image: 364 image_path = (GetEnvVariableOrError('ANDROID_HOST_OUT') + 365 DEFAULT_IMAGE_RELATIVE_PATH) 366 else: 367 image_path = args.image 368 base_cmd += ['-Ximage:{0}'.format(image_path)] 369 if args.dalvikvm_opts: 370 base_cmd += args.dalvikvm_opts 371 base_cmd += ['-cp', classpath, args.classname] + args.test_args 372 return base_cmd 373 374 375def main(): 376 # Parse arguments 377 parser = PrepareParser() 378 args = parser.parse_args() 379 if not args.raw_cmd and (not args.classpath or not args.classname): 380 parser.error('Either --raw-cmd or both -cp and --class are required') 381 if args.device_serial: 382 args.device = True 383 if args.expected_retcode: 384 args.expected_retcode = RetCode[args.expected_retcode] 385 if not args.expected_retcode and not args.check_script: 386 args.expected_retcode = RetCode.SUCCESS 387 388 # Prepare environment 389 classpath = args.classpath 390 if args.device: 391 test_env = DeviceTestEnv( 392 'bisection_search_', args.cleanup, args.logfile, args.timeout, 393 args.device_serial) 394 if classpath: 395 classpath = test_env.PushClasspath(classpath) 396 else: 397 test_env = HostTestEnv( 398 'bisection_search_', args.cleanup, args.logfile, args.timeout, args.x64) 399 base_cmd = PrepareBaseCommand(args, classpath) 400 output_checker = None 401 if args.expected_output: 402 if args.check_script: 403 output_checker = ExternalScriptOutputCheck( 404 args.check_script, args.expected_output, test_env.logfile) 405 else: 406 with open(args.expected_output, 'r') as expected_output_file: 407 output_checker = EqualsOutputCheck(expected_output_file.read()) 408 409 # Perform the search 410 try: 411 testable = Dex2OatWrapperTestable(base_cmd, test_env, args.expected_retcode, 412 output_checker, args.verbose) 413 if testable.Test(compiled_methods=[]): 414 (method, opt_pass) = BugSearch(testable) 415 else: 416 print('Testable fails with no methods compiled.') 417 sys.exit(1) 418 except Exception as e: 419 print('Error occurred.\nLogfile: {0}'.format(test_env.logfile.name)) 420 test_env.logfile.write('Exception: {0}\n'.format(e)) 421 raise 422 423 # Report results 424 if method is None: 425 print('Couldn\'t find any bugs.') 426 elif opt_pass is None: 427 print('Faulty method: {0}. Fails with just mandatory passes.'.format( 428 method)) 429 else: 430 print('Faulty method and pass: {0}, {1}.'.format(method, opt_pass)) 431 print('Logfile: {0}'.format(test_env.logfile.name)) 432 sys.exit(0) 433 434 435if __name__ == '__main__': 436 main() 437