• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
18import adb
19import argparse
20import json
21import logging
22import os
23import re
24import subprocess
25import sys
26import textwrap
27
28# Shared functions across gdbclient.py and ndk-gdb.py.
29import gdbrunner
30
31def 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
39def 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
48def 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
82def 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
90def 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
104def 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
114def 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
167def 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
212def 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 += """
226python
227
228def 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
243target_remote_with_retry(':{}', {})
244
245end
246""".format(port, connect_timeout)
247
248    return gdb_commands
249
250def 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
278def 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
371if __name__ == "__main__":
372    main()
373