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     # Gracefully handle [ or readlink being missing by always using `ps` if
    105     # readlink is missing. (API 18 has [, but not readlink).
    106 
    107     ps_script = """
    108         if $(ls /system/bin/readlink >/dev/null 2>&1); then
    109           if [ $(readlink /system/bin/ps) == "toolbox" ]; then
    110             ps;
    111           else
    112             ps -w;
    113           fi
    114         else
    115           ps;
    116         fi
    117     """
    118     ps_script = " ".join([line.strip() for line in ps_script.splitlines()])
    119 
    120     output, _ = device.shell([ps_script])
    121     return parse_ps_output(output)
    122 
    123 def parse_ps_output(output):
    124     processes = dict()
    125     output = adb.split_lines(output.replace("\r", ""))
    126     columns = output.pop(0).split()
    127     try:
    128         pid_column = columns.index("PID")
    129     except ValueError:
    130         pid_column = 1
    131     while output:
    132         columns = output.pop().split()
    133         process_name = columns[-1]
    134         pid = int(columns[pid_column])
    135         if process_name in processes:
    136             processes[process_name].append(pid)
    137         else:
    138             processes[process_name] = [pid]
    139 
    140     return processes
    141 
    142 
    143 def get_pids(device, process_name):
    144     processes = get_processes(device)
    145     return processes.get(process_name, [])
    146 
    147 
    148 def start_gdbserver(device, gdbserver_local_path, gdbserver_remote_path,
    149                     target_pid, run_cmd, debug_socket, port, user=None):
    150     """Start gdbserver in the background and forward necessary ports.
    151 
    152     Args:
    153         device: ADB device to start gdbserver on.
    154         gdbserver_local_path: Host path to push gdbserver from, can be None.
    155         gdbserver_remote_path: Device path to push gdbserver to.
    156         target_pid: PID of device process to attach to.
    157         run_cmd: Command to run on the device.
    158         debug_socket: Device path to place gdbserver unix domain socket.
    159         port: Host port to forward the debug_socket to.
    160         user: Device user to run gdbserver as.
    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     # Push gdbserver to the target.
    169     if gdbserver_local_path is not None:
    170         device.push(gdbserver_local_path, gdbserver_remote_path)
    171 
    172     # Run gdbserver.
    173     gdbserver_cmd = [gdbserver_remote_path, "--once",
    174                      "+{}".format(debug_socket)]
    175 
    176     if target_pid is not None:
    177         gdbserver_cmd += ["--attach", str(target_pid)]
    178     else:
    179         gdbserver_cmd += run_cmd
    180 
    181     device.forward("tcp:{}".format(port),
    182                    "localfilesystem:{}".format(debug_socket))
    183     atexit.register(lambda: device.forward_remove("tcp:{}".format(port)))
    184     gdbserver_cmd = get_run_as_cmd(user, gdbserver_cmd)
    185 
    186     gdbserver_output_path = os.path.join(tempfile.gettempdir(),
    187                                          "gdbclient.log")
    188     print("Redirecting gdbserver output to {}".format(gdbserver_output_path))
    189     gdbserver_output = file(gdbserver_output_path, 'w')
    190     return device.shell_popen(gdbserver_cmd, stdout=gdbserver_output,
    191                               stderr=gdbserver_output)
    192 
    193 
    194 def find_file(device, executable_path, sysroot, user=None):
    195     """Finds a device executable file.
    196 
    197     This function first attempts to find the local file which will
    198     contain debug symbols. If that fails, it will fall back to
    199     downloading the stripped file from the device.
    200 
    201     Args:
    202       device: the AndroidDevice object to use.
    203       executable_path: absolute path to the executable or symlink.
    204       sysroot: absolute path to the built symbol sysroot.
    205       user: if necessary, the user to download the file as.
    206 
    207     Returns:
    208       A tuple containing (<open file object>, <was found locally>).
    209 
    210     Raises:
    211       RuntimeError: could not find the executable binary.
    212       ValueError: |executable_path| is not absolute.
    213     """
    214     if not os.path.isabs(executable_path):
    215         raise ValueError("'{}' is not an absolute path".format(executable_path))
    216 
    217     def generate_files():
    218         """Yields (<file name>, <found locally>) tuples."""
    219         # First look locally to avoid shelling into the device if possible.
    220         # os.path.join() doesn't combine absolute paths, use + instead.
    221         yield (sysroot + executable_path, True)
    222 
    223         # Next check if the path is a symlink.
    224         try:
    225             target = device.shell(['readlink', '-e', '-n', executable_path])[0]
    226             yield (sysroot + target, True)
    227         except adb.ShellError:
    228             pass
    229 
    230         # Last, download the stripped executable from the device if necessary.
    231         file_name = "gdbclient-binary-{}".format(os.getppid())
    232         remote_temp_path = "/data/local/tmp/{}".format(file_name)
    233         local_path = os.path.join(tempfile.gettempdir(), file_name)
    234         cmd = get_run_as_cmd(user,
    235                              ["cat", executable_path, ">", remote_temp_path])
    236         try:
    237             device.shell(cmd)
    238         except adb.ShellError:
    239             raise RuntimeError("Failed to copy '{}' to temporary folder on "
    240                                "device".format(executable_path))
    241         device.pull(remote_temp_path, local_path)
    242         yield (local_path, False)
    243 
    244     for path, found_locally in generate_files():
    245         if os.path.isfile(path):
    246             return (open(path, "r"), found_locally)
    247     raise RuntimeError('Could not find executable {}'.format(executable_path))
    248 
    249 
    250 def find_binary(device, pid, sysroot, user=None):
    251     """Finds a device executable file corresponding to |pid|."""
    252     return find_file(device, "/proc/{}/exe".format(pid), sysroot, user)
    253 
    254 
    255 def get_binary_arch(binary_file):
    256     """Parse a binary's ELF header for arch."""
    257     try:
    258         binary_file.seek(0)
    259         binary = binary_file.read(0x14)
    260     except IOError:
    261         raise RuntimeError("failed to read binary file")
    262     ei_class = ord(binary[0x4]) # 1 = 32-bit, 2 = 64-bit
    263     ei_data = ord(binary[0x5]) # Endianness
    264 
    265     assert ei_class == 1 or ei_class == 2
    266     if ei_data != 1:
    267         raise RuntimeError("binary isn't little-endian?")
    268 
    269     e_machine = ord(binary[0x13]) << 8 | ord(binary[0x12])
    270     if e_machine == 0x28:
    271         assert ei_class == 1
    272         return "arm"
    273     elif e_machine == 0xB7:
    274         assert ei_class == 2
    275         return "arm64"
    276     elif e_machine == 0x03:
    277         assert ei_class == 1
    278         return "x86"
    279     elif e_machine == 0x3E:
    280         assert ei_class == 2
    281         return "x86_64"
    282     elif e_machine == 0x08:
    283         if ei_class == 1:
    284             return "mips"
    285         else:
    286             return "mips64"
    287     else:
    288         raise RuntimeError("unknown architecture: 0x{:x}".format(e_machine))
    289 
    290 
    291 def start_gdb(gdb_path, gdb_commands, gdb_flags=None):
    292     """Start gdb in the background and block until it finishes.
    293 
    294     Args:
    295         gdb_path: Path of the gdb binary.
    296         gdb_commands: Contents of GDB script to run.
    297         gdb_flags: List of flags to append to gdb command.
    298     """
    299 
    300     # Windows disallows opening the file while it's open for writing.
    301     gdb_script_fd, gdb_script_path = tempfile.mkstemp()
    302     os.write(gdb_script_fd, gdb_commands)
    303     os.close(gdb_script_fd)
    304     gdb_args = [gdb_path, "-x", gdb_script_path] + (gdb_flags or [])
    305 
    306     kwargs = {}
    307     if sys.platform.startswith("win"):
    308         kwargs["creationflags"] = subprocess.CREATE_NEW_CONSOLE
    309 
    310     gdb_process = subprocess.Popen(gdb_args, **kwargs)
    311     while gdb_process.returncode is None:
    312         try:
    313             gdb_process.communicate()
    314         except KeyboardInterrupt:
    315             pass
    316 
    317     os.unlink(gdb_script_path)
    318 
    319