blob: 7e2870f0bbf67ab40dc20b04a27271ad67d340ee [file] [log] [blame]
#!/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:]))