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