| #!/usr/bin/env python3 | |
| # Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file | |
| # for details. All rights reserved. Use of this source code is governed by a | |
| # BSD-style license that can be found in the LICENSE file. | |
| import argparse | |
| import os | |
| import shutil | |
| import subprocess | |
| import sys | |
| sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| import apk_masseur | |
| import apk_utils | |
| import extractmarker | |
| import toolhelper | |
| import utils | |
| import zip_utils | |
| def parse_options(argv): | |
| result = argparse.ArgumentParser( | |
| description='Instrument the dex files of a given apk to print what is ' | |
| 'executed.') | |
| result.add_argument('--apk', help='Path to the .apk', required=True) | |
| result.add_argument('--debug-agent', | |
| '--debug_agent', | |
| action='store_true', | |
| default=False, | |
| help='Enable Java debug agent on port 5005 and suspend compilation') | |
| result.add_argument('--dex-files', | |
| action='append', | |
| help='Name of dex files to instrument') | |
| result.add_argument('--discard', | |
| action='append', | |
| help='Name of dex files to discard') | |
| result.add_argument('--print-boxing-unboxing-callsites', | |
| action='store_true', | |
| default=False, | |
| help='Print caller->callee edges for primitive boxing') | |
| result.add_argument('--print-executed-classes-and-methods', | |
| action='store_true', | |
| default=False, | |
| help='Print the classes and methods that are executed') | |
| result.add_argument('--out', | |
| help='Destination of resulting apk', | |
| required=True) | |
| options, args = result.parse_known_args(argv) | |
| return options, args | |
| def add_instrumented_dex(dex_file, instrumented_dex_index, instrumented_dir): | |
| dex_name = get_dex_name(instrumented_dex_index) | |
| destination = os.path.join(instrumented_dir, dex_name) | |
| shutil.move(dex_file, destination) | |
| def get_dex_name(dex_index): | |
| assert dex_index > 0 | |
| return 'classes.dex' if dex_index == 1 else ('classes%s.dex' % dex_index) | |
| def instrument_dex_file(dex_file, include_instrumentation_server, options, | |
| tmp_dir): | |
| d8_cmd = [ | |
| 'java', '-cp', utils.R8_JAR, | |
| '-Dcom.android.tools.r8.instrumentation.tag=R8' | |
| ] | |
| if options.debug_agent: | |
| d8_cmd.append( | |
| '-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005' | |
| ) | |
| if options.print_boxing_unboxing_callsites: | |
| d8_cmd.append( | |
| '-Dcom.android.tools.r8.instrumentation.boxingunboxingcallsites=1') | |
| if options.print_executed_classes_and_methods: | |
| d8_cmd.append( | |
| '-Dcom.android.tools.r8.instrumentation.executedclassesandmethods=1' | |
| ) | |
| if not include_instrumentation_server: | |
| # We avoid injecting the InstrumentationServer by specifying it should only | |
| # be added if foo.bar.Baz is in the program. | |
| d8_cmd.append( | |
| '-Dcom.android.tools.r8.instrumentation.syntheticservercontext=foo.bar.Baz' | |
| ) | |
| d8_cmd.extend([ | |
| 'com.android.tools.r8.D8', '--min-api', | |
| str(apk_utils.get_min_api(options.apk)), '--output', tmp_dir, | |
| '--release', dex_file | |
| ]) | |
| subprocess.check_call(d8_cmd) | |
| instrumented_dex_files = [] | |
| instrumented_dex_index = 1 | |
| while True: | |
| instrumented_dex_name = get_dex_name(instrumented_dex_index) | |
| instrumented_dex_file = os.path.join(tmp_dir, instrumented_dex_name) | |
| if not os.path.exists(instrumented_dex_file): | |
| break | |
| instrumented_dex_files.append(instrumented_dex_file) | |
| instrumented_dex_index = instrumented_dex_index + 1 | |
| assert len(instrumented_dex_files) > 0 | |
| return instrumented_dex_files | |
| def should_discard_dex_file(dex_name, options): | |
| return options.discard is not None and dex_name in options.discard | |
| def should_instrument_dex_file(dex_name, options): | |
| return options.dex_files is not None and dex_name in options.dex_files | |
| def main(argv): | |
| options, args = parse_options(argv) | |
| with utils.TempDir() as tmp_dir: | |
| # Extract the dex files of the apk. | |
| uninstrumented_dir = os.path.join(tmp_dir, 'uninstrumented') | |
| os.mkdir(uninstrumented_dir) | |
| dex_predicate = \ | |
| lambda name : name.startswith('classes') and name.endswith('.dex') | |
| zip_utils.extract_all_that_matches(options.apk, uninstrumented_dir, | |
| dex_predicate) | |
| # Instrument each dex one by one. | |
| instrumented_dir = os.path.join(tmp_dir, 'instrumented') | |
| os.mkdir(instrumented_dir) | |
| include_instrumentation_server = True | |
| instrumented_dex_index = 1 | |
| uninstrumented_dex_index = 1 | |
| while True: | |
| dex_name = get_dex_name(uninstrumented_dex_index) | |
| dex_file = os.path.join(uninstrumented_dir, dex_name) | |
| if not os.path.exists(dex_file): | |
| break | |
| if not should_discard_dex_file(dex_name, options): | |
| if should_instrument_dex_file(dex_name, options): | |
| with utils.TempDir() as tmp_instrumentation_dir: | |
| instrumented_dex_files = \ | |
| instrument_dex_file( | |
| dex_file, | |
| include_instrumentation_server, | |
| options, | |
| tmp_instrumentation_dir) | |
| for instrumented_dex_file in instrumented_dex_files: | |
| add_instrumented_dex(instrumented_dex_file, | |
| instrumented_dex_index, | |
| instrumented_dir) | |
| instrumented_dex_index = instrumented_dex_index + 1 | |
| include_instrumentation_server = False | |
| else: | |
| add_instrumented_dex(dex_file, instrumented_dex_index, | |
| instrumented_dir) | |
| instrumented_dex_index = instrumented_dex_index + 1 | |
| uninstrumented_dex_index = uninstrumented_dex_index + 1 | |
| assert instrumented_dex_index > 1 | |
| # Masseur APK. | |
| apk_masseur.masseur(options.apk, dex=instrumented_dir, out=options.out) | |
| if __name__ == '__main__': | |
| sys.exit(main(sys.argv[1:])) |