blob: 9b2479a2a73473ae86551c1654bebb1b4f84b742 [file] [log] [blame]
Alex Lightbde70602020-12-30 14:27:09 -08001#!/usr/bin/python3
2#
3# Copyright (C) 2021 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#
18# Generates profiles from the set of all methods in a given set of dex/jars and
19# bisects to find minimal repro sets.
20#
21
22import shlex
23import argparse
24import pylibdexfile
25import math
26import subprocess
27from collections import namedtuple
28import sys
29import random
30import os
31
32ApkEntry = namedtuple("ApkEntry", ["file", "location"])
33
34
35def get_parser():
36 parser = argparse.ArgumentParser(
37 description="Bisect profile contents. We will wait while the user runs test"
38 )
39
40 class ApkAction(argparse.Action):
41
42 def __init__(self, option_strings, dest, **kwargs):
43 super(ApkAction, self).__init__(option_strings, dest, **kwargs)
44
45 def __call__(self, parser, namespace, values, option_string=None):
46 lst = getattr(namespace, self.dest)
47 if lst is None:
48 setattr(namespace, self.dest, [])
49 lst = getattr(namespace, self.dest)
50 if len(values) == 1:
51 values = (values[0], values[0])
52 assert len(values) == 2, values
53 lst.append(ApkEntry(*values))
54
55 apks = parser.add_argument_group(title="APK selection")
56 apks.add_argument(
57 "--apk",
58 action=ApkAction,
59 dest="apks",
60 nargs=1,
61 default=[],
62 help="an apk/dex/jar to get methods from. Uses same path as location. " +
63 "Use --apk-and-location if this isn't desired."
64 )
65 apks.add_argument(
66 "--apk-and-location",
67 action=ApkAction,
68 nargs=2,
69 dest="apks",
70 help="an apk/dex/jar + location to get methods from."
71 )
72 profiles = parser.add_argument_group(
73 title="Profile selection").add_mutually_exclusive_group()
74 profiles.add_argument(
75 "--input-text-profile", help="a text profile to use for bisect")
76 profiles.add_argument("--input-profile", help="a profile to use for bisect")
77 parser.add_argument(
78 "--output-source", help="human readable file create the profile from")
79 parser.add_argument("output_file", help="file we will write the profiles to")
80 return parser
81
82
83def dump_files(meths, args, output):
84 for m in meths:
85 print("HS{}".format(m), file=output)
86 output.flush()
87 profman_args = [
88 "profmand", "--reference-profile-file={}".format(args.output_file),
89 "--create-profile-from={}".format(args.output_source)
90 ]
91 print(" ".join(map(shlex.quote, profman_args)))
92 for apk in args.apks:
93 profman_args += [
94 "--apk={}".format(apk.file), "--dex-location={}".format(apk.location)
95 ]
96 profman = subprocess.run(profman_args)
97 profman.check_returncode()
98
99
100def run_test(meths, args):
101 with open(args.output_source, "wt") as output:
102 dump_files(meths, args, output)
103 print("Currently testing {} methods. ~{} rounds to go.".format(
104 len(meths), 1 + math.floor(math.log2(len(meths)))))
105 while True:
106 answer = input("Does the file at {} cause the issue (y/n):".format(
107 args.output_file))
108 if answer[0].lower() == "y":
109 return "y"
110 elif answer[0].lower() == "n":
111 return "n"
112 else:
113 print("Please enter 'y' or 'n' only!")
114
115
116def main():
117 parser = get_parser()
118 args = parser.parse_args()
119 if args.output_source is None:
120 fdnum = os.memfd_create("tempfile_profile")
121 args.output_source = "/proc/{}/fd/{}".format(os.getpid(), fdnum)
122 all_dexs = list()
123 for f in args.apks:
124 try:
125 all_dexs.append(pylibdexfile.FileDexFile(f.file, f.location))
126 except Exception as e1:
127 try:
128 all_dexs += pylibdexfile.OpenJar(f.file)
129 except Exception as e2:
130 parser.error("Failed to open file: {}. errors were {} and {}".format(
131 f.file, e1, e2))
132 if args.input_profile is not None:
133 profman_args = [
134 "profmand", "--dump-classes-and-methods",
135 "--profile-file={}".format(args.input_profile)
136 ]
137 for apk in args.apks:
138 profman_args.append("--apk={}".format(apk.file))
139 print(" ".join(map(shlex.quote, profman_args)))
140 res = subprocess.run(
141 profman_args, capture_output=True, universal_newlines=True)
142 res.check_returncode()
143 meth_list = list(filter(lambda a: a != "", res.stdout.split()))
144 elif args.input_text_profile is not None:
145 with open(args.input_text_profile, "rt") as inp:
146 meth_list = list(filter(lambda a: a != "", inp.readlines()))
147 else:
148 all_methods = set()
149 for d in all_dexs:
150 for m in d.methods:
151 all_methods.add(m.descriptor)
152 meth_list = list(all_methods)
153 print("Found {} methods. Will take ~{} iterations".format(
154 len(meth_list), 1 + math.floor(math.log2(len(meth_list)))))
155 print(
156 "type 'yes' if the behavior you are looking for is present (i.e. the compiled code crashes " +
157 "or something)"
158 )
159 print("Performing single check with all methods")
160 result = run_test(meth_list, args)
161 if result[0].lower() != "y":
162 cont = input(
163 "The behavior you were looking for did not occur when run against all methods. Continue " +
164 "(yes/no)? "
165 )
166 if cont[0].lower() != "y":
167 print("Aborting!")
168 sys.exit(1)
169 needs_dump = False
170 while len(meth_list) > 1:
171 test_methods = list(meth_list[0:len(meth_list) // 2])
172 result = run_test(test_methods, args)
173 if result[0].lower() == "y":
174 meth_list = test_methods
175 needs_dump = False
176 else:
177 meth_list = meth_list[len(meth_list) // 2:]
178 needs_dump = True
179 if needs_dump:
180 with open(args.output_source, "wt") as output:
181 dump_files(meth_list, args, output)
182 print("Found result!")
183 print("{}".format(meth_list[0]))
184 print("Leaving profile at {} and text profile at {}".format(
185 args.output_file, args.output_source))
186
187
188if __name__ == "__main__":
189 main()