Dan Albert | 2a18e9f | 2018-05-17 09:13:22 -0700 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # |
| 3 | # Copyright (C) 2015 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 | from __future__ import print_function |
| 19 | |
| 20 | import argparse |
| 21 | import contextlib |
| 22 | import os |
| 23 | import operator |
| 24 | import posixpath |
| 25 | import signal |
| 26 | import subprocess |
| 27 | import sys |
| 28 | import time |
| 29 | import xml.etree.cElementTree as ElementTree |
| 30 | |
| 31 | import logging |
| 32 | |
| 33 | # Shared functions across gdbclient.py and ndk-gdb.py. |
| 34 | # ndk-gdb is installed to $NDK/prebuilt/<platform>/bin |
| 35 | NDK_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..')) |
| 36 | sys.path.append(os.path.join(NDK_PATH, "python-packages")) |
| 37 | import adb |
| 38 | import gdbrunner |
| 39 | |
| 40 | |
| 41 | def log(msg): |
| 42 | logger = logging.getLogger(__name__) |
| 43 | logger.info(msg) |
| 44 | |
| 45 | |
| 46 | def enable_verbose_logging(): |
| 47 | logger = logging.getLogger(__name__) |
| 48 | handler = logging.StreamHandler(sys.stdout) |
| 49 | formatter = logging.Formatter() |
| 50 | |
| 51 | handler.setFormatter(formatter) |
| 52 | logger.addHandler(handler) |
| 53 | logger.propagate = False |
| 54 | |
| 55 | logger.setLevel(logging.INFO) |
| 56 | |
| 57 | |
| 58 | def error(msg): |
| 59 | sys.exit("ERROR: {}".format(msg)) |
| 60 | |
| 61 | |
| 62 | class ArgumentParser(gdbrunner.ArgumentParser): |
| 63 | def __init__(self): |
| 64 | super(ArgumentParser, self).__init__() |
| 65 | self.add_argument( |
| 66 | "--verbose", "-v", action="store_true", |
| 67 | help="enable verbose mode") |
| 68 | |
| 69 | self.add_argument( |
| 70 | "--force", "-f", action="store_true", |
| 71 | help="kill existing debug session if it exists") |
| 72 | |
| 73 | self.add_argument( |
| 74 | "--port", type=int, nargs="?", default="5039", |
| 75 | help="override the port used on the host") |
| 76 | |
| 77 | self.add_argument( |
| 78 | "--delay", type=float, default=0.25, |
| 79 | help="delay in seconds to wait after starting activity.\n" |
| 80 | "defaults to 0.25, higher values may be needed on slower devices.") |
| 81 | |
| 82 | self.add_argument( |
| 83 | "-p", "--project", dest="project", |
| 84 | help="specify application project path") |
| 85 | |
| 86 | app_group = self.add_argument_group("target selection") |
| 87 | start_group = app_group.add_mutually_exclusive_group() |
| 88 | |
| 89 | start_group.add_argument( |
| 90 | "--attach", nargs='?', dest="package_name", metavar="PKG_NAME", |
| 91 | help="attach to application (default)\n" |
| 92 | "autodetects PKG_NAME if not specified") |
| 93 | |
| 94 | # NB: args.launch can be False (--attach), None (--launch), or a string |
| 95 | start_group.add_argument( |
| 96 | "--launch", nargs='?', dest="launch", default=False, |
| 97 | metavar="ACTIVITY", |
| 98 | help="launch application activity\n" |
| 99 | "launches main activity if ACTIVITY not specified") |
| 100 | |
| 101 | start_group.add_argument( |
| 102 | "--launch-list", action="store_true", |
| 103 | help="list all launchable activity names from manifest") |
| 104 | |
| 105 | debug_group = self.add_argument_group("debugging options") |
| 106 | debug_group.add_argument( |
| 107 | "-x", "--exec", dest="exec_file", |
| 108 | help="execute gdb commands in EXEC_FILE after connection") |
| 109 | |
| 110 | debug_group.add_argument( |
| 111 | "--nowait", action="store_true", |
| 112 | help="do not wait for debugger to attach (may miss early JNI " |
| 113 | "breakpoints)") |
| 114 | |
| 115 | if sys.platform.startswith("win"): |
| 116 | tui_help = argparse.SUPPRESS |
| 117 | else: |
| 118 | tui_help = "use GDB's tui mode" |
| 119 | |
| 120 | debug_group.add_argument( |
| 121 | "-t", "--tui", action="store_true", dest="tui", |
| 122 | help=tui_help) |
| 123 | |
| 124 | debug_group.add_argument( |
| 125 | "--stdcxx-py-pr", dest="stdcxxpypr", |
| 126 | help="use C++ library pretty-printer", |
| 127 | choices=["auto", "none", "gnustl", "stlport"], |
| 128 | default="auto") |
| 129 | |
| 130 | |
| 131 | def extract_package_name(xmlroot): |
| 132 | if "package" in xmlroot.attrib: |
| 133 | return xmlroot.attrib["package"] |
| 134 | error("Failed to find package name in AndroidManifest.xml") |
| 135 | |
| 136 | |
| 137 | ANDROID_XMLNS = "{http://schemas.android.com/apk/res/android}" |
| 138 | def extract_launchable(xmlroot): |
| 139 | ''' |
| 140 | A given application can have several activities, and each activity |
| 141 | can have several intent filters. We want to only list, in the final |
| 142 | output, the activities which have a intent-filter that contains the |
| 143 | following elements: |
| 144 | |
| 145 | <action android:name="android.intent.action.MAIN" /> |
| 146 | <category android:name="android.intent.category.LAUNCHER" /> |
| 147 | ''' |
| 148 | launchable_activities = [] |
| 149 | application = xmlroot.findall("application")[0] |
| 150 | |
| 151 | main_action = "android.intent.action.MAIN" |
| 152 | launcher_category = "android.intent.category.LAUNCHER" |
| 153 | name_attrib = "{}name".format(ANDROID_XMLNS) |
| 154 | |
| 155 | for activity in application.iter("activity"): |
| 156 | if name_attrib not in activity.attrib: |
| 157 | continue |
| 158 | |
| 159 | for intent_filter in activity.iter("intent-filter"): |
| 160 | found_action = False |
| 161 | found_category = False |
| 162 | for child in intent_filter: |
| 163 | if child.tag == "action": |
| 164 | if not found_action and name_attrib in child.attrib: |
| 165 | if child.attrib[name_attrib] == main_action: |
| 166 | found_action = True |
| 167 | if child.tag == "category": |
| 168 | if not found_category and name_attrib in child.attrib: |
| 169 | if child.attrib[name_attrib] == launcher_category: |
| 170 | found_category = True |
| 171 | if found_action and found_category: |
| 172 | launchable_activities.append(activity.attrib[name_attrib]) |
| 173 | return launchable_activities |
| 174 | |
| 175 | |
| 176 | def ndk_bin_path(): |
| 177 | return os.path.dirname(os.path.realpath(__file__)) |
| 178 | |
| 179 | |
| 180 | def handle_args(): |
| 181 | def find_program(program, paths): |
| 182 | '''Find a binary in paths''' |
| 183 | exts = [""] |
| 184 | if sys.platform.startswith("win"): |
| 185 | exts += [".exe", ".bat", ".cmd"] |
| 186 | for path in paths: |
| 187 | if os.path.isdir(path): |
| 188 | for ext in exts: |
| 189 | full = path + os.sep + program + ext |
| 190 | if os.path.isfile(full): |
| 191 | return full |
| 192 | return None |
| 193 | |
| 194 | # FIXME: This is broken for PATH that contains quoted colons. |
| 195 | paths = os.environ["PATH"].replace('"', '').split(os.pathsep) |
| 196 | |
| 197 | args = ArgumentParser().parse_args() |
| 198 | |
| 199 | if args.tui and sys.platform.startswith("win"): |
| 200 | error("TUI is unsupported on Windows.") |
| 201 | |
| 202 | ndk_bin = ndk_bin_path() |
| 203 | args.make_cmd = find_program("make", [ndk_bin]) |
| 204 | args.jdb_cmd = find_program("jdb", paths) |
| 205 | if args.make_cmd is None: |
| 206 | error("Failed to find make in '{}'".format(ndk_bin)) |
| 207 | if args.jdb_cmd is None: |
| 208 | print("WARNING: Failed to find jdb on your path, defaulting to " |
| 209 | "--nowait") |
| 210 | args.nowait = True |
| 211 | |
| 212 | if args.verbose: |
| 213 | enable_verbose_logging() |
| 214 | |
| 215 | return args |
| 216 | |
| 217 | |
| 218 | def find_project(args): |
| 219 | manifest_name = "AndroidManifest.xml" |
| 220 | if args.project is not None: |
| 221 | log("Using project directory: {}".format(args.project)) |
| 222 | args.project = os.path.realpath(os.path.expanduser(args.project)) |
| 223 | if not os.path.exists(os.path.join(args.project, manifest_name)): |
| 224 | msg = "could not find AndroidManifest.xml in '{}'" |
| 225 | error(msg.format(args.project)) |
| 226 | else: |
| 227 | # Walk upwards until we find AndroidManifest.xml, or run out of path. |
| 228 | current_dir = os.getcwdu() |
| 229 | while not os.path.exists(os.path.join(current_dir, manifest_name)): |
| 230 | parent_dir = os.path.dirname(current_dir) |
| 231 | if parent_dir == current_dir: |
| 232 | error("Could not find AndroidManifest.xml in current" |
| 233 | " directory or a parent directory.\n" |
| 234 | " Launch this script from inside a project, or" |
| 235 | " use --project=<path>.") |
| 236 | current_dir = parent_dir |
| 237 | args.project = current_dir |
| 238 | log("Using project directory: {} ".format(args.project)) |
| 239 | args.manifest_path = os.path.join(args.project, manifest_name) |
| 240 | return args.project |
| 241 | |
| 242 | |
| 243 | def canonicalize_activity(package_name, activity_name): |
| 244 | if activity_name.startswith("."): |
| 245 | return "{}{}".format(package_name, activity_name) |
| 246 | return activity_name |
| 247 | |
| 248 | |
| 249 | def parse_manifest(args): |
| 250 | manifest = ElementTree.parse(args.manifest_path) |
| 251 | manifest_root = manifest.getroot() |
| 252 | package_name = extract_package_name(manifest_root) |
| 253 | log("Found package name: {}".format(package_name)) |
| 254 | |
| 255 | activities = extract_launchable(manifest_root) |
| 256 | activities = [canonicalize_activity(package_name, a) for a in activities] |
| 257 | |
| 258 | if args.launch_list: |
| 259 | print("Launchable activities: {}".format(", ".join(activities))) |
| 260 | sys.exit(0) |
| 261 | |
| 262 | args.activities = activities |
| 263 | args.package_name = package_name |
| 264 | |
| 265 | |
| 266 | def select_target(args): |
| 267 | assert args.launch != False |
| 268 | |
| 269 | if len(args.activities) == 0: |
| 270 | error("No launchable activities found.") |
| 271 | |
| 272 | if args.launch is None: |
| 273 | target = args.activities[0] |
| 274 | |
| 275 | if len(args.activities) > 1: |
| 276 | print("WARNING: Multiple launchable activities found, choosing" |
| 277 | " '{}'.".format(args.activities[0])) |
| 278 | else: |
| 279 | activity_name = canonicalize_activity(args.package_name, args.launch) |
| 280 | |
| 281 | if activity_name not in args.activities: |
| 282 | msg = "Could not find launchable activity: '{}'." |
| 283 | error(msg.format(activity_name)) |
| 284 | target = activity_name |
| 285 | return target |
| 286 | |
| 287 | |
| 288 | @contextlib.contextmanager |
| 289 | def cd(path): |
| 290 | curdir = os.getcwd() |
| 291 | os.chdir(path) |
| 292 | os.environ["PWD"] = path |
| 293 | try: |
| 294 | yield |
| 295 | finally: |
| 296 | os.environ["PWD"] = curdir |
| 297 | os.chdir(curdir) |
| 298 | |
| 299 | |
| 300 | def dump_var(args, variable, abi=None): |
| 301 | make_args = [args.make_cmd, "--no-print-dir", "-f", |
| 302 | os.path.join(NDK_PATH, "build/core/build-local.mk"), |
| 303 | "-C", args.project, "DUMP_{}".format(variable)] |
| 304 | |
| 305 | if abi is not None: |
| 306 | make_args.append("APP_ABI={}".format(abi)) |
| 307 | |
| 308 | with cd(args.project): |
| 309 | try: |
| 310 | make_output = subprocess.check_output(make_args, cwd=args.project) |
| 311 | except subprocess.CalledProcessError: |
| 312 | error("Failed to retrieve application ABI from Android.mk.") |
| 313 | return make_output.splitlines()[-1] |
| 314 | |
| 315 | |
| 316 | def get_api_level(device): |
| 317 | # Check the device API level |
| 318 | try: |
| 319 | api_level = int(device.get_prop("ro.build.version.sdk")) |
| 320 | except (TypeError, ValueError): |
| 321 | error("Failed to find target device's supported API level.\n" |
| 322 | "ndk-gdb only supports devices running Android 2.2 or higher.") |
| 323 | if api_level < 8: |
| 324 | error("ndk-gdb only supports devices running Android 2.2 or higher.\n" |
| 325 | "(expected API level 8, actual: {})".format(api_level)) |
| 326 | |
| 327 | return api_level |
| 328 | |
| 329 | |
| 330 | def fetch_abi(args): |
| 331 | ''' |
| 332 | Figure out the intersection of which ABIs the application is built for and |
| 333 | which ones the device supports, then pick the one preferred by the device, |
| 334 | so that we know which gdbserver to push and run on the device. |
| 335 | ''' |
| 336 | |
| 337 | app_abis = dump_var(args, "APP_ABI").split(" ") |
| 338 | if "all" in app_abis: |
| 339 | app_abis = dump_var(args, "NDK_ALL_ABIS").split(" ") |
| 340 | app_abis_msg = "Application ABIs: {}".format(", ".join(app_abis)) |
| 341 | log(app_abis_msg) |
| 342 | |
| 343 | new_abi_props = ["ro.product.cpu.abilist"] |
| 344 | old_abi_props = ["ro.product.cpu.abi", "ro.product.cpu.abi2"] |
| 345 | abi_props = new_abi_props |
| 346 | if args.device.get_prop("ro.product.cpu.abilist") is None: |
| 347 | abi_props = old_abi_props |
| 348 | |
| 349 | device_abis = [] |
| 350 | for key in abi_props: |
| 351 | value = args.device.get_prop(key) |
| 352 | if value is not None: |
| 353 | device_abis.extend(value.split(",")) |
| 354 | |
| 355 | device_abis_msg = "Device ABIs: {}".format(", ".join(device_abis)) |
| 356 | log(device_abis_msg) |
| 357 | |
| 358 | for abi in device_abis: |
| 359 | if abi in app_abis: |
| 360 | # TODO(jmgao): Do we expect gdb to work with ARM-x86 translation? |
| 361 | log("Selecting ABI: {}".format(abi)) |
| 362 | return abi |
| 363 | |
| 364 | msg = "Application cannot run on the selected device." |
| 365 | |
| 366 | # Don't repeat ourselves. |
| 367 | if not args.verbose: |
| 368 | msg += "\n{}\n{}".format(app_abis_msg, device_abis_msg) |
| 369 | |
| 370 | error(msg) |
| 371 | |
| 372 | |
| 373 | def get_run_as_cmd(user, cmd): |
| 374 | return ["run-as", user] + cmd |
| 375 | |
| 376 | |
| 377 | def get_app_data_dir(args, package_name): |
| 378 | cmd = ["/system/bin/sh", "-c", "pwd", "2>/dev/null"] |
| 379 | cmd = get_run_as_cmd(package_name, cmd) |
| 380 | (rc, stdout, _) = args.device.shell_nocheck(cmd) |
| 381 | if rc != 0: |
| 382 | error("Could not find application's data directory. Are you sure that " |
| 383 | "the application is installed and debuggable?") |
| 384 | data_dir = stdout.strip() |
| 385 | |
| 386 | # Applications with minSdkVersion >= 24 will have their data directories |
| 387 | # created with rwx------ permissions, preventing adbd from forwarding to |
| 388 | # the gdbserver socket. To be safe, if we're on a device >= 24, always |
| 389 | # chmod the directory. |
| 390 | if get_api_level(args.device) >= 24: |
| 391 | chmod_cmd = ["/system/bin/chmod", "a+x", data_dir] |
| 392 | chmod_cmd = get_run_as_cmd(package_name, chmod_cmd) |
| 393 | (rc, _, _) = args.device.shell_nocheck(chmod_cmd) |
| 394 | if rc != 0: |
| 395 | error("Failed to make application data directory world executable") |
| 396 | |
| 397 | log("Found application data directory: {}".format(data_dir)) |
| 398 | return data_dir |
| 399 | |
| 400 | |
| 401 | def abi_to_arch(abi): |
| 402 | if abi.startswith("armeabi"): |
| 403 | return "arm" |
| 404 | elif abi == "arm64-v8a": |
| 405 | return "arm64" |
| 406 | else: |
| 407 | return abi |
| 408 | |
| 409 | |
| 410 | def get_gdbserver_path(args, package_name, app_data_dir, arch): |
| 411 | app_gdbserver_path = "{}/lib/gdbserver".format(app_data_dir) |
| 412 | cmd = ["ls", app_gdbserver_path, "2>/dev/null"] |
| 413 | cmd = get_run_as_cmd(package_name, cmd) |
| 414 | (rc, _, _) = args.device.shell_nocheck(cmd) |
| 415 | if rc == 0: |
| 416 | log("Found app gdbserver: {}".format(app_gdbserver_path)) |
| 417 | return app_gdbserver_path |
| 418 | |
| 419 | # We need to upload our gdbserver |
| 420 | log("App gdbserver not found at {}, uploading.".format(app_gdbserver_path)) |
| 421 | local_path = "{}/prebuilt/android-{}/gdbserver/gdbserver" |
| 422 | local_path = local_path.format(NDK_PATH, arch) |
| 423 | remote_path = "/data/local/tmp/{}-gdbserver".format(arch) |
| 424 | args.device.push(local_path, remote_path) |
| 425 | |
| 426 | # Copy gdbserver into the data directory on M+, because selinux prevents |
| 427 | # execution of binaries directly from /data/local/tmp. |
| 428 | if get_api_level(args.device) >= 23: |
| 429 | destination = "{}/{}-gdbserver".format(app_data_dir, arch) |
| 430 | log("Copying gdbserver to {}.".format(destination)) |
| 431 | cmd = ["cat", remote_path, "|", "run-as", package_name, |
| 432 | "sh", "-c", "'cat > {}'".format(destination)] |
| 433 | (rc, _, _) = args.device.shell_nocheck(cmd) |
| 434 | if rc != 0: |
| 435 | error("Failed to copy gdbserver to {}.".format(destination)) |
| 436 | (rc, _, _) = args.device.shell_nocheck(["run-as", package_name, |
| 437 | "chmod", "700", destination]) |
| 438 | if rc != 0: |
| 439 | error("Failed to chmod gdbserver at {}.".format(destination)) |
| 440 | |
| 441 | remote_path = destination |
| 442 | |
| 443 | log("Uploaded gdbserver to {}".format(remote_path)) |
| 444 | return remote_path |
| 445 | |
| 446 | |
| 447 | def pull_binaries(device, out_dir, app_64bit): |
| 448 | required_files = [] |
| 449 | libraries = ["libc.so", "libm.so", "libdl.so"] |
| 450 | |
| 451 | if app_64bit: |
| 452 | required_files = ["/system/bin/app_process64", "/system/bin/linker64"] |
| 453 | library_path = "/system/lib64" |
| 454 | else: |
| 455 | required_files = ["/system/bin/linker"] |
| 456 | library_path = "/system/lib" |
| 457 | |
| 458 | for library in libraries: |
| 459 | required_files.append(posixpath.join(library_path, library)) |
| 460 | |
| 461 | for required_file in required_files: |
| 462 | # os.path.join not used because joining absolute paths will pick the last one |
| 463 | local_path = os.path.realpath(out_dir + required_file) |
| 464 | local_dirname = os.path.dirname(local_path) |
| 465 | if not os.path.isdir(local_dirname): |
| 466 | os.makedirs(local_dirname) |
| 467 | log("Pulling '{}' to '{}'".format(required_file, local_path)) |
| 468 | device.pull(required_file, local_path) |
| 469 | |
| 470 | # /system/bin/app_process is 32-bit on 32-bit devices, but a symlink to |
| 471 | # app_process64 on 64-bit. If we need the 32-bit version, try to pull |
| 472 | # app_process32, and if that fails, pull app_process. |
| 473 | if not app_64bit: |
| 474 | destination = os.path.realpath(out_dir + "/system/bin/app_process") |
| 475 | try: |
| 476 | device.pull("/system/bin/app_process32", destination) |
| 477 | except: |
| 478 | device.pull("/system/bin/app_process", destination) |
| 479 | |
| 480 | def generate_gdb_script(args, sysroot, binary_path, app_64bit, jdb_pid, connect_timeout=5): |
| 481 | if sys.platform.startswith("win"): |
| 482 | # GDB expects paths to use forward slashes. |
| 483 | sysroot = sysroot.replace("\\", "/") |
| 484 | binary_path = binary_path.replace("\\", "/") |
| 485 | |
| 486 | gdb_commands = "set osabi GNU/Linux\n" |
| 487 | gdb_commands += "file '{}'\n".format(binary_path) |
| 488 | |
| 489 | solib_search_path = [sysroot, "{}/system/bin".format(sysroot)] |
| 490 | if app_64bit: |
| 491 | solib_search_path.append("{}/system/lib64".format(sysroot)) |
| 492 | else: |
| 493 | solib_search_path.append("{}/system/lib".format(sysroot)) |
| 494 | solib_search_path = os.pathsep.join(solib_search_path) |
| 495 | gdb_commands += "set solib-absolute-prefix {}\n".format(sysroot) |
| 496 | gdb_commands += "set solib-search-path {}\n".format(solib_search_path) |
| 497 | |
| 498 | # Try to connect for a few seconds, sometimes the device gdbserver takes |
| 499 | # a little bit to come up, especially on emulators. |
| 500 | gdb_commands += """ |
| 501 | python |
| 502 | |
| 503 | def target_remote_with_retry(target, timeout_seconds): |
| 504 | import time |
| 505 | end_time = time.time() + timeout_seconds |
| 506 | while True: |
| 507 | try: |
| 508 | gdb.execute('target remote ' + target) |
| 509 | return True |
| 510 | except gdb.error as e: |
| 511 | time_left = end_time - time.time() |
| 512 | if time_left < 0 or time_left > timeout_seconds: |
| 513 | print("Error: unable to connect to device.") |
| 514 | print(e) |
| 515 | return False |
| 516 | time.sleep(min(0.25, time_left)) |
| 517 | |
| 518 | target_remote_with_retry(':{}', {}) |
| 519 | |
| 520 | end |
| 521 | """.format(args.port, connect_timeout) |
| 522 | |
| 523 | if jdb_pid is not None: |
| 524 | # After we've interrupted the app, reinvoke ndk-gdb.py to start jdb and |
| 525 | # wake up the app. |
| 526 | gdb_commands += """ |
| 527 | python |
| 528 | def start_jdb_to_unblock_app(): |
| 529 | import subprocess |
| 530 | subprocess.Popen({}) |
| 531 | start_jdb_to_unblock_app() |
| 532 | end |
| 533 | """.format(repr( |
| 534 | [ |
| 535 | sys.executable, |
| 536 | os.path.realpath(__file__), |
| 537 | "--internal-wakeup-pid-with-jdb", |
| 538 | args.device.adb_path, |
| 539 | args.device.serial, |
| 540 | args.jdb_cmd, |
| 541 | str(jdb_pid), |
| 542 | str(bool(args.verbose)), |
| 543 | ])) |
| 544 | |
| 545 | # Set up the pretty printer if needed |
| 546 | if args.pypr_dir is not None and args.pypr_fn is not None: |
| 547 | gdb_commands += """ |
| 548 | python |
| 549 | import sys |
| 550 | sys.path.append("{pypr_dir}") |
| 551 | from printers import {pypr_fn} |
| 552 | {pypr_fn}(None) |
| 553 | end""".format(pypr_dir=args.pypr_dir.replace("\\", "/"), pypr_fn=args.pypr_fn) |
| 554 | |
| 555 | if args.exec_file is not None: |
| 556 | try: |
| 557 | exec_file = open(args.exec_file, "r") |
| 558 | except IOError: |
| 559 | error("Failed to open GDB exec file: '{}'.".format(args.exec_file)) |
| 560 | |
| 561 | with exec_file: |
| 562 | gdb_commands += exec_file.read() |
| 563 | |
| 564 | return gdb_commands |
| 565 | |
| 566 | |
| 567 | def detect_stl_pretty_printer(args): |
| 568 | stl = dump_var(args, "APP_STL") |
| 569 | if not stl: |
| 570 | detected = "none" |
| 571 | if args.stdcxxpypr == "auto": |
| 572 | log("APP_STL not found, disabling pretty printer") |
| 573 | elif stl.startswith("stlport"): |
| 574 | detected = "stlport" |
| 575 | elif stl.startswith("gnustl"): |
| 576 | detected = "gnustl" |
| 577 | else: |
| 578 | detected = "none" |
| 579 | |
| 580 | if args.stdcxxpypr == "auto": |
| 581 | log("Detected pretty printer: {}".format(detected)) |
| 582 | return detected |
| 583 | if detected != args.stdcxxpypr and args.stdcxxpypr != "none": |
| 584 | print("WARNING: detected APP_STL ('{}') does not match pretty printer".format(detected)) |
| 585 | log("Using specified pretty printer: {}".format(args.stdcxxpypr)) |
| 586 | return args.stdcxxpypr |
| 587 | |
| 588 | |
| 589 | def find_pretty_printer(pretty_printer): |
| 590 | if pretty_printer == "gnustl": |
| 591 | path = os.path.join("libstdcxx", "gcc-4.9") |
| 592 | function = "register_libstdcxx_printers" |
| 593 | elif pretty_printer == "stlport": |
| 594 | path = os.path.join("stlport", "stlport") |
| 595 | function = "register_stlport_printers" |
| 596 | pp_path = os.path.join( |
| 597 | ndk_bin_path(), "..", "share", "pretty-printers", path) |
| 598 | return pp_path, function |
| 599 | |
| 600 | |
| 601 | def start_jdb(adb_path, serial, jdb_cmd, pid, verbose): |
| 602 | pid = int(pid) |
| 603 | device = adb.get_device(serial, adb_path=adb_path) |
| 604 | if verbose == "True": |
| 605 | enable_verbose_logging() |
| 606 | |
| 607 | log("Starting jdb to unblock application.") |
| 608 | |
| 609 | # Do setup stuff to keep ^C in the parent from killing us. |
| 610 | signal.signal(signal.SIGINT, signal.SIG_IGN) |
| 611 | windows = sys.platform.startswith("win") |
| 612 | if not windows: |
| 613 | os.setpgrp() |
| 614 | |
| 615 | jdb_port = 65534 |
| 616 | device.forward("tcp:{}".format(jdb_port), "jdwp:{}".format(pid)) |
| 617 | jdb_cmd = [jdb_cmd, "-connect", |
| 618 | "com.sun.jdi.SocketAttach:hostname=localhost,port={}".format(jdb_port)] |
| 619 | |
| 620 | flags = subprocess.CREATE_NEW_PROCESS_GROUP if windows else 0 |
| 621 | jdb = subprocess.Popen(jdb_cmd, |
| 622 | stdin=subprocess.PIPE, |
| 623 | stdout=subprocess.PIPE, |
| 624 | stderr=subprocess.STDOUT, |
| 625 | creationflags=flags) |
| 626 | |
| 627 | # Wait until jdb can communicate with the app. Once it can, the app will |
| 628 | # start polling for a Java debugger (e.g. every 200ms). We need to wait |
| 629 | # a while longer then so that the app notices jdb. |
| 630 | jdb_magic = "__verify_jdb_has_started__" |
| 631 | jdb.stdin.write('print "{}"\n'.format(jdb_magic)) |
| 632 | saw_magic_str = False |
| 633 | while True: |
| 634 | line = jdb.stdout.readline() |
| 635 | if line == "": |
| 636 | break |
| 637 | log("jdb output: " + line.rstrip()) |
| 638 | if jdb_magic in line and not saw_magic_str: |
| 639 | saw_magic_str = True |
| 640 | time.sleep(0.3) |
| 641 | jdb.stdin.write("exit\n") |
| 642 | jdb.wait() |
| 643 | if saw_magic_str: |
| 644 | log("JDB finished unblocking application.") |
| 645 | else: |
| 646 | log("error: did not find magic string in JDB output.") |
| 647 | |
| 648 | |
| 649 | def main(): |
| 650 | if sys.argv[1:2] == ["--internal-wakeup-pid-with-jdb"]: |
| 651 | return start_jdb(*sys.argv[2:]) |
| 652 | |
| 653 | args = handle_args() |
| 654 | device = args.device |
| 655 | |
| 656 | if device is None: |
| 657 | error("Could not find a unique connected device/emulator.") |
| 658 | |
| 659 | # Warn on old Pixel C firmware (b/29381985). Newer devices may have Yama |
| 660 | # enabled but still work with ndk-gdb (b/19277529). |
| 661 | yama_check = device.shell_nocheck(["cat", "/proc/sys/kernel/yama/ptrace_scope", "2>/dev/null"]) |
| 662 | if (yama_check[0] == 0 and yama_check[1].rstrip() not in ["", "0"] and |
| 663 | (device.get_prop("ro.build.product"), device.get_prop("ro.product.name")) == ("dragon", "ryu")): |
| 664 | print("WARNING: The device uses Yama ptrace_scope to restrict debugging. ndk-gdb will") |
| 665 | print(" likely be unable to attach to a process. With root access, the restriction") |
| 666 | print(" can be lifted by writing 0 to /proc/sys/kernel/yama/ptrace_scope. Consider") |
| 667 | print(" upgrading your Pixel C to MXC89L or newer, where Yama is disabled.") |
| 668 | |
| 669 | adb_version = subprocess.check_output(device.adb_cmd + ["version"]) |
| 670 | log("ADB command used: '{}'".format(" ".join(device.adb_cmd))) |
| 671 | log("ADB version: {}".format(" ".join(adb_version.splitlines()))) |
| 672 | |
| 673 | project = find_project(args) |
| 674 | if args.package_name: |
| 675 | log("Attaching to specified package: {}".format(args.package_name)) |
| 676 | else: |
| 677 | parse_manifest(args) |
| 678 | |
| 679 | pkg_name = args.package_name |
| 680 | |
| 681 | if args.launch is False: |
| 682 | log("Attaching to existing application process.") |
| 683 | else: |
| 684 | args.launch = select_target(args) |
| 685 | log("Selected target activity: '{}'".format(args.launch)) |
| 686 | |
| 687 | abi = fetch_abi(args) |
| 688 | |
| 689 | out_dir = os.path.join(project, (dump_var(args, "TARGET_OUT", abi))) |
| 690 | out_dir = os.path.realpath(out_dir) |
| 691 | |
| 692 | pretty_printer = detect_stl_pretty_printer(args) |
| 693 | if pretty_printer != "none": |
| 694 | (args.pypr_dir, args.pypr_fn) = find_pretty_printer(pretty_printer) |
| 695 | else: |
| 696 | (args.pypr_dir, args.pypr_fn) = (None, None) |
| 697 | |
| 698 | app_data_dir = get_app_data_dir(args, pkg_name) |
| 699 | arch = abi_to_arch(abi) |
| 700 | gdbserver_path = get_gdbserver_path(args, pkg_name, app_data_dir, arch) |
| 701 | |
| 702 | # Kill the process and gdbserver if requested. |
| 703 | if args.force: |
| 704 | kill_pids = gdbrunner.get_pids(device, gdbserver_path) |
| 705 | if args.launch: |
| 706 | kill_pids += gdbrunner.get_pids(device, pkg_name) |
| 707 | kill_pids = map(str, kill_pids) |
| 708 | if kill_pids: |
| 709 | log("Killing processes: {}".format(", ".join(kill_pids))) |
| 710 | device.shell_nocheck(["run-as", pkg_name, "kill", "-9"] + kill_pids) |
| 711 | |
| 712 | # Launch the application if needed, and get its pid |
| 713 | if args.launch: |
| 714 | am_cmd = ["am", "start"] |
| 715 | if not args.nowait: |
| 716 | am_cmd.append("-D") |
| 717 | component_name = "{}/{}".format(pkg_name, args.launch) |
| 718 | am_cmd.append(component_name) |
| 719 | log("Launching activity {}...".format(component_name)) |
| 720 | (rc, _, _) = device.shell_nocheck(am_cmd) |
| 721 | if rc != 0: |
| 722 | error("Failed to start {}".format(component_name)) |
| 723 | |
| 724 | if args.delay > 0.0: |
| 725 | log("Sleeping for {} seconds.".format(args.delay)) |
| 726 | time.sleep(args.delay) |
| 727 | |
| 728 | pids = gdbrunner.get_pids(device, pkg_name) |
| 729 | if len(pids) == 0: |
| 730 | error("Failed to find running process '{}'".format(pkg_name)) |
| 731 | if len(pids) > 1: |
| 732 | error("Multiple running processes named '{}'".format(pkg_name)) |
| 733 | pid = pids[0] |
| 734 | |
| 735 | # Pull the linker, zygote, and notable system libraries |
| 736 | app_64bit = "64" in abi |
| 737 | pull_binaries(device, out_dir, app_64bit) |
| 738 | if app_64bit: |
| 739 | zygote_path = os.path.join(out_dir, "system", "bin", "app_process64") |
| 740 | else: |
| 741 | zygote_path = os.path.join(out_dir, "system", "bin", "app_process") |
| 742 | |
| 743 | # Start gdbserver. |
| 744 | debug_socket = posixpath.join(app_data_dir, "debug_socket") |
| 745 | log("Starting gdbserver...") |
| 746 | gdbrunner.start_gdbserver( |
| 747 | device, None, gdbserver_path, |
| 748 | target_pid=pid, run_cmd=None, debug_socket=debug_socket, |
| 749 | port=args.port, run_as_cmd=["run-as", pkg_name]) |
| 750 | |
| 751 | gdb_path = os.path.join(ndk_bin_path(), "gdb") |
| 752 | |
| 753 | # Start jdb to unblock the application if necessary. |
| 754 | jdb_pid = pid if (args.launch and not args.nowait) else None |
| 755 | |
| 756 | # Start gdb. |
| 757 | gdb_commands = generate_gdb_script(args, out_dir, zygote_path, app_64bit, jdb_pid) |
| 758 | gdb_flags = [] |
| 759 | if args.tui: |
| 760 | gdb_flags.append("--tui") |
| 761 | gdbrunner.start_gdb(gdb_path, gdb_commands, gdb_flags) |
| 762 | |
| 763 | if __name__ == "__main__": |
| 764 | main() |