1# 2# Copyright (C) 2015 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15# 16 17"""Helpers used by both gdbclient.py and ndk-gdb.py.""" 18 19import adb 20import argparse 21import atexit 22import os 23import re 24import shlex 25import subprocess 26import sys 27import tempfile 28 29class ArgumentParser(argparse.ArgumentParser): 30 """ArgumentParser subclass that provides adb device selection.""" 31 32 def __init__(self): 33 super(ArgumentParser, self).__init__() 34 self.add_argument( 35 "--adb", dest="adb_path", 36 help="use specific adb command") 37 38 group = self.add_argument_group(title="device selection") 39 group = group.add_mutually_exclusive_group() 40 group.add_argument( 41 "-a", action="store_const", dest="device", const="-a", 42 help="directs commands to all interfaces") 43 group.add_argument( 44 "-d", action="store_const", dest="device", const="-d", 45 help="directs commands to the only connected USB device") 46 group.add_argument( 47 "-e", action="store_const", dest="device", const="-e", 48 help="directs commands to the only connected emulator") 49 group.add_argument( 50 "-s", metavar="SERIAL", action="store", dest="serial", 51 help="directs commands to device/emulator with the given serial") 52 53 def parse_args(self, args=None, namespace=None): 54 result = super(ArgumentParser, self).parse_args(args, namespace) 55 56 adb_path = result.adb_path or "adb" 57 58 # Try to run the specified adb command 59 try: 60 subprocess.check_output([adb_path, "version"], 61 stderr=subprocess.STDOUT) 62 except (OSError, subprocess.CalledProcessError): 63 msg = "ERROR: Unable to run adb executable (tried '{}')." 64 if not result.adb_path: 65 msg += "\n Try specifying its location with --adb." 66 sys.exit(msg.format(adb_path)) 67 68 try: 69 if result.device == "-a": 70 result.device = adb.get_device(adb_path=adb_path) 71 elif result.device == "-d": 72 result.device = adb.get_usb_device(adb_path=adb_path) 73 elif result.device == "-e": 74 result.device = adb.get_emulator_device(adb_path=adb_path) 75 else: 76 result.device = adb.get_device(result.serial, adb_path=adb_path) 77 except (adb.DeviceNotFoundError, adb.NoUniqueDeviceError, RuntimeError): 78 # Don't error out if we can't find a device. 79 result.device = None 80 81 return result 82 83 84def get_processes(device): 85 """Return a dict from process name to list of running PIDs on the device.""" 86 87 # Some custom ROMs use busybox instead of toolbox for ps. Without -w, 88 # busybox truncates the output, and very long package names like 89 # com.exampleisverylongtoolongbyfar.plasma exceed the limit. 90 # 91 # API 26 use toybox instead of toolbox for ps and needs -A to list 92 # all processes. 93 # 94 # Perform the check for this on the device to avoid an adb roundtrip 95 # Some devices might not have readlink or which, so we need to handle 96 # this as well. 97 # 98 # Gracefully handle [ or readlink being missing by always using `ps` if 99 # readlink is missing. (API 18 has [, but not readlink). 100 101 ps_script = """ 102 if $(ls /system/bin/readlink >/dev/null 2>&1); then 103 if [ $(readlink /system/bin/ps) == "busybox" ]; then 104 ps -w; 105 elif [ $(readlink /system/bin/ps) == "toybox" ]; then 106 ps -A; 107 else 108 ps; 109 fi 110 else 111 ps; 112 fi 113 """ 114 ps_script = " ".join([line.strip() for line in ps_script.splitlines()]) 115 116 output, _ = device.shell([ps_script]) 117 return parse_ps_output(output) 118 119 120def parse_ps_output(output): 121 processes = dict() 122 output = adb.split_lines(output.replace("\r", "")) 123 columns = output.pop(0).split() 124 try: 125 pid_column = columns.index("PID") 126 except ValueError: 127 pid_column = 1 128 while output: 129 columns = output.pop().split() 130 process_name = columns[-1] 131 pid = int(columns[pid_column]) 132 if process_name in processes: 133 processes[process_name].append(pid) 134 else: 135 processes[process_name] = [pid] 136 137 return processes 138 139 140def get_pids(device, process_name): 141 processes = get_processes(device) 142 return processes.get(process_name, []) 143 144 145def start_gdbserver(device, gdbserver_local_path, gdbserver_remote_path, 146 target_pid, run_cmd, debug_socket, port, run_as_cmd=[], 147 lldb=False, chroot="", cwd=""): 148 """Start gdbserver in the background and forward necessary ports. 149 150 Args: 151 device: ADB device to start gdbserver on. 152 gdbserver_local_path: Host path to push gdbserver from, can be None. 153 gdbserver_remote_path: Device path to push gdbserver to. 154 target_pid: PID of device process to attach to. 155 run_cmd: Command to run on the device. 156 debug_socket: Device path to place gdbserver unix domain socket. 157 port: Host port to forward the debug_socket to. 158 run_as_cmd: run-as or su command to prepend to commands. 159 chroot: The directory to pass to chroot when running the command. Cannot be set with cwd. 160 cwd: The working directory for the command. Cannot be set with chroot. 161 162 Returns: 163 Popen handle to the `adb shell` process gdbserver was started with. 164 """ 165 166 assert target_pid is None or run_cmd is None 167 168 if chroot: 169 # Chroot changes the working directory, so cd won't work if ran before chroot. 170 # On the other hand naively passing cd to chroot also won't work: chroot 171 # executes a binary, while cd is a bash command. 172 # They are unlikely to be used together anyway, so we simply disallow it. 173 if cwd: 174 raise ValueError('chroot and cwd cannot be set together') 175 run_as_cmd = ["chroot", chroot] + run_as_cmd 176 177 # Remove the old socket file. 178 device.shell_nocheck(run_as_cmd + ["rm", debug_socket]) 179 180 # Push gdbserver to the target. 181 if gdbserver_local_path is not None: 182 try: 183 device.push(gdbserver_local_path, chroot + gdbserver_remote_path) 184 # If the user here is potentially on Windows, adb cannot inspect execute 185 # permissions. Since we don't know where the users are, chmod 186 # gdbserver_remote_path on device regardless. 187 device.shell(["chmod", "+x", gdbserver_remote_path]) 188 except subprocess.CalledProcessError as err: 189 print("Command failed:") 190 print(shlex.join(err.cmd)) 191 print("Output:") 192 print(err.output.decode("utf-8")) 193 raise 194 195 # Run gdbserver. 196 gdbserver_cmd = [gdbserver_remote_path] 197 if lldb: 198 gdbserver_cmd.extend(["gdbserver", "unix://" + debug_socket]) 199 else: 200 gdbserver_cmd.extend(["--once", "+{}".format(debug_socket)]) 201 202 if target_pid is not None: 203 gdbserver_cmd += ["--attach", str(target_pid)] 204 else: 205 gdbserver_cmd += ["--"] + run_cmd 206 207 forward_gdbserver_port(device, local=port, remote="localfilesystem:{}".format(chroot + debug_socket)) 208 209 gdbserver_cmd = run_as_cmd + gdbserver_cmd 210 211 if lldb: 212 gdbserver_output_path = os.path.join(tempfile.gettempdir(), 213 "lldb-client.log") 214 print("Redirecting lldb-server output to {}".format(gdbserver_output_path)) 215 else: 216 gdbserver_output_path = os.path.join(tempfile.gettempdir(), 217 "gdbclient.log") 218 print("Redirecting gdbserver output to {}".format(gdbserver_output_path)) 219 gdbserver_output = open(gdbserver_output_path, 'w') 220 221 if cwd: 222 gdbserver_cmd = ["cd", cwd, "&&"] + gdbserver_cmd 223 return device.shell_popen(gdbserver_cmd, stdout=gdbserver_output, 224 stderr=gdbserver_output) 225 226 227def get_uid(device): 228 """Gets the uid adbd runs as.""" 229 line, _ = device.shell(["id", "-u"]) 230 return int(line.strip()) 231 232 233def forward_gdbserver_port(device, local, remote): 234 """Forwards local TCP port `port` to `remote` via `adb forward`.""" 235 if get_uid(device) != 0: 236 WARNING = '\033[93m' 237 ENDC = '\033[0m' 238 print(WARNING + 239 "Port forwarding may not work because adbd is not running as root. " + 240 " Run `adb root` to fix." + ENDC) 241 device.forward("tcp:{}".format(local), remote) 242 atexit.register(lambda: device.forward_remove("tcp:{}".format(local))) 243 244 245def find_file(device, executable_path, sysroot, run_as_cmd=None): 246 """Finds a device executable file. 247 248 This function first attempts to find the local file which will 249 contain debug symbols. If that fails, it will fall back to 250 downloading the stripped file from the device. 251 252 Args: 253 device: the AndroidDevice object to use. 254 executable_path: absolute path to the executable or symlink. 255 sysroot: absolute path to the built symbol sysroot. 256 run_as_cmd: if necessary, run-as or su command to prepend 257 258 Returns: 259 A tuple containing (<open file object>, <was found locally>). 260 261 Raises: 262 RuntimeError: could not find the executable binary. 263 ValueError: |executable_path| is not absolute. 264 """ 265 if not os.path.isabs(executable_path): 266 raise ValueError("'{}' is not an absolute path".format(executable_path)) 267 268 def generate_files(): 269 """Yields (<file name>, <found locally>) tuples.""" 270 # First look locally to avoid shelling into the device if possible. 271 # os.path.join() doesn't combine absolute paths, use + instead. 272 yield (sysroot + executable_path, True) 273 274 # Next check if the path is a symlink. 275 try: 276 target = device.shell(['readlink', '-e', '-n', executable_path])[0] 277 yield (sysroot + target, True) 278 except adb.ShellError: 279 pass 280 281 # Last, download the stripped executable from the device if necessary. 282 file_name = "gdbclient-binary-{}".format(os.getppid()) 283 remote_temp_path = "/data/local/tmp/{}".format(file_name) 284 local_path = os.path.join(tempfile.gettempdir(), file_name) 285 286 cmd = ["cat", executable_path, ">", remote_temp_path] 287 if run_as_cmd: 288 cmd = run_as_cmd + cmd 289 290 try: 291 device.shell(cmd) 292 except adb.ShellError: 293 raise RuntimeError("Failed to copy '{}' to temporary folder on " 294 "device".format(executable_path)) 295 device.pull(remote_temp_path, local_path) 296 yield (local_path, False) 297 298 for path, found_locally in generate_files(): 299 if os.path.isfile(path): 300 return (open(path, "rb"), found_locally) 301 raise RuntimeError('Could not find executable {}'.format(executable_path)) 302 303def find_executable_path(device, executable_name, run_as_cmd=None): 304 """Find a device executable from its name 305 306 This function calls which on the device to retrieve the absolute path of 307 the executable. 308 309 Args: 310 device: the AndroidDevice object to use. 311 executable_name: the name of the executable to find. 312 run_as_cmd: if necessary, run-as or su command to prepend 313 314 Returns: 315 The absolute path of the executable. 316 317 Raises: 318 RuntimeError: could not find the executable. 319 """ 320 cmd = ["which", executable_name] 321 if run_as_cmd: 322 cmd = run_as_cmd + cmd 323 324 try: 325 output, _ = device.shell(cmd) 326 return adb.split_lines(output)[0] 327 except adb.ShellError: 328 raise RuntimeError("Could not find executable '{}' on " 329 "device".format(executable_name)) 330 331def find_binary(device, pid, sysroot, run_as_cmd=None): 332 """Finds a device executable file corresponding to |pid|.""" 333 return find_file(device, "/proc/{}/exe".format(pid), sysroot, run_as_cmd) 334 335 336def get_binary_arch(binary_file): 337 """Parse a binary's ELF header for arch.""" 338 try: 339 binary_file.seek(0) 340 binary = binary_file.read(0x14) 341 except IOError: 342 raise RuntimeError("failed to read binary file") 343 ei_class = binary[0x4] # 1 = 32-bit, 2 = 64-bit 344 ei_data = binary[0x5] # Endianness 345 346 assert ei_class == 1 or ei_class == 2 347 if ei_data != 1: 348 raise RuntimeError("binary isn't little-endian?") 349 350 e_machine = binary[0x13] << 8 | binary[0x12] 351 if e_machine == 0x28: 352 assert ei_class == 1 353 return "arm" 354 elif e_machine == 0xB7: 355 assert ei_class == 2 356 return "arm64" 357 elif e_machine == 0x03: 358 assert ei_class == 1 359 return "x86" 360 elif e_machine == 0x3E: 361 assert ei_class == 2 362 return "x86_64" 363 elif e_machine == 0xF3: 364 assert ei_class == 2 365 return "riscv64" 366 else: 367 raise RuntimeError("unknown architecture: 0x{:x}".format(e_machine)) 368 369 370def get_binary_interp(binary_path, llvm_readobj_path): 371 args = [llvm_readobj_path, "--elf-output-style=GNU", "-l", binary_path] 372 output = subprocess.check_output(args, universal_newlines=True) 373 m = re.search(r"\[Requesting program interpreter: (.*?)\]\n", output) 374 if m is None: 375 return None 376 else: 377 return m.group(1) 378 379 380def start_gdb(gdb_path, gdb_commands, gdb_flags=None, lldb=False): 381 """Start gdb in the background and block until it finishes. 382 383 Args: 384 gdb_path: Path of the gdb binary. 385 gdb_commands: Contents of GDB script to run. 386 gdb_flags: List of flags to append to gdb command. 387 """ 388 389 # Windows disallows opening the file while it's open for writing. 390 script_fd, script_path = tempfile.mkstemp() 391 os.write(script_fd, gdb_commands.encode()) 392 os.close(script_fd) 393 if lldb: 394 script_parameter = "--source" 395 else: 396 script_parameter = "-x" 397 gdb_args = [gdb_path, script_parameter, script_path] + (gdb_flags or []) 398 399 creationflags = 0 400 if sys.platform.startswith("win"): 401 creationflags = subprocess.CREATE_NEW_CONSOLE 402 403 gdb_process = subprocess.Popen(gdb_args, creationflags=creationflags) 404 while gdb_process.returncode is None: 405 try: 406 gdb_process.communicate() 407 except KeyboardInterrupt: 408 pass 409 410 os.unlink(script_path) 411