Home | History | Annotate | Download | only in scripts
      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 import adb
     19 import argparse
     20 import json
     21 import logging
     22 import os
     23 import re
     24 import subprocess
     25 import sys
     26 import textwrap
     27 
     28 # Shared functions across gdbclient.py and ndk-gdb.py.
     29 import gdbrunner
     30 
     31 def get_gdbserver_path(root, arch):
     32     path = "{}/prebuilts/misc/gdbserver/android-{}/gdbserver{}"
     33     if arch.endswith("64"):
     34         return path.format(root, arch, "64")
     35     else:
     36         return path.format(root, arch, "")
     37 
     38 
     39 def get_tracer_pid(device, pid):
     40     if pid is None:
     41         return 0
     42 
     43     line, _ = device.shell(["grep", "-e", "^TracerPid:", "/proc/{}/status".format(pid)])
     44     tracer_pid = re.sub('TracerPid:\t(.*)\n', r'\1', line)
     45     return int(tracer_pid)
     46 
     47 
     48 def parse_args():
     49     parser = gdbrunner.ArgumentParser()
     50 
     51     group = parser.add_argument_group(title="attach target")
     52     group = group.add_mutually_exclusive_group(required=True)
     53     group.add_argument(
     54         "-p", dest="target_pid", metavar="PID", type=int,
     55         help="attach to a process with specified PID")
     56     group.add_argument(
     57         "-n", dest="target_name", metavar="NAME",
     58         help="attach to a process with specified name")
     59     group.add_argument(
     60         "-r", dest="run_cmd", metavar="CMD", nargs=argparse.REMAINDER,
     61         help="run a binary on the device, with args")
     62 
     63     parser.add_argument(
     64         "--port", nargs="?", default="5039",
     65         help="override the port used on the host [default: 5039]")
     66     parser.add_argument(
     67         "--user", nargs="?", default="root",
     68         help="user to run commands as on the device [default: root]")
     69     parser.add_argument(
     70         "--setup-forwarding", default=None, choices=["gdb", "vscode"],
     71         help=("Setup the gdbserver and port forwarding. Prints commands or " +
     72               ".vscode/launch.json configuration needed to connect the debugging " +
     73               "client to the server."))
     74 
     75     parser.add_argument(
     76         "--env", nargs=1, action="append", metavar="VAR=VALUE",
     77         help="set environment variable when running a binary")
     78 
     79     return parser.parse_args()
     80 
     81 
     82 def verify_device(root, device):
     83     name = device.get_prop("ro.product.name")
     84     target_device = os.environ["TARGET_PRODUCT"]
     85     if target_device != name:
     86         msg = "TARGET_PRODUCT ({}) does not match attached device ({})"
     87         sys.exit(msg.format(target_device, name))
     88 
     89 
     90 def get_remote_pid(device, process_name):
     91     processes = gdbrunner.get_processes(device)
     92     if process_name not in processes:
     93         msg = "failed to find running process {}".format(process_name)
     94         sys.exit(msg)
     95     pids = processes[process_name]
     96     if len(pids) > 1:
     97         msg = "multiple processes match '{}': {}".format(process_name, pids)
     98         sys.exit(msg)
     99 
    100     # Fetch the binary using the PID later.
    101     return pids[0]
    102 
    103 
    104 def ensure_linker(device, sysroot, is64bit):
    105     local_path = os.path.join(sysroot, "system", "bin", "linker")
    106     remote_path = "/system/bin/linker"
    107     if is64bit:
    108         local_path += "64"
    109         remote_path += "64"
    110     if not os.path.exists(local_path):
    111         device.pull(remote_path, local_path)
    112 
    113 
    114 def handle_switches(args, sysroot):
    115     """Fetch the targeted binary and determine how to attach gdb.
    116 
    117     Args:
    118         args: Parsed arguments.
    119         sysroot: Local sysroot path.
    120 
    121     Returns:
    122         (binary_file, attach_pid, run_cmd).
    123         Precisely one of attach_pid or run_cmd will be None.
    124     """
    125 
    126     device = args.device
    127     binary_file = None
    128     pid = None
    129     run_cmd = None
    130 
    131     args.su_cmd = ["su", args.user] if args.user else []
    132 
    133     if args.target_pid:
    134         # Fetch the binary using the PID later.
    135         pid = args.target_pid
    136     elif args.target_name:
    137         # Fetch the binary using the PID later.
    138         pid = get_remote_pid(device, args.target_name)
    139     elif args.run_cmd:
    140         if not args.run_cmd[0]:
    141             sys.exit("empty command passed to -r")
    142         run_cmd = args.run_cmd
    143         if not run_cmd[0].startswith("/"):
    144             try:
    145                 run_cmd[0] = gdbrunner.find_executable_path(device, args.run_cmd[0],
    146                                                             run_as_cmd=args.su_cmd)
    147             except RuntimeError:
    148               sys.exit("Could not find executable '{}' passed to -r, "
    149                        "please provide an absolute path.".format(args.run_cmd[0]))
    150 
    151         binary_file, local = gdbrunner.find_file(device, run_cmd[0], sysroot,
    152                                                  run_as_cmd=args.su_cmd)
    153     if binary_file is None:
    154         assert pid is not None
    155         try:
    156             binary_file, local = gdbrunner.find_binary(device, pid, sysroot,
    157                                                        run_as_cmd=args.su_cmd)
    158         except adb.ShellError:
    159             sys.exit("failed to pull binary for PID {}".format(pid))
    160 
    161     if not local:
    162         logging.warning("Couldn't find local unstripped executable in {},"
    163                         " symbols may not be available.".format(sysroot))
    164 
    165     return (binary_file, pid, run_cmd)
    166 
    167 def generate_vscode_script(gdbpath, root, sysroot, binary_name, port, dalvik_gdb_script, solib_search_path):
    168     # TODO It would be nice if we didn't need to copy this or run the
    169     #      gdbclient.py program manually. Doing this would probably require
    170     #      writing a vscode extension or modifying an existing one.
    171     res = {
    172         "name": "(gdbclient.py) Attach {} (port: {})".format(binary_name.split("/")[-1], port),
    173         "type": "cppdbg",
    174         "request": "launch",  # Needed for gdbserver.
    175         "cwd": root,
    176         "program": binary_name,
    177         "MIMode": "gdb",
    178         "miDebuggerServerAddress": "localhost:{}".format(port),
    179         "miDebuggerPath": gdbpath,
    180         "setupCommands": [
    181             {
    182                 # Required for vscode.
    183                 "description": "Enable pretty-printing for gdb",
    184                 "text": "-enable-pretty-printing",
    185                 "ignoreFailures": True,
    186             },
    187             {
    188                 "description": "gdb command: dir",
    189                 "text": "-environment-directory {}".format(root),
    190                 "ignoreFailures": False
    191             },
    192             {
    193                 "description": "gdb command: set solib-search-path",
    194                 "text": "-gdb-set solib-search-path {}".format(":".join(solib_search_path)),
    195                 "ignoreFailures": False
    196             },
    197             {
    198                 "description": "gdb command: set solib-absolute-prefix",
    199                 "text": "-gdb-set solib-absolute-prefix {}".format(sysroot),
    200                 "ignoreFailures": False
    201             },
    202         ]
    203     }
    204     if dalvik_gdb_script:
    205         res["setupCommands"].append({
    206             "description": "gdb command: source art commands",
    207             "text": "-interpreter-exec console \"source {}\"".format(dalvik_gdb_script),
    208             "ignoreFailures": False,
    209         })
    210     return json.dumps(res, indent=4)
    211 
    212 def generate_gdb_script(root, sysroot, binary_name, port, dalvik_gdb_script, solib_search_path, connect_timeout):
    213     solib_search_path = ":".join(solib_search_path)
    214 
    215     gdb_commands = ""
    216     gdb_commands += "file '{}'\n".format(binary_name)
    217     gdb_commands += "directory '{}'\n".format(root)
    218     gdb_commands += "set solib-absolute-prefix {}\n".format(sysroot)
    219     gdb_commands += "set solib-search-path {}\n".format(solib_search_path)
    220     if dalvik_gdb_script:
    221         gdb_commands += "source {}\n".format(dalvik_gdb_script)
    222 
    223     # Try to connect for a few seconds, sometimes the device gdbserver takes
    224     # a little bit to come up, especially on emulators.
    225     gdb_commands += """
    226 python
    227 
    228 def target_remote_with_retry(target, timeout_seconds):
    229   import time
    230   end_time = time.time() + timeout_seconds
    231   while True:
    232     try:
    233       gdb.execute("target extended-remote " + target)
    234       return True
    235     except gdb.error as e:
    236       time_left = end_time - time.time()
    237       if time_left < 0 or time_left > timeout_seconds:
    238         print("Error: unable to connect to device.")
    239         print(e)
    240         return False
    241       time.sleep(min(0.25, time_left))
    242 
    243 target_remote_with_retry(':{}', {})
    244 
    245 end
    246 """.format(port, connect_timeout)
    247 
    248     return gdb_commands
    249 
    250 def generate_setup_script(gdbpath, sysroot, binary_file, is64bit, port, debugger, connect_timeout=5):
    251     # Generate a setup script.
    252     # TODO: Detect the zygote and run 'art-on' automatically.
    253     root = os.environ["ANDROID_BUILD_TOP"]
    254     symbols_dir = os.path.join(sysroot, "system", "lib64" if is64bit else "lib")
    255     vendor_dir = os.path.join(sysroot, "vendor", "lib64" if is64bit else "lib")
    256 
    257     solib_search_path = []
    258     symbols_paths = ["", "hw", "ssl/engines", "drm", "egl", "soundfx"]
    259     vendor_paths = ["", "hw", "egl"]
    260     solib_search_path += [os.path.join(symbols_dir, x) for x in symbols_paths]
    261     solib_search_path += [os.path.join(vendor_dir, x) for x in vendor_paths]
    262 
    263     dalvik_gdb_script = os.path.join(root, "development", "scripts", "gdb", "dalvik.gdb")
    264     if not os.path.exists(dalvik_gdb_script):
    265         logging.warning(("couldn't find {} - ART debugging options will not " +
    266                          "be available").format(dalvik_gdb_script))
    267         dalvik_gdb_script = None
    268 
    269     if debugger == "vscode":
    270         return generate_vscode_script(
    271             gdbpath, root, sysroot, binary_file.name, port, dalvik_gdb_script, solib_search_path)
    272     elif debugger == "gdb":
    273         return generate_gdb_script(root, sysroot, binary_file.name, port, dalvik_gdb_script, solib_search_path, connect_timeout)
    274     else:
    275         raise Exception("Unknown debugger type " + debugger)
    276 
    277 
    278 def main():
    279     required_env = ["ANDROID_BUILD_TOP",
    280                     "ANDROID_PRODUCT_OUT", "TARGET_PRODUCT"]
    281     for env in required_env:
    282         if env not in os.environ:
    283             sys.exit(
    284                 "Environment variable '{}' not defined, have you run lunch?".format(env))
    285 
    286     args = parse_args()
    287     device = args.device
    288 
    289     if device is None:
    290         sys.exit("ERROR: Failed to find device.")
    291 
    292     root = os.environ["ANDROID_BUILD_TOP"]
    293     sysroot = os.path.join(os.environ["ANDROID_PRODUCT_OUT"], "symbols")
    294 
    295     # Make sure the environment matches the attached device.
    296     verify_device(root, device)
    297 
    298     debug_socket = "/data/local/tmp/debug_socket"
    299     pid = None
    300     run_cmd = None
    301 
    302     # Fetch binary for -p, -n.
    303     binary_file, pid, run_cmd = handle_switches(args, sysroot)
    304 
    305     with binary_file:
    306         arch = gdbrunner.get_binary_arch(binary_file)
    307         is64bit = arch.endswith("64")
    308 
    309         # Make sure we have the linker
    310         ensure_linker(device, sysroot, is64bit)
    311 
    312         tracer_pid = get_tracer_pid(device, pid)
    313         if tracer_pid == 0:
    314             cmd_prefix = args.su_cmd
    315             if args.env:
    316                 cmd_prefix += ['env'] + [v[0] for v in args.env]
    317 
    318             # Start gdbserver.
    319             gdbserver_local_path = get_gdbserver_path(root, arch)
    320             gdbserver_remote_path = "/data/local/tmp/{}-gdbserver".format(arch)
    321             gdbrunner.start_gdbserver(
    322                 device, gdbserver_local_path, gdbserver_remote_path,
    323                 target_pid=pid, run_cmd=run_cmd, debug_socket=debug_socket,
    324                 port=args.port, run_as_cmd=cmd_prefix)
    325         else:
    326             print "Connecting to tracing pid {} using local port {}".format(tracer_pid, args.port)
    327             gdbrunner.forward_gdbserver_port(device, local=args.port,
    328                                              remote="tcp:{}".format(args.port))
    329 
    330         # Find where gdb is
    331         if sys.platform.startswith("linux"):
    332             platform_name = "linux-x86"
    333         elif sys.platform.startswith("darwin"):
    334             platform_name = "darwin-x86"
    335         else:
    336             sys.exit("Unknown platform: {}".format(sys.platform))
    337 
    338         gdb_path = os.path.join(root, "prebuilts", "gdb", platform_name, "bin",
    339                                 "gdb")
    340         # Generate a gdb script.
    341         setup_commands = generate_setup_script(gdbpath=gdb_path,
    342                                                sysroot=sysroot,
    343                                                binary_file=binary_file,
    344                                                is64bit=is64bit,
    345                                                port=args.port,
    346                                                debugger=args.setup_forwarding or "gdb")
    347 
    348         if not args.setup_forwarding:
    349             # Print a newline to separate our messages from the GDB session.
    350             print("")
    351 
    352             # Start gdb.
    353             gdbrunner.start_gdb(gdb_path, setup_commands)
    354         else:
    355             print("")
    356             print setup_commands
    357             print("")
    358             if args.setup_forwarding == "vscode":
    359                 print textwrap.dedent("""
    360                         Paste the above json into .vscode/launch.json and start the debugger as
    361                         normal. Press enter in this terminal once debugging is finished to shutdown
    362                         the gdbserver and close all the ports.""")
    363             else:
    364                 print textwrap.dedent("""
    365                         Paste the above gdb commands into the gdb frontend to setup the gdbserver
    366                         connection. Press enter in this terminal once debugging is finished to
    367                         shutdown the gdbserver and close all the ports.""")
    368             print("")
    369             raw_input("Press enter to shutdown gdbserver")
    370 
    371 if __name__ == "__main__":
    372     main()
    373