blob: 0ab16419b7841e852338826384cc60ca3372e827 [file] [log] [blame]
#
# Copyright (C) 2015 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Helpers used by both gdbclient.py and ndk-gdb.py."""
import adb
import argparse
import atexit
import os
import subprocess
import tempfile
class ArgumentParser(argparse.ArgumentParser):
"""ArgumentParser subclass that provides adb device selection."""
class DeviceAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
if option_string is None:
raise RuntimeError("DeviceAction called without option_string")
elif option_string == "-a":
# Handled in parse_args
return
elif option_string == "-d":
namespace.device = adb.get_usb_device()
elif option_string == "-e":
namespace.device = adb.get_emulator_device()
elif option_string == "-s":
namespace.device = adb.get_device(values[0])
else:
raise RuntimeError("Unexpected flag {}".format(option_string))
def __init__(self):
super(ArgumentParser, self).__init__()
group = self.add_argument_group(title="device selection")
group = group.add_mutually_exclusive_group()
group.add_argument(
"-a", nargs=0, action=self.DeviceAction,
help="directs commands to all interfaces")
group.add_argument(
"-d", nargs=0, action=self.DeviceAction,
help="directs commands to the only connected USB device")
group.add_argument(
"-e", nargs=0, action=self.DeviceAction,
help="directs commands to the only connected emulator")
group.add_argument(
"-s", nargs=1, metavar="SERIAL", action=self.DeviceAction,
help="directs commands to device/emulator with the given serial")
def parse_args(self, args=None, namespace=None):
result = super(ArgumentParser, self).parse_args(args, namespace)
# Default to -a behavior if no flags are given.
if "device" not in result:
result.device = adb.get_device()
return result
def get_run_as_cmd(user, cmd):
"""Generate a run-as or su command depending on user."""
if user is None:
return cmd
elif user == "root":
return ["su", "0"] + cmd
else:
return ["run-as", user] + cmd
def get_processes(device):
"""Return a dict from process name to list of running PIDs on the device."""
# Some custom ROMs use busybox instead of toolbox for ps. Without -w,
# busybox truncates the output, and very long package names like
# com.exampleisverylongtoolongbyfar.plasma exceed the limit.
#
# Perform the check for this on the device to avoid an adb roundtrip
# Some devices might not have readlink or which, so we need to handle
# this as well.
ps_script = """
if [ ! -x /system/bin/readlink -o ! -x /system/bin/which ]; then
ps;
elif [ $(readlink $(which ps)) == "toolbox" ]; then
ps;
else
ps -w;
fi
"""
ps_script = " ".join([line.strip() for line in ps_script.splitlines()])
output, _ = device.shell([ps_script])
processes = dict()
output = output.replace("\r", "").splitlines()
columns = output.pop(0).split()
try:
pid_column = columns.index("PID")
except ValueError:
pid_column = 1
while output:
columns = output.pop().split()
process_name = columns[-1]
pid = int(columns[pid_column])
if process_name in processes:
processes[process_name].append(pid)
else:
processes[process_name] = [pid]
return processes
def start_gdbserver(device, gdbserver_local_path, gdbserver_remote_path,
target_pid, run_cmd, debug_socket, port, user=None):
"""Start gdbserver in the background and forward necessary ports.
Args:
device: ADB device to start gdbserver on.
gdbserver_local_path: Host path to push gdbserver from.
gdbserver_remote_path: Device path to push gdbserver to.
target_pid: PID of device process to attach to.
run_cmd: Command to run on the device.
debug_socket: Device path to place gdbserver unix domain socket.
port: Host port to forward the debug_socket to.
user: Device user to run gdbserver as.
Returns:
Popen handle to the `adb shell` process gdbserver was started with.
"""
assert target_pid is None or run_cmd is None
# Push gdbserver to the target.
device.push(gdbserver_local_path, gdbserver_remote_path)
# Run gdbserver.
gdbserver_cmd = [gdbserver_remote_path, "--once",
"+{}".format(debug_socket)]
if target_pid is not None:
gdbserver_cmd += ["--attach", str(target_pid)]
else:
gdbserver_cmd += run_cmd
device.forward("tcp:{}".format(port),
"localfilesystem:{}".format(debug_socket))
atexit.register(lambda: device.forward_remove("tcp:{}".format(port)))
gdbserver_cmd = get_run_as_cmd(user, gdbserver_cmd)
# Use ppid so that the file path stays the same.
gdbclient_output_path = os.path.join(tempfile.gettempdir(),
"gdbclient-{}".format(os.getppid()))
print "Redirecting gdbclient output to {}".format(gdbclient_output_path)
gdbclient_output = file(gdbclient_output_path, 'w')
return device.shell_popen(gdbserver_cmd, stdout=gdbclient_output,
stderr=gdbclient_output)
def pull_file(device, path, user=None):
"""Pull a file from a device as a user."""
file_name = "gdbclient-binary-{}".format(os.getppid())
remote_temp_path = "/data/local/tmp/{}".format(file_name)
local_temp_path = os.path.join(tempfile.gettempdir(), file_name)
cmd = get_run_as_cmd(user, ["cat", path, ">", remote_temp_path])
try:
device.shell(cmd)
except adb.ShellError:
raise RuntimeError("Failed to copy file to temporary folder on device")
device.pull(remote_temp_path, local_temp_path)
return open(local_temp_path, "r")
def pull_binary(device, pid, user=None):
"""Pull a running process's binary from a device as a user"""
return pull_file(device, "/proc/{}/exe".format(pid), user)
def get_binary_arch(binary_file):
"""Parse a binary's ELF header for arch."""
try:
binary_file.seek(0)
binary = binary_file.read(0x14)
except IOError:
raise RuntimeError("failed to read binary file")
ei_class = ord(binary[0x4]) # 1 = 32-bit, 2 = 64-bit
ei_data = ord(binary[0x5]) # Endianness
assert ei_class == 1 or ei_class == 2
if ei_data != 1:
raise RuntimeError("binary isn't little-endian?")
e_machine = ord(binary[0x13]) << 8 | ord(binary[0x12])
if e_machine == 0x28:
assert ei_class == 1
return "arm"
elif e_machine == 0xB7:
assert ei_class == 2
return "arm64"
elif e_machine == 0x03:
assert ei_class == 1
return "x86"
elif e_machine == 0x3E:
assert ei_class == 2
return "x86_64"
elif e_machine == 0x08:
if ei_class == 1:
return "mips"
else:
return "mips64"
else:
raise RuntimeError("unknown architecture: 0x{:x}".format(e_machine))
def start_gdb(gdb_path, gdb_commands):
"""Start gdb in the background and block until it finishes.
Args:
gdb_path: Path of the gdb binary.
gdb_commands: Contents of GDB script to run.
"""
with tempfile.NamedTemporaryFile() as gdb_script:
gdb_script.write(gdb_commands)
gdb_script.flush()
gdb_args = [gdb_path, "-x", gdb_script.name]
gdb_process = subprocess.Popen(gdb_args)
while gdb_process.returncode is None:
try:
gdb_process.communicate()
except KeyboardInterrupt:
pass