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