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