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    if lldb:
193        gdbserver_output_path = os.path.join(tempfile.gettempdir(),
194                                             "lldb-client.log")
195        print("Redirecting lldb-server output to {}".format(gdbserver_output_path))
196    else:
197        gdbserver_output_path = os.path.join(tempfile.gettempdir(),
198                                             "gdbclient.log")
199        print("Redirecting gdbserver output to {}".format(gdbserver_output_path))
200    gdbserver_output = file(gdbserver_output_path, 'w')
201    return device.shell_popen(gdbserver_cmd, stdout=gdbserver_output,
202                              stderr=gdbserver_output)
203
204
205def get_uid(device):
206    """Gets the uid adbd runs as."""
207    line, _ = device.shell(["id", "-u"])
208    return int(line.strip())
209
210
211def forward_gdbserver_port(device, local, remote):
212    """Forwards local TCP port `port` to `remote` via `adb forward`."""
213    if get_uid(device) != 0:
214        WARNING = '\033[93m'
215        ENDC = '\033[0m'
216        print(WARNING +
217              "Port forwarding may not work because adbd is not running as root. " +
218              " Run `adb root` to fix." + ENDC)
219    device.forward("tcp:{}".format(local), remote)
220    atexit.register(lambda: device.forward_remove("tcp:{}".format(local)))
221
222
223def find_file(device, executable_path, sysroot, run_as_cmd=None):
224    """Finds a device executable file.
225
226    This function first attempts to find the local file which will
227    contain debug symbols. If that fails, it will fall back to
228    downloading the stripped file from the device.
229
230    Args:
231      device: the AndroidDevice object to use.
232      executable_path: absolute path to the executable or symlink.
233      sysroot: absolute path to the built symbol sysroot.
234      run_as_cmd: if necessary, run-as or su command to prepend
235
236    Returns:
237      A tuple containing (<open file object>, <was found locally>).
238
239    Raises:
240      RuntimeError: could not find the executable binary.
241      ValueError: |executable_path| is not absolute.
242    """
243    if not os.path.isabs(executable_path):
244        raise ValueError("'{}' is not an absolute path".format(executable_path))
245
246    def generate_files():
247        """Yields (<file name>, <found locally>) tuples."""
248        # First look locally to avoid shelling into the device if possible.
249        # os.path.join() doesn't combine absolute paths, use + instead.
250        yield (sysroot + executable_path, True)
251
252        # Next check if the path is a symlink.
253        try:
254            target = device.shell(['readlink', '-e', '-n', executable_path])[0]
255            yield (sysroot + target, True)
256        except adb.ShellError:
257            pass
258
259        # Last, download the stripped executable from the device if necessary.
260        file_name = "gdbclient-binary-{}".format(os.getppid())
261        remote_temp_path = "/data/local/tmp/{}".format(file_name)
262        local_path = os.path.join(tempfile.gettempdir(), file_name)
263
264        cmd = ["cat", executable_path, ">", remote_temp_path]
265        if run_as_cmd:
266            cmd = run_as_cmd + cmd
267
268        try:
269            device.shell(cmd)
270        except adb.ShellError:
271            raise RuntimeError("Failed to copy '{}' to temporary folder on "
272                               "device".format(executable_path))
273        device.pull(remote_temp_path, local_path)
274        yield (local_path, False)
275
276    for path, found_locally in generate_files():
277        if os.path.isfile(path):
278            return (open(path, "r"), found_locally)
279    raise RuntimeError('Could not find executable {}'.format(executable_path))
280
281def find_executable_path(device, executable_name, run_as_cmd=None):
282    """Find a device executable from its name
283
284    This function calls which on the device to retrieve the absolute path of
285    the executable.
286
287    Args:
288      device: the AndroidDevice object to use.
289      executable_name: the name of the executable to find.
290      run_as_cmd: if necessary, run-as or su command to prepend
291
292    Returns:
293      The absolute path of the executable.
294
295    Raises:
296      RuntimeError: could not find the executable.
297    """
298    cmd = ["which", executable_name]
299    if run_as_cmd:
300        cmd = run_as_cmd + cmd
301
302    try:
303        output, _ = device.shell(cmd)
304        return adb.split_lines(output)[0]
305    except adb.ShellError:
306        raise  RuntimeError("Could not find executable '{}' on "
307                            "device".format(executable_name))
308
309def find_binary(device, pid, sysroot, run_as_cmd=None):
310    """Finds a device executable file corresponding to |pid|."""
311    return find_file(device, "/proc/{}/exe".format(pid), sysroot, run_as_cmd)
312
313
314def get_binary_arch(binary_file):
315    """Parse a binary's ELF header for arch."""
316    try:
317        binary_file.seek(0)
318        binary = binary_file.read(0x14)
319    except IOError:
320        raise RuntimeError("failed to read binary file")
321    ei_class = ord(binary[0x4]) # 1 = 32-bit, 2 = 64-bit
322    ei_data = ord(binary[0x5]) # Endianness
323
324    assert ei_class == 1 or ei_class == 2
325    if ei_data != 1:
326        raise RuntimeError("binary isn't little-endian?")
327
328    e_machine = ord(binary[0x13]) << 8 | ord(binary[0x12])
329    if e_machine == 0x28:
330        assert ei_class == 1
331        return "arm"
332    elif e_machine == 0xB7:
333        assert ei_class == 2
334        return "arm64"
335    elif e_machine == 0x03:
336        assert ei_class == 1
337        return "x86"
338    elif e_machine == 0x3E:
339        assert ei_class == 2
340        return "x86_64"
341    elif e_machine == 0x08:
342        if ei_class == 1:
343            return "mips"
344        else:
345            return "mips64"
346    else:
347        raise RuntimeError("unknown architecture: 0x{:x}".format(e_machine))
348
349
350def get_binary_interp(binary_path, llvm_readobj_path):
351    args = [llvm_readobj_path, "--elf-output-style=GNU", "-l", binary_path]
352    output = subprocess.check_output(args, universal_newlines=True)
353    m = re.search(r"\[Requesting program interpreter: (.*?)\]\n", output)
354    if m is None:
355        return None
356    else:
357        return m.group(1)
358
359
360def start_gdb(gdb_path, gdb_commands, gdb_flags=None, lldb=False):
361    """Start gdb in the background and block until it finishes.
362
363    Args:
364        gdb_path: Path of the gdb binary.
365        gdb_commands: Contents of GDB script to run.
366        gdb_flags: List of flags to append to gdb command.
367    """
368
369    # Windows disallows opening the file while it's open for writing.
370    script_fd, script_path = tempfile.mkstemp()
371    os.write(script_fd, gdb_commands)
372    os.close(script_fd)
373    if lldb:
374        script_parameter = "--source"
375    else:
376        script_parameter = "-x"
377    gdb_args = [gdb_path, script_parameter, script_path] + (gdb_flags or [])
378
379    creationflags = 0
380    if sys.platform.startswith("win"):
381        creationflags = subprocess.CREATE_NEW_CONSOLE
382
383    gdb_process = subprocess.Popen(gdb_args, creationflags=creationflags)
384    while gdb_process.returncode is None:
385        try:
386            gdb_process.communicate()
387        except KeyboardInterrupt:
388            pass
389
390    os.unlink(script_path)
391