blob: 7e2870f0bbf67ab40dc20b04a27271ad67d340ee [file] [log] [blame]
Christoffer Quist Adamseneb29bfe2023-02-21 14:50:22 +01001#!/usr/bin/env python3
2# Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
3# for details. All rights reserved. Use of this source code is governed by a
4# BSD-style license that can be found in the LICENSE file.
5
6import argparse
7import os
8import shutil
9import subprocess
10import sys
11
12sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13
14import apk_masseur
15import apk_utils
16import extractmarker
17import toolhelper
18import utils
19import zip_utils
20
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020021
Christoffer Quist Adamseneb29bfe2023-02-21 14:50:22 +010022def parse_options(argv):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020023 result = argparse.ArgumentParser(
24 description='Instrument the dex files of a given apk to print what is '
25 'executed.')
26 result.add_argument('--apk', help='Path to the .apk', required=True)
27 result.add_argument('--dex-files',
28 action='append',
29 help='Name of dex files to instrument')
30 result.add_argument('--discard',
31 action='append',
32 help='Name of dex files to discard')
Christoffer Adamsen6a9ed942024-04-30 10:54:09 +020033 result.add_argument('--print-boxing-unboxing-callsites',
34 action='store_true',
35 default=False,
36 help='Print caller->callee edges for primitive boxing')
37 result.add_argument('--print-executed-classes-and-methods',
38 action='store_true',
39 default=False,
40 help='Print the classes and methods that are executed')
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020041 result.add_argument('--out',
42 help='Destination of resulting apk',
43 required=True)
44 options, args = result.parse_known_args(argv)
45 return options, args
46
Christoffer Quist Adamseneb29bfe2023-02-21 14:50:22 +010047
48def add_instrumented_dex(dex_file, instrumented_dex_index, instrumented_dir):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020049 dex_name = get_dex_name(instrumented_dex_index)
50 destination = os.path.join(instrumented_dir, dex_name)
51 shutil.move(dex_file, destination)
52
Christoffer Quist Adamseneb29bfe2023-02-21 14:50:22 +010053
54def get_dex_name(dex_index):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020055 assert dex_index > 0
56 return 'classes.dex' if dex_index == 1 else ('classes%s.dex' % dex_index)
Christoffer Quist Adamseneb29bfe2023-02-21 14:50:22 +010057
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020058
59def instrument_dex_file(dex_file, include_instrumentation_server, options,
60 tmp_dir):
61 d8_cmd = [
62 'java', '-cp', utils.R8_JAR,
Christoffer Adamsen6a9ed942024-04-30 10:54:09 +020063 '-Dcom.android.tools.r8.instrumentation.tag=R8'
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020064 ]
Christoffer Adamsen6a9ed942024-04-30 10:54:09 +020065 if options.print_boxing_unboxing_callsites:
66 callsites = ':'.join([
67 # Boxing
68 "Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean;",
69 "Ljava/lang/Byte;->valueOf(B)Ljava/lang/Byte;",
70 "Ljava/lang/Character;->valueOf(C)Ljava/lang/Character;",
71 "Ljava/lang/Double;->valueOf(D)Ljava/lang/Double;",
72 "Ljava/lang/Float;->valueOf(F)Ljava/lang/Float;",
73 "Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;",
74 "Ljava/lang/Long;->valueOf(J)Ljava/lang/Long;",
75 "Ljava/lang/Short;->valueOf(S)Ljava/lang/Short;",
76 # Unboxing
77 "Ljava/lang/Boolean;->booleanValue()Z",
78 "Ljava/lang/Byte;->byteValue()B",
79 "Ljava/lang/Character;->charValue()C",
80 "Ljava/lang/Double;->doubleValue()D",
81 "Ljava/lang/Float;->floatValue()F",
82 "Ljava/lang/Integer;->intValue()I",
83 "Ljava/lang/Long;->longValue()J",
84 "Ljava/lang/Short;->shortValue()S"
85 ])
86 d8_cmd.append(
87 '-Dcom.android.tools.r8.instrumentation.callsites='
88 + callsites)
89 if options.print_executed_classes_and_methods:
90 d8_cmd.append(
91 '-Dcom.android.tools.r8.instrumentation.executedclassesandmethods=1')
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020092 if not include_instrumentation_server:
93 # We avoid injecting the InstrumentationServer by specifying it should only
94 # be added if foo.bar.Baz is in the program.
95 d8_cmd.append(
Christoffer Adamsen6a9ed942024-04-30 10:54:09 +020096 '-Dcom.android.tools.r8.instrumentation.syntheticservercontext=foo.bar.Baz'
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020097 )
98 d8_cmd.extend([
99 'com.android.tools.r8.D8', '--min-api',
100 str(apk_utils.get_min_api(options.apk)), '--output', tmp_dir,
101 '--release', dex_file
102 ])
103 subprocess.check_call(d8_cmd)
104 instrumented_dex_files = []
105 instrumented_dex_index = 1
106 while True:
107 instrumented_dex_name = get_dex_name(instrumented_dex_index)
108 instrumented_dex_file = os.path.join(tmp_dir, instrumented_dex_name)
109 if not os.path.exists(instrumented_dex_file):
110 break
111 instrumented_dex_files.append(instrumented_dex_file)
112 instrumented_dex_index = instrumented_dex_index + 1
113 assert len(instrumented_dex_files) > 0
114 return instrumented_dex_files
115
Christoffer Quist Adamseneb29bfe2023-02-21 14:50:22 +0100116
117def should_discard_dex_file(dex_name, options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200118 return options.discard is not None and dex_name in options.discard
119
Christoffer Quist Adamseneb29bfe2023-02-21 14:50:22 +0100120
121def should_instrument_dex_file(dex_name, options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200122 return options.dex_files is not None and dex_name in options.dex_files
123
Christoffer Quist Adamseneb29bfe2023-02-21 14:50:22 +0100124
125def main(argv):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200126 options, args = parse_options(argv)
127 with utils.TempDir() as tmp_dir:
128 # Extract the dex files of the apk.
129 uninstrumented_dir = os.path.join(tmp_dir, 'uninstrumented')
130 os.mkdir(uninstrumented_dir)
Christoffer Quist Adamseneb29bfe2023-02-21 14:50:22 +0100131
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200132 dex_predicate = \
133 lambda name : name.startswith('classes') and name.endswith('.dex')
134 zip_utils.extract_all_that_matches(options.apk, uninstrumented_dir,
135 dex_predicate)
Christoffer Quist Adamseneb29bfe2023-02-21 14:50:22 +0100136
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200137 # Instrument each dex one by one.
138 instrumented_dir = os.path.join(tmp_dir, 'instrumented')
139 os.mkdir(instrumented_dir)
Christoffer Quist Adamseneb29bfe2023-02-21 14:50:22 +0100140
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200141 include_instrumentation_server = True
142 instrumented_dex_index = 1
143 uninstrumented_dex_index = 1
144 while True:
145 dex_name = get_dex_name(uninstrumented_dex_index)
146 dex_file = os.path.join(uninstrumented_dir, dex_name)
147 if not os.path.exists(dex_file):
148 break
149 if not should_discard_dex_file(dex_name, options):
150 if should_instrument_dex_file(dex_name, options):
151 with utils.TempDir() as tmp_instrumentation_dir:
152 instrumented_dex_files = \
153 instrument_dex_file(
154 dex_file,
155 include_instrumentation_server,
156 options,
157 tmp_instrumentation_dir)
158 for instrumented_dex_file in instrumented_dex_files:
159 add_instrumented_dex(instrumented_dex_file,
160 instrumented_dex_index,
161 instrumented_dir)
162 instrumented_dex_index = instrumented_dex_index + 1
163 include_instrumentation_server = False
164 else:
165 add_instrumented_dex(dex_file, instrumented_dex_index,
166 instrumented_dir)
167 instrumented_dex_index = instrumented_dex_index + 1
168 uninstrumented_dex_index = uninstrumented_dex_index + 1
Christoffer Quist Adamseneb29bfe2023-02-21 14:50:22 +0100169
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200170 assert instrumented_dex_index > 1
Christoffer Quist Adamseneb29bfe2023-02-21 14:50:22 +0100171
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200172 # Masseur APK.
173 apk_masseur.masseur(options.apk, dex=instrumented_dir, out=options.out)
174
Christoffer Quist Adamseneb29bfe2023-02-21 14:50:22 +0100175
176if __name__ == '__main__':
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200177 sys.exit(main(sys.argv[1:]))