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