| #!/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('--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.print_boxing_unboxing_callsites: |
| callsites = ':'.join([ |
| # Boxing |
| "Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean;", |
| "Ljava/lang/Byte;->valueOf(B)Ljava/lang/Byte;", |
| "Ljava/lang/Character;->valueOf(C)Ljava/lang/Character;", |
| "Ljava/lang/Double;->valueOf(D)Ljava/lang/Double;", |
| "Ljava/lang/Float;->valueOf(F)Ljava/lang/Float;", |
| "Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;", |
| "Ljava/lang/Long;->valueOf(J)Ljava/lang/Long;", |
| "Ljava/lang/Short;->valueOf(S)Ljava/lang/Short;", |
| # Unboxing |
| "Ljava/lang/Boolean;->booleanValue()Z", |
| "Ljava/lang/Byte;->byteValue()B", |
| "Ljava/lang/Character;->charValue()C", |
| "Ljava/lang/Double;->doubleValue()D", |
| "Ljava/lang/Float;->floatValue()F", |
| "Ljava/lang/Integer;->intValue()I", |
| "Ljava/lang/Long;->longValue()J", |
| "Ljava/lang/Short;->shortValue()S" |
| ]) |
| d8_cmd.append( |
| '-Dcom.android.tools.r8.instrumentation.callsites=' |
| + callsites) |
| 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:])) |