| #!/usr/bin/env python3 |
| # |
| # Copyright (C) 2015 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| |
| from __future__ import print_function |
| |
| import argparse |
| import contextlib |
| import logging |
| import os |
| import posixpath |
| import signal |
| import subprocess |
| import sys |
| import textwrap |
| import time |
| from collections.abc import Iterator |
| from typing import NoReturn |
| from xml.etree import ElementTree |
| from xml.etree.ElementTree import Element |
| |
| import adb |
| import gdbrunner |
| |
| NDK_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) |
| |
| |
| def log(msg: str) -> None: |
| logger = logging.getLogger(__name__) |
| logger.info(msg) |
| |
| |
| def enable_verbose_logging() -> None: |
| logger = logging.getLogger(__name__) |
| handler = logging.StreamHandler(sys.stdout) |
| formatter = logging.Formatter() |
| |
| handler.setFormatter(formatter) |
| logger.addHandler(handler) |
| logger.propagate = False |
| |
| logger.setLevel(logging.INFO) |
| |
| |
| def error(msg: str) -> NoReturn: |
| sys.exit("ERROR: {}".format(msg)) |
| |
| |
| class ArgumentParser(gdbrunner.ArgumentParser): |
| def __init__(self) -> None: |
| super().__init__() |
| self.add_argument( |
| "--verbose", "-v", action="store_true", help="enable verbose mode" |
| ) |
| |
| self.add_argument( |
| "--force", |
| "-f", |
| action="store_true", |
| help="kill existing debug session if it exists", |
| ) |
| |
| self.add_argument( |
| "--port", |
| type=int, |
| nargs="?", |
| default="5039", |
| help="override the port used on the host.", |
| ) |
| |
| self.add_argument( |
| "--delay", |
| type=float, |
| default=0.25, |
| help="delay in seconds to wait after starting activity.\n" |
| "defaults to 0.25, higher values may be needed on slower devices.", |
| ) |
| |
| self.add_argument( |
| "-p", "--project", dest="project", help="specify application project path" |
| ) |
| |
| lldb_group = self.add_mutually_exclusive_group() |
| lldb_group.add_argument("--lldb", action="store_true", help="Use lldb.") |
| lldb_group.add_argument( |
| "--no-lldb", action="store_true", help="Do not use lldb." |
| ) |
| |
| app_group = self.add_argument_group("target selection") |
| start_group = app_group.add_mutually_exclusive_group() |
| |
| start_group.add_argument( |
| "--attach", |
| nargs="?", |
| dest="package_name", |
| metavar="PKG_NAME", |
| help="attach to application (default)\n" |
| "autodetects PKG_NAME if not specified", |
| ) |
| |
| # NB: args.launch can be False (--attach), None (--launch), or a string |
| start_group.add_argument( |
| "--launch", |
| nargs="?", |
| dest="launch", |
| default=False, |
| metavar="ACTIVITY", |
| help="launch application activity\n" |
| "launches main activity if ACTIVITY not specified", |
| ) |
| |
| start_group.add_argument( |
| "--launch-list", |
| action="store_true", |
| help="list all launchable activity names from manifest", |
| ) |
| |
| debug_group = self.add_argument_group("debugging options") |
| debug_group.add_argument( |
| "-x", |
| "--exec", |
| dest="exec_file", |
| help="execute gdb commands in EXEC_FILE after connection", |
| ) |
| |
| debug_group.add_argument( |
| "--nowait", |
| action="store_true", |
| help="do not wait for debugger to attach (may miss early JNI " |
| "breakpoints)", |
| ) |
| |
| if sys.platform.startswith("win"): |
| tui_help = argparse.SUPPRESS |
| else: |
| tui_help = "use GDB's tui mode" |
| |
| debug_group.add_argument( |
| "-t", "--tui", action="store_true", dest="tui", help=tui_help |
| ) |
| |
| |
| def extract_package_name(xmlroot: Element) -> str: |
| if "package" in xmlroot.attrib: |
| return xmlroot.attrib["package"] |
| error("Failed to find package name in AndroidManifest.xml") |
| |
| |
| ANDROID_XMLNS = "{http://schemas.android.com/apk/res/android}" |
| |
| |
| def extract_launchable(xmlroot: Element) -> list[str]: |
| """ |
| A given application can have several activities, and each activity |
| can have several intent filters. We want to only list, in the final |
| output, the activities which have a intent-filter that contains the |
| following elements: |
| |
| <action android:name="android.intent.action.MAIN" /> |
| <category android:name="android.intent.category.LAUNCHER" /> |
| """ |
| launchable_activities = [] |
| application = xmlroot.findall("application")[0] |
| |
| main_action = "android.intent.action.MAIN" |
| launcher_category = "android.intent.category.LAUNCHER" |
| name_attrib = "{}name".format(ANDROID_XMLNS) |
| |
| # pylint: disable=too-many-nested-blocks |
| for activity in application.iter("activity"): |
| if name_attrib not in activity.attrib: |
| continue |
| |
| for intent_filter in activity.iter("intent-filter"): |
| found_action = False |
| found_category = False |
| for child in intent_filter: |
| if child.tag == "action": |
| if not found_action and name_attrib in child.attrib: |
| if child.attrib[name_attrib] == main_action: |
| found_action = True |
| if child.tag == "category": |
| if not found_category and name_attrib in child.attrib: |
| if child.attrib[name_attrib] == launcher_category: |
| found_category = True |
| if found_action and found_category: |
| launchable_activities.append(activity.attrib[name_attrib]) |
| return launchable_activities |
| |
| |
| def ndk_bin_path() -> str: |
| return os.path.dirname(os.path.dirname(os.path.realpath(__file__))) |
| |
| |
| def handle_args() -> argparse.Namespace: |
| def find_program(program: str, paths: list[str]) -> str | None: |
| """Find a binary in paths""" |
| exts = [""] |
| if sys.platform.startswith("win"): |
| exts += [".exe", ".bat", ".cmd"] |
| for path in paths: |
| if os.path.isdir(path): |
| for ext in exts: |
| full = path + os.sep + program + ext |
| if os.path.isfile(full): |
| return full |
| return None |
| |
| # FIXME: This is broken for PATH that contains quoted colons. |
| paths = os.environ["PATH"].replace('"', "").split(os.pathsep) |
| |
| args: argparse.Namespace = ArgumentParser().parse_args() |
| |
| if args.tui and sys.platform.startswith("win"): |
| error("TUI is unsupported on Windows.") |
| |
| ndk_bin = ndk_bin_path() |
| args.make_cmd = find_program("make", [ndk_bin]) |
| args.jdb_cmd = find_program("jdb", paths) |
| if args.make_cmd is None: |
| error("Failed to find make in '{}'".format(ndk_bin)) |
| if args.jdb_cmd is None: |
| print("WARNING: Failed to find jdb on your path, defaulting to --nowait") |
| args.nowait = True |
| |
| if args.verbose: |
| enable_verbose_logging() |
| |
| return args |
| |
| |
| def find_project(args: argparse.Namespace) -> str: |
| manifest_name = "AndroidManifest.xml" |
| project: str | None = args.project |
| if project is not None: |
| log("Using project directory: {}".format(args.project)) |
| args.project = os.path.realpath(os.path.expanduser(args.project)) |
| if not os.path.exists(os.path.join(args.project, manifest_name)): |
| msg = "could not find AndroidManifest.xml in '{}'" |
| error(msg.format(args.project)) |
| else: |
| # Walk upwards until we find AndroidManifest.xml, or run out of path. |
| current_dir = os.getcwd() |
| while not os.path.exists(os.path.join(current_dir, manifest_name)): |
| parent_dir = os.path.dirname(current_dir) |
| if parent_dir == current_dir: |
| error( |
| "Could not find AndroidManifest.xml in current" |
| " directory or a parent directory.\n" |
| " Launch this script from inside a project, or" |
| " use --project=<path>." |
| ) |
| current_dir = parent_dir |
| args.project = current_dir |
| log("Using project directory: {} ".format(args.project)) |
| assert project is not None |
| args.manifest_path = os.path.join(project, manifest_name) |
| args.project = project |
| return project |
| |
| |
| def canonicalize_activity(package_name: str, activity_name: str) -> str: |
| if activity_name.startswith("."): |
| return "{}{}".format(package_name, activity_name) |
| return activity_name |
| |
| |
| def parse_manifest(args: argparse.Namespace) -> None: |
| manifest = ElementTree.parse(args.manifest_path) |
| manifest_root = manifest.getroot() |
| package_name = extract_package_name(manifest_root) |
| log("Found package name: {}".format(package_name)) |
| |
| activities = extract_launchable(manifest_root) |
| activities = [canonicalize_activity(package_name, a) for a in activities] |
| |
| if args.launch_list: |
| print("Launchable activities: {}".format(", ".join(activities))) |
| sys.exit(0) |
| |
| args.activities = activities |
| args.package_name = package_name |
| |
| |
| def select_target(args: argparse.Namespace) -> str: |
| assert args.launch |
| |
| if len(args.activities) == 0: |
| error("No launchable activities found.") |
| |
| target: str |
| if args.launch is None: |
| target = args.activities[0] |
| |
| if len(args.activities) > 1: |
| print( |
| "WARNING: Multiple launchable activities found, choosing" |
| " '{}'.".format(args.activities[0]) |
| ) |
| else: |
| activity_name = canonicalize_activity(args.package_name, args.launch) |
| |
| if activity_name not in args.activities: |
| msg = "Could not find launchable activity: '{}'." |
| error(msg.format(activity_name)) |
| target = activity_name |
| return target |
| |
| |
| @contextlib.contextmanager |
| def cd(path: str) -> Iterator[None]: |
| curdir = os.getcwd() |
| os.chdir(path) |
| os.environ["PWD"] = path |
| try: |
| yield |
| finally: |
| os.environ["PWD"] = curdir |
| os.chdir(curdir) |
| |
| |
| def dump_var(args: argparse.Namespace, variable: str, abi: str | None = None) -> str: |
| make_args = [ |
| args.make_cmd, |
| "--no-print-dir", |
| "-f", |
| os.path.join(NDK_PATH, "build/core/build-local.mk"), |
| "-C", |
| args.project, |
| "DUMP_{}".format(variable), |
| ] |
| |
| if abi is not None: |
| make_args.append("APP_ABI={}".format(abi)) |
| |
| with cd(args.project): |
| try: |
| make_output = subprocess.check_output(make_args, cwd=args.project) |
| except subprocess.CalledProcessError: |
| error("Failed to retrieve application ABI from Android.mk.") |
| return make_output.splitlines()[-1].decode() |
| |
| |
| def get_api_level(device: adb.AndroidDevice) -> int: |
| # Check the device API level |
| try: |
| api_str = device.get_prop("ro.build.version.sdk") |
| if api_str is None: |
| raise KeyError |
| api_level = int() |
| except (ValueError, KeyError): |
| error( |
| "Failed to find target device's supported API level.\n" |
| "ndk-gdb only supports devices running Android 2.2 or higher." |
| ) |
| if api_level < 8: |
| error( |
| "ndk-gdb only supports devices running Android 2.2 or higher.\n" |
| "(expected API level 8, actual: {})".format(api_level) |
| ) |
| |
| return api_level |
| |
| |
| def fetch_abi(args: argparse.Namespace) -> str: |
| """ |
| Figure out the intersection of which ABIs the application is built for and |
| which ones the device supports, then pick the one preferred by the device, |
| so that we know which gdbserver to push and run on the device. |
| """ |
| |
| app_abis = dump_var(args, "APP_ABI").split(" ") |
| if "all" in app_abis: |
| app_abis = dump_var(args, "NDK_ALL_ABIS").split(" ") |
| app_abis_msg = "Application ABIs: {}".format(", ".join(app_abis)) |
| log(app_abis_msg) |
| |
| new_abi_props = ["ro.product.cpu.abilist"] |
| old_abi_props = ["ro.product.cpu.abi", "ro.product.cpu.abi2"] |
| abi_props = new_abi_props |
| if args.device.get_prop("ro.product.cpu.abilist") is None: |
| abi_props = old_abi_props |
| |
| device_abis: list[str] = [] |
| for key in abi_props: |
| value = args.device.get_prop(key) |
| if value is not None: |
| device_abis.extend(value.split(",")) |
| |
| device_abis_msg = "Device ABIs: {}".format(", ".join(device_abis)) |
| log(device_abis_msg) |
| |
| for abi in device_abis: |
| if abi in app_abis: |
| # TODO(jmgao): Do we expect gdb to work with ARM-x86 translation? |
| log("Selecting ABI: {}".format(abi)) |
| return abi |
| |
| msg = "Application cannot run on the selected device." |
| |
| # Don't repeat ourselves. |
| if not args.verbose: |
| msg += "\n{}\n{}".format(app_abis_msg, device_abis_msg) |
| |
| error(msg) |
| |
| |
| def get_run_as_cmd(user: str, cmd: list[str]) -> list[str]: |
| return ["run-as", user] + cmd |
| |
| |
| def get_app_data_dir(args: argparse.Namespace, package_name: str) -> str: |
| cmd = ["/system/bin/sh", "-c", "pwd", "2>/dev/null"] |
| cmd = get_run_as_cmd(package_name, cmd) |
| device: adb.AndroidDevice = args.device |
| (rc, stdout, _) = device.shell_nocheck(cmd) |
| if rc != 0: |
| error( |
| "Could not find application's data directory. Are you sure that " |
| "the application is installed and debuggable?" |
| ) |
| data_dir = stdout.strip() |
| |
| # Applications with minSdkVersion >= 24 will have their data directories |
| # created with rwx------ permissions, preventing adbd from forwarding to |
| # the gdbserver socket. To be safe, if we're on a device >= 24, always |
| # chmod the directory. |
| if get_api_level(args.device) >= 24: |
| chmod_cmd = ["/system/bin/chmod", "a+x", data_dir] |
| chmod_cmd = get_run_as_cmd(package_name, chmod_cmd) |
| (rc, _, _) = args.device.shell_nocheck(chmod_cmd) |
| if rc != 0: |
| error("Failed to make application data directory world executable") |
| |
| log("Found application data directory: {}".format(data_dir)) |
| return data_dir |
| |
| |
| def abi_to_arch(abi: str) -> str: |
| if abi.startswith("armeabi"): |
| return "arm" |
| if abi == "arm64-v8a": |
| return "arm64" |
| return abi |
| |
| |
| def abi_to_llvm_arch(abi: str) -> str: |
| if abi.startswith("armeabi"): |
| return "arm" |
| if abi == "arm64-v8a": |
| return "aarch64" |
| if abi == "x86": |
| return "i386" |
| return "x86_64" |
| |
| |
| def get_llvm_host_name() -> str: |
| platform = sys.platform |
| if platform.startswith("win"): |
| return "windows-x86_64" |
| if platform.startswith("darwin"): |
| return "darwin-x86_64" |
| return "linux-x86_64" |
| |
| |
| def get_python_executable(toolchain_path: str) -> str: |
| if sys.platform.startswith("win"): |
| return os.path.join(toolchain_path, "python3", "python.exe") |
| return os.path.join(toolchain_path, "python3", "bin", "python3") |
| |
| |
| def get_lldb_path(toolchain_path: str) -> str | None: |
| for lldb_name in ["lldb.sh", "lldb.cmd", "lldb", "lldb.exe"]: |
| debugger_path = os.path.join(toolchain_path, "bin", lldb_name) |
| if os.path.isfile(debugger_path): |
| return debugger_path |
| return None |
| |
| |
| def get_llvm_package_version(llvm_toolchain_dir: str) -> str: |
| version_file_path = os.path.join(llvm_toolchain_dir, "AndroidVersion.txt") |
| try: |
| version_file = open(version_file_path, "r", encoding="utf-8") |
| except IOError: |
| error( |
| "Failed to open llvm package version file: '{}'.".format(version_file_path) |
| ) |
| |
| with version_file: |
| return version_file.readline().strip() |
| |
| |
| def get_debugger_server_path( |
| args: argparse.Namespace, |
| package_name: str, |
| app_data_dir: str, |
| arch: str, |
| server_name: str, |
| local_path: str, |
| ) -> str: |
| app_debugger_server_path = "{}/lib/{}".format(app_data_dir, server_name) |
| cmd = ["ls", app_debugger_server_path, "2>/dev/null"] |
| cmd = get_run_as_cmd(package_name, cmd) |
| (rc, _, _) = args.device.shell_nocheck(cmd) |
| if rc == 0: |
| log("Found app {}: {}".format(server_name, app_debugger_server_path)) |
| return app_debugger_server_path |
| |
| # We need to upload our debugger server |
| log( |
| "App {} not found at {}, uploading.".format( |
| server_name, app_debugger_server_path |
| ) |
| ) |
| remote_path = "/data/local/tmp/{}-{}".format(arch, server_name) |
| args.device.push(local_path, remote_path) |
| |
| # Copy debugger server into the data directory on M+, because selinux prevents |
| # execution of binaries directly from /data/local/tmp. |
| if get_api_level(args.device) >= 23: |
| destination = "{}/{}-{}".format(app_data_dir, arch, server_name) |
| log("Copying {} to {}.".format(server_name, destination)) |
| cmd = [ |
| "cat", |
| remote_path, |
| "|", |
| "run-as", |
| package_name, |
| "sh", |
| "-c", |
| "'cat > {}'".format(destination), |
| ] |
| (rc, _, _) = args.device.shell_nocheck(cmd) |
| if rc != 0: |
| error("Failed to copy {} to {}.".format(server_name, destination)) |
| (rc, _, _) = args.device.shell_nocheck( |
| ["run-as", package_name, "chmod", "700", destination] |
| ) |
| if rc != 0: |
| error("Failed to chmod {} at {}.".format(server_name, destination)) |
| |
| remote_path = destination |
| |
| log("Uploaded {} to {}".format(server_name, remote_path)) |
| return remote_path |
| |
| |
| def pull_binaries(device: adb.AndroidDevice, out_dir: str, app_64bit: bool) -> None: |
| required_files = [] |
| libraries = ["libc.so", "libm.so", "libdl.so"] |
| |
| if app_64bit: |
| required_files = ["/system/bin/app_process64", "/system/bin/linker64"] |
| library_path = "/system/lib64" |
| else: |
| required_files = ["/system/bin/linker"] |
| library_path = "/system/lib" |
| |
| for library in libraries: |
| required_files.append(posixpath.join(library_path, library)) |
| |
| for required_file in required_files: |
| # os.path.join not used because joining absolute paths will pick the last one |
| local_path = os.path.realpath(out_dir + required_file) |
| local_dirname = os.path.dirname(local_path) |
| if not os.path.isdir(local_dirname): |
| os.makedirs(local_dirname) |
| log("Pulling '{}' to '{}'".format(required_file, local_path)) |
| device.pull(required_file, local_path) |
| |
| # /system/bin/app_process is 32-bit on 32-bit devices, but a symlink to |
| # app_process64 on 64-bit. If we need the 32-bit version, try to pull |
| # app_process32, and if that fails, pull app_process. |
| if not app_64bit: |
| destination = os.path.realpath(out_dir + "/system/bin/app_process") |
| try: |
| device.pull("/system/bin/app_process32", destination) |
| except subprocess.CalledProcessError: |
| device.pull("/system/bin/app_process", destination) |
| |
| |
| def generate_lldb_script( |
| args: argparse.Namespace, |
| sysroot: str, |
| binary_path: str, |
| app_64bit: bool, |
| jdb_pid: int, |
| llvm_toolchain_dir: str, |
| ) -> str: |
| lldb_commands = [] |
| solib_search_paths = [ |
| "{}/system/bin".format(sysroot), |
| "{}/system/lib{}".format(sysroot, "64" if app_64bit else ""), |
| ] |
| lldb_commands.append( |
| "settings append target.exec-search-paths {}".format( |
| " ".join(solib_search_paths) |
| ) |
| ) |
| |
| lldb_commands.append("target create '{}'".format(binary_path)) |
| lldb_commands.append("target modules search-paths add / {}/".format(sysroot)) |
| |
| lldb_commands.append("gdb-remote {}".format(args.port)) |
| if jdb_pid is not None: |
| # After we've interrupted the app, reinvoke ndk-gdb.py to start jdb and |
| # wake up the app. |
| lldb_commands.append( |
| """ |
| script |
| def start_jdb_to_unblock_app(): |
| import subprocess |
| subprocess.Popen({}) |
| |
| start_jdb_to_unblock_app() |
| exit() |
| """.format( |
| repr( |
| [ |
| # We can't use sys.executable because it is the python2. |
| # lldb wrapper will set PYTHONHOME to point to python3. |
| get_python_executable(llvm_toolchain_dir), |
| os.path.realpath(__file__), |
| "--internal-wakeup-pid-with-jdb", |
| args.device.adb_path, |
| args.device.serial, |
| args.jdb_cmd, |
| str(jdb_pid), |
| str(bool(args.verbose)), |
| ] |
| ) |
| ) |
| ) |
| |
| if args.tui: |
| lldb_commands.append("gui") |
| |
| if args.exec_file is not None: |
| try: |
| exec_file = open(args.exec_file, "r", encoding="utf-8") |
| except IOError: |
| error("Failed to open lldb exec file: '{}'.".format(args.exec_file)) |
| |
| with exec_file: |
| lldb_commands.append(exec_file.read()) |
| |
| return "\n".join(lldb_commands) |
| |
| |
| def generate_gdb_script( |
| args: argparse.Namespace, |
| sysroot: str, |
| binary_path: str, |
| app_64bit: bool, |
| jdb_pid: int, |
| connect_timeout: int = 5, |
| ) -> str: |
| if sys.platform.startswith("win"): |
| # GDB expects paths to use forward slashes. |
| sysroot = sysroot.replace("\\", "/") |
| binary_path = binary_path.replace("\\", "/") |
| |
| gdb_commands = "set osabi GNU/Linux\n" |
| gdb_commands += "file '{}'\n".format(binary_path) |
| |
| solib_search_paths = [sysroot, "{}/system/bin".format(sysroot)] |
| if app_64bit: |
| solib_search_paths.append("{}/system/lib64".format(sysroot)) |
| else: |
| solib_search_paths.append("{}/system/lib".format(sysroot)) |
| solib_search_path = os.pathsep.join(solib_search_paths) |
| gdb_commands += "set solib-absolute-prefix {}\n".format(sysroot) |
| gdb_commands += "set solib-search-path {}\n".format(solib_search_path) |
| |
| # Try to connect for a few seconds, sometimes the device gdbserver takes |
| # a little bit to come up, especially on emulators. |
| gdb_commands += """ |
| python |
| |
| def target_remote_with_retry(target, timeout_seconds): |
| import time |
| end_time = time.time() + timeout_seconds |
| while True: |
| try: |
| gdb.execute('target remote ' + target) |
| return True |
| except gdb.error as e: |
| time_left = end_time - time.time() |
| if time_left < 0 or time_left > timeout_seconds: |
| print("Error: unable to connect to device.") |
| print(e) |
| return False |
| time.sleep(min(0.25, time_left)) |
| |
| target_remote_with_retry(':{}', {}) |
| |
| end |
| """.format( |
| args.port, connect_timeout |
| ) |
| |
| if jdb_pid is not None: |
| # After we've interrupted the app, reinvoke ndk-gdb.py to start jdb and |
| # wake up the app. |
| gdb_commands += """ |
| python |
| def start_jdb_to_unblock_app(): |
| import subprocess |
| subprocess.Popen({}) |
| start_jdb_to_unblock_app() |
| end |
| """.format( |
| repr( |
| [ |
| sys.executable, |
| os.path.realpath(__file__), |
| "--internal-wakeup-pid-with-jdb", |
| args.device.adb_path, |
| args.device.serial, |
| args.jdb_cmd, |
| str(jdb_pid), |
| str(bool(args.verbose)), |
| ] |
| ) |
| ) |
| |
| if args.exec_file is not None: |
| try: |
| exec_file = open(args.exec_file, "r", encoding="utf-8") |
| except IOError: |
| error("Failed to open GDB exec file: '{}'.".format(args.exec_file)) |
| |
| with exec_file: |
| gdb_commands += exec_file.read() |
| |
| return gdb_commands |
| |
| |
| def start_jdb(argv_subset: list[str]) -> None: |
| adb_path, serial, jdb_cmd, pid_str, verbose = argv_subset |
| pid = int(pid_str) |
| device = adb.get_device(serial, adb_path=adb_path) |
| if verbose == "True": |
| enable_verbose_logging() |
| |
| log("Starting jdb to unblock application.") |
| |
| # Do setup stuff to keep ^C in the parent from killing us. |
| signal.signal(signal.SIGINT, signal.SIG_IGN) |
| windows = sys.platform.startswith("win") |
| if not windows: |
| os.setpgrp() |
| |
| jdb_port = 65534 |
| device.forward("tcp:{}".format(jdb_port), "jdwp:{}".format(pid)) |
| jdb_args = [ |
| jdb_cmd, |
| "-connect", |
| "com.sun.jdi.SocketAttach:hostname=localhost,port={}".format(jdb_port), |
| ] |
| |
| if sys.platform == "win32": |
| flags = subprocess.CREATE_NEW_PROCESS_GROUP |
| else: |
| flags = 0 |
| jdb = subprocess.Popen( |
| jdb_args, |
| stdin=subprocess.PIPE, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT, |
| creationflags=flags, |
| text=True, |
| ) |
| |
| assert jdb.stdin is not None |
| assert jdb.stdout is not None |
| # Wait until jdb can communicate with the app. Once it can, the app will |
| # start polling for a Java debugger (e.g. every 200ms). We need to wait |
| # a while longer then so that the app notices jdb. |
| jdb_magic = "__verify_jdb_has_started__" |
| jdb.stdin.write('print "{}"\n'.format(jdb_magic)) |
| saw_magic_str = False |
| while True: |
| line = jdb.stdout.readline() |
| if line == "": |
| break |
| log("jdb output: " + line.rstrip()) |
| if jdb_magic in line and not saw_magic_str: |
| saw_magic_str = True |
| time.sleep(0.3) |
| jdb.stdin.write("exit\n") |
| jdb.wait() |
| if saw_magic_str: |
| log("JDB finished unblocking application.") |
| else: |
| log("error: did not find magic string in JDB output.") |
| |
| |
| def advise_apk_debugging() -> None: |
| print("**Android Studio's debugger can be used for non-Studio projects.**") |
| print("See https://developer.android.com/studio/debug/apk-debugger") |
| print() |
| print( |
| textwrap.dedent( |
| """\ |
| ndk-lldb is still usable for debugging command line Android tools or |
| ANT-based app builds, but it was never meant to handle other use |
| cases. Android Studio can debug your APK even if Android Studio |
| wasn't used to build the project, and this will be *much* easier |
| than using ndk-lldb in most circumstances. |
| """ |
| ) |
| ) |
| |
| |
| def main() -> None: |
| if sys.argv[1:2] == ["--internal-wakeup-pid-with-jdb"]: |
| start_jdb(sys.argv[2:]) |
| return |
| |
| advise_apk_debugging() |
| |
| args = handle_args() |
| device = args.device |
| use_lldb = not args.no_lldb |
| |
| if not use_lldb: |
| print("WARNING: --no-lldb was used but GDB is no longer supported.") |
| print("GDB will be used, but will be removed in the next release.") |
| |
| if device is None: |
| error("Could not find a unique connected device/emulator.") |
| |
| # Warn on old Pixel C firmware (b/29381985). Newer devices may have Yama |
| # enabled but still work with ndk-gdb (b/19277529). |
| yama_check = device.shell_nocheck( |
| ["cat", "/proc/sys/kernel/yama/ptrace_scope", "2>/dev/null"] |
| ) |
| if ( |
| yama_check[0] == 0 |
| and yama_check[1].rstrip() not in ["", "0"] |
| and (device.get_prop("ro.build.product"), device.get_prop("ro.product.name")) |
| == ("dragon", "ryu") |
| ): |
| print( |
| "WARNING: The device uses Yama ptrace_scope to restrict debugging. ndk-gdb will" |
| ) |
| print( |
| " likely be unable to attach to a process. With root access, the restriction" |
| ) |
| print( |
| " can be lifted by writing 0 to /proc/sys/kernel/yama/ptrace_scope. Consider" |
| ) |
| print(" upgrading your Pixel C to MXC89L or newer, where Yama is disabled.") |
| |
| adb_version = subprocess.check_output(device.adb_cmd + ["version"]).decode() |
| log("ADB command used: '{}'".format(" ".join(device.adb_cmd))) |
| log("ADB version: {}".format(" ".join(adb_version.splitlines()))) |
| |
| project = find_project(args) |
| if args.package_name: |
| log("Attaching to specified package: {}".format(args.package_name)) |
| else: |
| parse_manifest(args) |
| |
| pkg_name = args.package_name |
| |
| if args.launch is False: |
| log("Attaching to existing application process.") |
| else: |
| args.launch = select_target(args) |
| log("Selected target activity: '{}'".format(args.launch)) |
| |
| abi = fetch_abi(args) |
| arch = abi_to_arch(abi) |
| |
| out_dir = os.path.join(project, (dump_var(args, "TARGET_OUT", abi))) |
| out_dir = os.path.realpath(out_dir) |
| |
| app_data_dir = get_app_data_dir(args, pkg_name) |
| |
| llvm_toolchain_dir = os.path.join( |
| NDK_PATH, "toolchains", "llvm", "prebuilt", get_llvm_host_name() |
| ) |
| if use_lldb: |
| server_local_path = os.path.join( |
| llvm_toolchain_dir, |
| "lib64", |
| "clang", |
| get_llvm_package_version(llvm_toolchain_dir), |
| "lib", |
| "linux", |
| abi_to_llvm_arch(abi), |
| "lldb-server", |
| ) |
| server_name = "lldb-server" |
| else: |
| server_local_path = "{}/prebuilt/android-{}/gdbserver/gdbserver" |
| server_local_path = server_local_path.format(NDK_PATH, arch) |
| server_name = "gdbserver" |
| if not os.path.exists(server_local_path): |
| error("Can not find {}: {}".format(server_name, server_local_path)) |
| log("Using {}: {}".format(server_name, server_local_path)) |
| debugger_server_path = get_debugger_server_path( |
| args, pkg_name, app_data_dir, arch, server_name, server_local_path |
| ) |
| |
| # Kill the process and gdbserver if requested. |
| if args.force: |
| kill_pids = gdbrunner.get_pids(device, debugger_server_path) |
| if args.launch: |
| kill_pids += gdbrunner.get_pids(device, pkg_name) |
| kill_pids = [str(pid) for pid in kill_pids] |
| if kill_pids: |
| log("Killing processes: {}".format(", ".join(kill_pids))) |
| device.shell_nocheck(["run-as", pkg_name, "kill", "-9"] + kill_pids) |
| |
| # Launch the application if needed, and get its pid |
| if args.launch: |
| am_cmd = ["am", "start"] |
| if not args.nowait: |
| am_cmd.append("-D") |
| component_name = "{}/{}".format(pkg_name, args.launch) |
| am_cmd.append(component_name) |
| log("Launching activity {}...".format(component_name)) |
| (rc, _, _) = device.shell_nocheck(am_cmd) |
| if rc != 0: |
| error("Failed to start {}".format(component_name)) |
| |
| if args.delay > 0.0: |
| log("Sleeping for {} seconds.".format(args.delay)) |
| time.sleep(args.delay) |
| |
| pids = gdbrunner.get_pids(device, pkg_name) |
| if len(pids) == 0: |
| error("Failed to find running process '{}'".format(pkg_name)) |
| if len(pids) > 1: |
| error("Multiple running processes named '{}'".format(pkg_name)) |
| pid = pids[0] |
| |
| # Pull the linker, zygote, and notable system libraries |
| app_64bit = "64" in abi |
| pull_binaries(device, out_dir, app_64bit) |
| if app_64bit: |
| zygote_path = os.path.join(out_dir, "system", "bin", "app_process64") |
| else: |
| zygote_path = os.path.join(out_dir, "system", "bin", "app_process") |
| |
| # Start gdbserver. |
| debug_socket = posixpath.join(app_data_dir, "debug_socket") |
| log("Starting {}...".format(server_name)) |
| gdbrunner.start_gdbserver( |
| device, |
| None, |
| debugger_server_path, |
| target_pid=pid, |
| run_cmd=None, |
| debug_socket=debug_socket, |
| port=args.port, |
| run_as_cmd=["run-as", pkg_name], |
| lldb=use_lldb, |
| ) |
| |
| # Start jdb to unblock the application if necessary. |
| jdb_pid = pid if (args.launch and not args.nowait) else None |
| |
| # Start gdb. |
| if use_lldb: |
| script_commands = generate_lldb_script( |
| args, out_dir, zygote_path, app_64bit, jdb_pid, llvm_toolchain_dir |
| ) |
| debugger_path = get_lldb_path(llvm_toolchain_dir) |
| flags = [] |
| else: |
| script_commands = generate_gdb_script( |
| args, out_dir, zygote_path, app_64bit, jdb_pid |
| ) |
| debugger_path = os.path.join(ndk_bin_path(), "gdb") |
| flags = ["--tui"] if args.tui else [] |
| print(debugger_path) |
| gdbrunner.start_gdb(debugger_path, script_commands, flags, lldb=use_lldb) |
| |
| |
| if __name__ == "__main__": |
| main() |