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