Home | History | Annotate | Download | only in gdbrunner
      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 
     19 import adb
     20 import argparse
     21 import atexit
     22 import os
     23 import subprocess
     24 import sys
     25 import tempfile
     26 
     27 class ArgumentParser(argparse.ArgumentParser):
     28     """ArgumentParser subclass that provides adb device selection."""
     29 
     30     def __init__(self):
     31         super(ArgumentParser, self).__init__()
     32         self.add_argument(
     33             "--adb", dest="adb_path",
     34             help="Use specific adb command")
     35 
     36         group = self.add_argument_group(title="device selection")
     37         group = group.add_mutually_exclusive_group()
     38         group.add_argument(
     39             "-a", action="store_const", dest="device", const="-a",
     40             help="directs commands to all interfaces")
     41         group.add_argument(
     42             "-d", action="store_const", dest="device", const="-d",
     43             help="directs commands to the only connected USB device")
     44         group.add_argument(
     45             "-e", action="store_const", dest="device", const="-e",
     46             help="directs commands to the only connected emulator")
     47         group.add_argument(
     48             "-s", metavar="SERIAL", action="store", dest="serial",
     49             help="directs commands to device/emulator with the given serial")
     50 
     51     def parse_args(self, args=None, namespace=None):
     52         result = super(ArgumentParser, self).parse_args(args, namespace)
     53 
     54         adb_path = result.adb_path or "adb"
     55 
     56         # Try to run the specified adb command
     57         try:
     58             subprocess.check_output([adb_path, "version"],
     59                                     stderr=subprocess.STDOUT)
     60         except (OSError, subprocess.CalledProcessError):
     61             msg = "ERROR: Unable to run adb executable (tried '{}')."
     62             if not result.adb_path:
     63                 msg += "\n       Try specifying its location with --adb."
     64             sys.exit(msg.format(adb_path))
     65 
     66         try:
     67             if result.device == "-a":
     68                 result.device = adb.get_device(adb_path=adb_path)
     69             elif result.device == "-d":
     70                 result.device = adb.get_usb_device(adb_path=adb_path)
     71             elif result.device == "-e":
     72                 result.device = adb.get_emulator_device(adb_path=adb_path)
     73             else:
     74                 result.device = adb.get_device(result.serial, adb_path=adb_path)
     75         except (adb.DeviceNotFoundError, adb.NoUniqueDeviceError, RuntimeError):
     76             # Don't error out if we can't find a device.
     77             result.device = None
     78 
     79         return result
     80 
     81 
     82 def get_run_as_cmd(user, cmd):
     83     """Generate a run-as or su command depending on user."""
     84 
     85     if user is None:
     86         return cmd
     87     elif user == "root":
     88         return ["su", "0"] + cmd
     89     else:
     90         return ["run-as", user] + cmd
     91 
     92 
     93 def get_processes(device):
     94     """Return a dict from process name to list of running PIDs on the device."""
     95 
     96     # Some custom ROMs use busybox instead of toolbox for ps. Without -w,
     97     # busybox truncates the output, and very long package names like
     98     # com.exampleisverylongtoolongbyfar.plasma exceed the limit.
     99     #
    100     # Perform the check for this on the device to avoid an adb roundtrip
    101     # Some devices might not have readlink or which, so we need to handle
    102     # this as well.
    103 
    104     ps_script = """
    105         if [ ! -x /system/bin/readlink -o ! -x /system/bin/which ]; then
    106             ps;
    107         elif [ $(readlink $(which ps)) == "toolbox" ]; then
    108             ps;
    109         else
    110             ps -w;
    111         fi
    112     """
    113     ps_script = " ".join([line.strip() for line in ps_script.splitlines()])
    114 
    115     output, _ = device.shell([ps_script])
    116 
    117     processes = dict()
    118     output = output.replace("\r", "").splitlines()
    119     columns = output.pop(0).split()
    120     try:
    121         pid_column = columns.index("PID")
    122     except ValueError:
    123         pid_column = 1
    124     while output:
    125         columns = output.pop().split()
    126         process_name = columns[-1]
    127         pid = int(columns[pid_column])
    128         if process_name in processes:
    129             processes[process_name].append(pid)
    130         else:
    131             processes[process_name] = [pid]
    132 
    133     return processes
    134 
    135 
    136 def get_pids(device, process_name):
    137     processes = get_processes(device)
    138     return processes.get(process_name, [])
    139 
    140 
    141 def start_gdbserver(device, gdbserver_local_path, gdbserver_remote_path,
    142                     target_pid, run_cmd, debug_socket, port, user=None):
    143     """Start gdbserver in the background and forward necessary ports.
    144 
    145     Args:
    146         device: ADB device to start gdbserver on.
    147         gdbserver_local_path: Host path to push gdbserver from, can be None.
    148         gdbserver_remote_path: Device path to push gdbserver to.
    149         target_pid: PID of device process to attach to.
    150         run_cmd: Command to run on the device.
    151         debug_socket: Device path to place gdbserver unix domain socket.
    152         port: Host port to forward the debug_socket to.
    153         user: Device user to run gdbserver as.
    154 
    155     Returns:
    156         Popen handle to the `adb shell` process gdbserver was started with.
    157     """
    158 
    159     assert target_pid is None or run_cmd is None
    160 
    161     # Push gdbserver to the target.
    162     if gdbserver_local_path is not None:
    163         device.push(gdbserver_local_path, gdbserver_remote_path)
    164 
    165     # Run gdbserver.
    166     gdbserver_cmd = [gdbserver_remote_path, "--once",
    167                      "+{}".format(debug_socket)]
    168 
    169     if target_pid is not None:
    170         gdbserver_cmd += ["--attach", str(target_pid)]
    171     else:
    172         gdbserver_cmd += run_cmd
    173 
    174     device.forward("tcp:{}".format(port),
    175                    "localfilesystem:{}".format(debug_socket))
    176     atexit.register(lambda: device.forward_remove("tcp:{}".format(port)))
    177     gdbserver_cmd = get_run_as_cmd(user, gdbserver_cmd)
    178 
    179     # Use ppid so that the file path stays the same.
    180     gdbclient_output_path = os.path.join(tempfile.gettempdir(),
    181                                          "gdbclient-{}".format(os.getppid()))
    182     print "Redirecting gdbclient output to {}".format(gdbclient_output_path)
    183     gdbclient_output = file(gdbclient_output_path, 'w')
    184     return device.shell_popen(gdbserver_cmd, stdout=gdbclient_output,
    185                               stderr=gdbclient_output)
    186 
    187 
    188 def find_file(device, executable_path, sysroot, user=None):
    189     """Finds a device executable file.
    190 
    191     This function first attempts to find the local file which will
    192     contain debug symbols. If that fails, it will fall back to
    193     downloading the stripped file from the device.
    194 
    195     Args:
    196       device: the AndroidDevice object to use.
    197       executable_path: absolute path to the executable or symlink.
    198       sysroot: absolute path to the built symbol sysroot.
    199       user: if necessary, the user to download the file as.
    200 
    201     Returns:
    202       A tuple containing (<open file object>, <was found locally>).
    203 
    204     Raises:
    205       RuntimeError: could not find the executable binary.
    206       ValueError: |executable_path| is not absolute.
    207     """
    208     if not os.path.isabs(executable_path):
    209         raise ValueError("'{}' is not an absolute path".format(executable_path))
    210 
    211     def generate_files():
    212         """Yields (<file name>, <found locally>) tuples."""
    213         # First look locally to avoid shelling into the device if possible.
    214         # os.path.join() doesn't combine absolute paths, use + instead.
    215         yield (sysroot + executable_path, True)
    216 
    217         # Next check if the path is a symlink.
    218         try:
    219             target = device.shell(['readlink', '-e', '-n', executable_path])[0]
    220             yield (sysroot + target, True)
    221         except adb.ShellError:
    222             pass
    223 
    224         # Last, download the stripped executable from the device if necessary.
    225         file_name = "gdbclient-binary-{}".format(os.getppid())
    226         remote_temp_path = "/data/local/tmp/{}".format(file_name)
    227         local_path = os.path.join(tempfile.gettempdir(), file_name)
    228         cmd = get_run_as_cmd(user,
    229                              ["cat", executable_path, ">", remote_temp_path])
    230         try:
    231             device.shell(cmd)
    232         except adb.ShellError:
    233             raise RuntimeError("Failed to copy '{}' to temporary folder on "
    234                                "device".format(executable_path))
    235         device.pull(remote_temp_path, local_path)
    236         yield (local_path, False)
    237 
    238     for path, found_locally in generate_files():
    239         if os.path.isfile(path):
    240             return (open(path, "r"), found_locally)
    241     raise RuntimeError('Could not find executable {}'.format(executable_path))
    242 
    243 
    244 def find_binary(device, pid, sysroot, user=None):
    245     """Finds a device executable file corresponding to |pid|."""
    246     return find_file(device, "/proc/{}/exe".format(pid), sysroot, user)
    247 
    248 
    249 def get_binary_arch(binary_file):
    250     """Parse a binary's ELF header for arch."""
    251     try:
    252         binary_file.seek(0)
    253         binary = binary_file.read(0x14)
    254     except IOError:
    255         raise RuntimeError("failed to read binary file")
    256     ei_class = ord(binary[0x4]) # 1 = 32-bit, 2 = 64-bit
    257     ei_data = ord(binary[0x5]) # Endianness
    258 
    259     assert ei_class == 1 or ei_class == 2
    260     if ei_data != 1:
    261         raise RuntimeError("binary isn't little-endian?")
    262 
    263     e_machine = ord(binary[0x13]) << 8 | ord(binary[0x12])
    264     if e_machine == 0x28:
    265         assert ei_class == 1
    266         return "arm"
    267     elif e_machine == 0xB7:
    268         assert ei_class == 2
    269         return "arm64"
    270     elif e_machine == 0x03:
    271         assert ei_class == 1
    272         return "x86"
    273     elif e_machine == 0x3E:
    274         assert ei_class == 2
    275         return "x86_64"
    276     elif e_machine == 0x08:
    277         if ei_class == 1:
    278             return "mips"
    279         else:
    280             return "mips64"
    281     else:
    282         raise RuntimeError("unknown architecture: 0x{:x}".format(e_machine))
    283 
    284 
    285 def start_gdb(gdb_path, gdb_commands, gdb_flags=None):
    286     """Start gdb in the background and block until it finishes.
    287 
    288     Args:
    289         gdb_path: Path of the gdb binary.
    290         gdb_commands: Contents of GDB script to run.
    291         gdb_flags: List of flags to append to gdb command.
    292     """
    293 
    294     with tempfile.NamedTemporaryFile() as gdb_script:
    295         gdb_script.write(gdb_commands)
    296         gdb_script.flush()
    297         gdb_args = [gdb_path, "-x", gdb_script.name] + (gdb_flags or [])
    298         gdb_process = subprocess.Popen(gdb_args)
    299         while gdb_process.returncode is None:
    300             try:
    301                 gdb_process.communicate()
    302             except KeyboardInterrupt:
    303                 pass
    304 
    305