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