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