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 posixpath
24import re
25import shutil
26import subprocess
27import sys
28import tempfile
29import textwrap
30
31# Shared functions across gdbclient.py and ndk-gdb.py.
32import gdbrunner
33
34g_temp_dirs = []
35
36
37def read_toolchain_config(root):
38    """Finds out current toolchain path and version."""
39    def get_value(str):
40        return str[str.index('"') + 1:str.rindex('"')]
41
42    config_path = os.path.join(root, 'build', 'soong', 'cc', 'config',
43                               'global.go')
44    with open(config_path) as f:
45        contents = f.readlines()
46    clang_base = ""
47    clang_version = ""
48    for line in contents:
49        line = line.strip()
50        if line.startswith('ClangDefaultBase'):
51            clang_base = get_value(line)
52        elif line.startswith('ClangDefaultVersion'):
53            clang_version = get_value(line)
54    return (clang_base, clang_version)
55
56
57def get_gdbserver_path(root, arch):
58    path = "{}/prebuilts/misc/gdbserver/android-{}/gdbserver{}"
59    if arch.endswith("64"):
60        return path.format(root, arch, "64")
61    else:
62        return path.format(root, arch, "")
63
64
65def get_lldb_path(toolchain_path):
66    for lldb_name in ['lldb.sh', 'lldb.cmd', 'lldb', 'lldb.exe']:
67        debugger_path = os.path.join(toolchain_path, "bin", lldb_name)
68        if os.path.isfile(debugger_path):
69            return debugger_path
70    return None
71
72
73def get_lldb_server_path(root, clang_base, clang_version, arch):
74    arch = {
75        'arm': 'arm',
76        'arm64': 'aarch64',
77        'x86': 'i386',
78        'x86_64': 'x86_64',
79    }[arch]
80    return os.path.join(root, clang_base, "linux-x86",
81                        clang_version, "runtimes_ndk_cxx", arch, "lldb-server")
82
83
84def get_tracer_pid(device, pid):
85    if pid is None:
86        return 0
87
88    line, _ = device.shell(["grep", "-e", "^TracerPid:", "/proc/{}/status".format(pid)])
89    tracer_pid = re.sub('TracerPid:\t(.*)\n', r'\1', line)
90    return int(tracer_pid)
91
92
93def parse_args():
94    parser = gdbrunner.ArgumentParser()
95
96    group = parser.add_argument_group(title="attach target")
97    group = group.add_mutually_exclusive_group(required=True)
98    group.add_argument(
99        "-p", dest="target_pid", metavar="PID", type=int,
100        help="attach to a process with specified PID")
101    group.add_argument(
102        "-n", dest="target_name", metavar="NAME",
103        help="attach to a process with specified name")
104    group.add_argument(
105        "-r", dest="run_cmd", metavar="CMD", nargs=argparse.REMAINDER,
106        help="run a binary on the device, with args")
107
108    parser.add_argument(
109        "--port", nargs="?", default="5039",
110        help="override the port used on the host [default: 5039]")
111    parser.add_argument(
112        "--user", nargs="?", default="root",
113        help="user to run commands as on the device [default: root]")
114    parser.add_argument(
115        "--setup-forwarding", default=None, choices=["gdb", "vscode"],
116        help=("Setup the gdbserver and port forwarding. Prints commands or " +
117              ".vscode/launch.json configuration needed to connect the debugging " +
118              "client to the server."))
119
120    lldb_group = parser.add_mutually_exclusive_group()
121    lldb_group.add_argument("--lldb", action="store_true", help="Use lldb.")
122    lldb_group.add_argument("--no-lldb", action="store_true", help="Do not use lldb.")
123
124    parser.add_argument(
125        "--env", nargs=1, action="append", metavar="VAR=VALUE",
126        help="set environment variable when running a binary")
127
128    return parser.parse_args()
129
130
131def verify_device(root, device):
132    name = device.get_prop("ro.product.name")
133    target_device = os.environ["TARGET_PRODUCT"]
134    if target_device != name:
135        msg = "TARGET_PRODUCT ({}) does not match attached device ({})"
136        sys.exit(msg.format(target_device, name))
137
138
139def get_remote_pid(device, process_name):
140    processes = gdbrunner.get_processes(device)
141    if process_name not in processes:
142        msg = "failed to find running process {}".format(process_name)
143        sys.exit(msg)
144    pids = processes[process_name]
145    if len(pids) > 1:
146        msg = "multiple processes match '{}': {}".format(process_name, pids)
147        sys.exit(msg)
148
149    # Fetch the binary using the PID later.
150    return pids[0]
151
152
153def make_temp_dir(prefix):
154    global g_temp_dirs
155    result = tempfile.mkdtemp(prefix='gdbclient-linker-')
156    g_temp_dirs.append(result)
157    return result
158
159
160def ensure_linker(device, sysroot, interp):
161    """Ensure that the device's linker exists on the host.
162
163    PT_INTERP is usually /system/bin/linker[64], but on the device, that file is
164    a symlink to /apex/com.android.runtime/bin/linker[64]. The symbolized linker
165    binary on the host is located in ${sysroot}/apex, not in ${sysroot}/system,
166    so add the ${sysroot}/apex path to the solib search path.
167
168    PT_INTERP will be /system/bin/bootstrap/linker[64] for executables using the
169    non-APEX/bootstrap linker. No search path modification is needed.
170
171    For a tapas build, only an unbundled app is built, and there is no linker in
172    ${sysroot} at all, so copy the linker from the device.
173
174    Returns:
175        A directory to add to the soinfo search path or None if no directory
176        needs to be added.
177    """
178
179    # Static executables have no interpreter.
180    if interp is None:
181        return None
182
183    # gdb will search for the linker using the PT_INTERP path. First try to find
184    # it in the sysroot.
185    local_path = os.path.join(sysroot, interp.lstrip("/"))
186    if os.path.exists(local_path):
187        return None
188
189    # If the linker on the device is a symlink, search for the symlink's target
190    # in the sysroot directory.
191    interp_real, _ = device.shell(["realpath", interp])
192    interp_real = interp_real.strip()
193    local_path = os.path.join(sysroot, interp_real.lstrip("/"))
194    if os.path.exists(local_path):
195        if posixpath.basename(interp) == posixpath.basename(interp_real):
196            # Add the interpreter's directory to the search path.
197            return os.path.dirname(local_path)
198        else:
199            # If PT_INTERP is linker_asan[64], but the sysroot file is
200            # linker[64], then copy the local file to the name gdb expects.
201            result = make_temp_dir('gdbclient-linker-')
202            shutil.copy(local_path, os.path.join(result, posixpath.basename(interp)))
203            return result
204
205    # Pull the system linker.
206    result = make_temp_dir('gdbclient-linker-')
207    device.pull(interp, os.path.join(result, posixpath.basename(interp)))
208    return result
209
210
211def handle_switches(args, sysroot):
212    """Fetch the targeted binary and determine how to attach gdb.
213
214    Args:
215        args: Parsed arguments.
216        sysroot: Local sysroot path.
217
218    Returns:
219        (binary_file, attach_pid, run_cmd).
220        Precisely one of attach_pid or run_cmd will be None.
221    """
222
223    device = args.device
224    binary_file = None
225    pid = None
226    run_cmd = None
227
228    args.su_cmd = ["su", args.user] if args.user else []
229
230    if args.target_pid:
231        # Fetch the binary using the PID later.
232        pid = args.target_pid
233    elif args.target_name:
234        # Fetch the binary using the PID later.
235        pid = get_remote_pid(device, args.target_name)
236    elif args.run_cmd:
237        if not args.run_cmd[0]:
238            sys.exit("empty command passed to -r")
239        run_cmd = args.run_cmd
240        if not run_cmd[0].startswith("/"):
241            try:
242                run_cmd[0] = gdbrunner.find_executable_path(device, args.run_cmd[0],
243                                                            run_as_cmd=args.su_cmd)
244            except RuntimeError:
245              sys.exit("Could not find executable '{}' passed to -r, "
246                       "please provide an absolute path.".format(args.run_cmd[0]))
247
248        binary_file, local = gdbrunner.find_file(device, run_cmd[0], sysroot,
249                                                 run_as_cmd=args.su_cmd)
250    if binary_file is None:
251        assert pid is not None
252        try:
253            binary_file, local = gdbrunner.find_binary(device, pid, sysroot,
254                                                       run_as_cmd=args.su_cmd)
255        except adb.ShellError:
256            sys.exit("failed to pull binary for PID {}".format(pid))
257
258    if not local:
259        logging.warning("Couldn't find local unstripped executable in {},"
260                        " symbols may not be available.".format(sysroot))
261
262    return (binary_file, pid, run_cmd)
263
264def generate_vscode_script(gdbpath, root, sysroot, binary_name, port, dalvik_gdb_script, solib_search_path):
265    # TODO It would be nice if we didn't need to copy this or run the
266    #      gdbclient.py program manually. Doing this would probably require
267    #      writing a vscode extension or modifying an existing one.
268    res = {
269        "name": "(gdbclient.py) Attach {} (port: {})".format(binary_name.split("/")[-1], port),
270        "type": "cppdbg",
271        "request": "launch",  # Needed for gdbserver.
272        "cwd": root,
273        "program": binary_name,
274        "MIMode": "gdb",
275        "miDebuggerServerAddress": "localhost:{}".format(port),
276        "miDebuggerPath": gdbpath,
277        "setupCommands": [
278            {
279                # Required for vscode.
280                "description": "Enable pretty-printing for gdb",
281                "text": "-enable-pretty-printing",
282                "ignoreFailures": True,
283            },
284            {
285                "description": "gdb command: dir",
286                "text": "-environment-directory {}".format(root),
287                "ignoreFailures": False
288            },
289            {
290                "description": "gdb command: set solib-search-path",
291                "text": "-gdb-set solib-search-path {}".format(":".join(solib_search_path)),
292                "ignoreFailures": False
293            },
294            {
295                "description": "gdb command: set solib-absolute-prefix",
296                "text": "-gdb-set solib-absolute-prefix {}".format(sysroot),
297                "ignoreFailures": False
298            },
299        ]
300    }
301    if dalvik_gdb_script:
302        res["setupCommands"].append({
303            "description": "gdb command: source art commands",
304            "text": "-interpreter-exec console \"source {}\"".format(dalvik_gdb_script),
305            "ignoreFailures": False,
306        })
307    return json.dumps(res, indent=4)
308
309def generate_gdb_script(root, sysroot, binary_name, port, dalvik_gdb_script, solib_search_path, connect_timeout):
310    solib_search_path = ":".join(solib_search_path)
311
312    gdb_commands = ""
313    gdb_commands += "file '{}'\n".format(binary_name)
314    gdb_commands += "directory '{}'\n".format(root)
315    gdb_commands += "set solib-absolute-prefix {}\n".format(sysroot)
316    gdb_commands += "set solib-search-path {}\n".format(solib_search_path)
317    if dalvik_gdb_script:
318        gdb_commands += "source {}\n".format(dalvik_gdb_script)
319
320    # Try to connect for a few seconds, sometimes the device gdbserver takes
321    # a little bit to come up, especially on emulators.
322    gdb_commands += """
323python
324
325def target_remote_with_retry(target, timeout_seconds):
326  import time
327  end_time = time.time() + timeout_seconds
328  while True:
329    try:
330      gdb.execute("target extended-remote " + target)
331      return True
332    except gdb.error as e:
333      time_left = end_time - time.time()
334      if time_left < 0 or time_left > timeout_seconds:
335        print("Error: unable to connect to device.")
336        print(e)
337        return False
338      time.sleep(min(0.25, time_left))
339
340target_remote_with_retry(':{}', {})
341
342end
343""".format(port, connect_timeout)
344
345    return gdb_commands
346
347
348def generate_lldb_script(root, sysroot, binary_name, port, solib_search_path):
349    commands = []
350    commands.append(
351        'settings append target.exec-search-paths {}'.format(' '.join(solib_search_path)))
352
353    commands.append('target create {}'.format(binary_name))
354    commands.append("settings set target.source-map '' '{}'".format(root))
355    commands.append('target modules search-paths add / {}/'.format(sysroot))
356    commands.append('gdb-remote {}'.format(port))
357    return '\n'.join(commands)
358
359
360def generate_setup_script(debugger_path, sysroot, linker_search_dir, binary_file, is64bit, port, debugger, connect_timeout=5):
361    # Generate a setup script.
362    # TODO: Detect the zygote and run 'art-on' automatically.
363    root = os.environ["ANDROID_BUILD_TOP"]
364    symbols_dir = os.path.join(sysroot, "system", "lib64" if is64bit else "lib")
365    vendor_dir = os.path.join(sysroot, "vendor", "lib64" if is64bit else "lib")
366
367    solib_search_path = []
368    symbols_paths = ["", "hw", "ssl/engines", "drm", "egl", "soundfx"]
369    vendor_paths = ["", "hw", "egl"]
370    solib_search_path += [os.path.join(symbols_dir, x) for x in symbols_paths]
371    solib_search_path += [os.path.join(vendor_dir, x) for x in vendor_paths]
372    if linker_search_dir is not None:
373        solib_search_path += [linker_search_dir]
374
375    dalvik_gdb_script = os.path.join(root, "development", "scripts", "gdb", "dalvik.gdb")
376    if not os.path.exists(dalvik_gdb_script):
377        logging.warning(("couldn't find {} - ART debugging options will not " +
378                         "be available").format(dalvik_gdb_script))
379        dalvik_gdb_script = None
380
381    if debugger == "vscode":
382        return generate_vscode_script(
383            debugger_path, root, sysroot, binary_file.name, port, dalvik_gdb_script, solib_search_path)
384    elif debugger == "gdb":
385        return generate_gdb_script(root, sysroot, binary_file.name, port, dalvik_gdb_script, solib_search_path, connect_timeout)
386    elif debugger == 'lldb':
387        return generate_lldb_script(
388            root, sysroot, binary_file.name, port, solib_search_path)
389    else:
390        raise Exception("Unknown debugger type " + debugger)
391
392
393def do_main():
394    required_env = ["ANDROID_BUILD_TOP",
395                    "ANDROID_PRODUCT_OUT", "TARGET_PRODUCT"]
396    for env in required_env:
397        if env not in os.environ:
398            sys.exit(
399                "Environment variable '{}' not defined, have you run lunch?".format(env))
400
401    args = parse_args()
402    device = args.device
403
404    if device is None:
405        sys.exit("ERROR: Failed to find device.")
406
407    root = os.environ["ANDROID_BUILD_TOP"]
408    sysroot = os.path.join(os.environ["ANDROID_PRODUCT_OUT"], "symbols")
409
410    # Make sure the environment matches the attached device.
411    verify_device(root, device)
412
413    debug_socket = "/data/local/tmp/debug_socket"
414    pid = None
415    run_cmd = None
416
417    # Fetch binary for -p, -n.
418    binary_file, pid, run_cmd = handle_switches(args, sysroot)
419
420    with binary_file:
421        if sys.platform.startswith("linux"):
422            platform_name = "linux-x86"
423        elif sys.platform.startswith("darwin"):
424            platform_name = "darwin-x86"
425        else:
426            sys.exit("Unknown platform: {}".format(sys.platform))
427
428        arch = gdbrunner.get_binary_arch(binary_file)
429        is64bit = arch.endswith("64")
430
431        # Make sure we have the linker
432        clang_base, clang_version = read_toolchain_config(root)
433        toolchain_path = os.path.join(root, clang_base, platform_name,
434                                      clang_version)
435        llvm_readobj_path = os.path.join(toolchain_path, "bin", "llvm-readobj")
436        interp = gdbrunner.get_binary_interp(binary_file.name, llvm_readobj_path)
437        linker_search_dir = ensure_linker(device, sysroot, interp)
438
439        tracer_pid = get_tracer_pid(device, pid)
440        use_lldb = args.lldb
441        if tracer_pid == 0:
442            cmd_prefix = args.su_cmd
443            if args.env:
444                cmd_prefix += ['env'] + [v[0] for v in args.env]
445
446            # Start gdbserver.
447            if use_lldb:
448                server_local_path = get_lldb_server_path(
449                    root, clang_base, clang_version, arch)
450                server_remote_path = "/data/local/tmp/{}-lldb-server".format(
451                    arch)
452            else:
453                server_local_path = get_gdbserver_path(root, arch)
454                server_remote_path = "/data/local/tmp/{}-gdbserver".format(
455                    arch)
456            gdbrunner.start_gdbserver(
457                device, server_local_path, server_remote_path,
458                target_pid=pid, run_cmd=run_cmd, debug_socket=debug_socket,
459                port=args.port, run_as_cmd=cmd_prefix, lldb=use_lldb)
460        else:
461            print(
462                "Connecting to tracing pid {} using local port {}".format(
463                    tracer_pid, args.port))
464            gdbrunner.forward_gdbserver_port(device, local=args.port,
465                                             remote="tcp:{}".format(args.port))
466
467        if use_lldb:
468            debugger_path = get_lldb_path(toolchain_path)
469            debugger = 'lldb'
470        else:
471            debugger_path = os.path.join(
472                root, "prebuilts", "gdb", platform_name, "bin", "gdb")
473            debugger = args.setup_forwarding or "gdb"
474
475        # Generate a gdb script.
476        setup_commands = generate_setup_script(debugger_path=debugger_path,
477                                               sysroot=sysroot,
478                                               linker_search_dir=linker_search_dir,
479                                               binary_file=binary_file,
480                                               is64bit=is64bit,
481                                               port=args.port,
482                                               debugger=debugger)
483
484        if use_lldb or not args.setup_forwarding:
485            # Print a newline to separate our messages from the GDB session.
486            print("")
487
488            # Start gdb.
489            gdbrunner.start_gdb(debugger_path, setup_commands, lldb=use_lldb)
490        else:
491            print("")
492            print(setup_commands)
493            print("")
494            if args.setup_forwarding == "vscode":
495                print(textwrap.dedent("""
496                        Paste the above json into .vscode/launch.json and start the debugger as
497                        normal. Press enter in this terminal once debugging is finished to shutdown
498                        the gdbserver and close all the ports."""))
499            else:
500                print(textwrap.dedent("""
501                        Paste the above gdb commands into the gdb frontend to setup the gdbserver
502                        connection. Press enter in this terminal once debugging is finished to
503                        shutdown the gdbserver and close all the ports."""))
504            print("")
505            raw_input("Press enter to shutdown gdbserver")
506
507
508def main():
509    try:
510        do_main()
511    finally:
512        global g_temp_dirs
513        for temp_dir in g_temp_dirs:
514            shutil.rmtree(temp_dir)
515
516
517if __name__ == "__main__":
518    main()
519