fwgdb.py: Add new script to connect to remote-GDB DUT in firmware
This patch adds the first part of a script that is supposed to make
firmware debugging and control via remote-GDB easier for the masses. The
first features include finding the right PTY to connect to via Servo,
automatically freezing (SIGSTOP) any existing processes (like minicom)
that attach to that terminal, and automatically rebooting the DUT into
GDB mode if necessary. A lot of emphasis is put on smart defaults and
auto-detection to ensure that the tool is as simple to use as possible
and will generally do the "right" thing (e.g. reboot the DUT by default,
unless there is already a GDB session active... this way both "let me
fire up my machine and do some quick test" and "oh, I just hit that rare
crash I was waiting for, let me connect my debugger to analyze it" can
be solved with the default invocation).
I am still planning to add much more to this. There will be
board-specific GDB initialization files to work around some existing
problems and custom GDB commands to make common debugging tasks easier.
This is more of a request for comments to understand design and style
issues in this approach early.
BUG=chrome-os-partner:18390
TEST=Flash /build/<board>/firmware/image.dev.bin to a DUT and connect it
via Servo. Run 'fwgdb' and enjoy not spending an extra 10 minutes trying
to figure out the right command line you need for a GDB invocation.
Change-Id: I6c51123b2be0fef7a7ce9403636733cb38f0c2d8
Signed-off-by: Julius Werner <jwerner@chromium.org>
Reviewed-on: https://chromium-review.googlesource.com/205561
diff --git a/scripts/fwgdb.py b/scripts/fwgdb.py
new file mode 100644
index 0000000..8c47ba6
--- /dev/null
+++ b/scripts/fwgdb.py
@@ -0,0 +1,266 @@
+# Copyright 2014 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Connect to a DUT in firmware via remote GDB, install custom GDB commands."""
+
+import errno
+import logging
+import os
+import re
+import signal
+import socket
+import time
+
+from chromite.cbuildbot import constants
+from chromite.lib import commandline
+from chromite.lib import cros_build_lib
+from chromite.lib import timeout_util
+
+# pylint: disable=W0622
+from chromite.lib.cros_build_lib import Error, Warning, Info, Debug
+
+# Need to do this before Servo import
+cros_build_lib.AssertInsideChroot()
+
+from servo import client
+from servo import multiservo
+
+_SRC_ROOT = os.path.join(constants.CHROOT_SOURCE_ROOT, 'src')
+_SRC_DC = os.path.join(_SRC_ROOT, 'platform/depthcharge')
+_SRC_VB = os.path.join(_SRC_ROOT, 'platform/vboot_reference')
+_SRC_LP = os.path.join(_SRC_ROOT, 'third_party/coreboot/payloads/libpayload')
+
+_PTRN_DEVMODE = 'Entering VbBootDeveloper()'
+_PTRN_GDB = 'Ready for GDB connection'
+_PTRN_BOARD = 'Starting depthcharge on ([a-z_]+)...'
+
+
+class TerminalFreezer(object):
+ """SIGSTOP all processes (and their parents) that have the TTY open."""
+
+ def __init__(self, tty):
+ self._tty = tty
+ self._processes = None
+
+ def __enter__(self):
+ lsof = cros_build_lib.RunCommand(['lsof', '-FR', self._tty],
+ capture_output=True, log_output=True, error_code_ok=True)
+ self._processes = re.findall(r'^(?:R|p)(\d+)$', lsof.output, re.MULTILINE)
+
+ # SIGSTOP parents before children
+ try:
+ for p in reversed(self._processes):
+ Info('Sending SIGSTOP to process %s!', p)
+ time.sleep(0.02)
+ os.kill(int(p), signal.SIGSTOP)
+ except OSError:
+ self.__exit__(None, None, None)
+ raise
+
+ def __exit__(self, _t, _v, _b):
+ # ...and wake 'em up again in reverse order
+ for p in self._processes:
+ Info('Sending SIGCONT to process %s!', p)
+ try:
+ os.kill(int(p), signal.SIGCONT)
+ except OSError as e:
+ Error("Error when trying to unfreeze process %s: %s" % (p, str(e)))
+
+
+def ParsePortage(board):
+ """Parse some data from portage files. equery takes ages in comparison."""
+ with open(os.path.join('/build', board, 'packages/Packages'), 'r') as f:
+ chost = None
+ use = None
+ for line in f:
+ if line[:7] == 'CHOST: ':
+ chost = line[7:].strip()
+ if line[:5] == 'USE: ':
+ use = line[5:].strip()
+ if chost and use:
+ return (chost, use)
+
+
+def ParseArgs(argv):
+ """Parse and validate command line arguments."""
+ parser = commandline.ArgumentParser(default_log_level='warning')
+
+ parser.add_argument('-b', '--board',
+ help='The board overlay name (auto-detect by default)')
+ parser.add_argument('-s', '--symbols',
+ help='Root directory or complete path to symbolized ELF '
+ '(defaults to /build/<BOARD>/firmware)')
+ parser.add_argument('-r', '--reboot', choices=['yes', 'no', 'auto'],
+ help='Reboot the DUT before connect (default: reboot if '
+ 'the remote and is unreachable)', default='auto')
+ parser.add_argument('-e', '--execute', action='append', default=[],
+ help='GDB command to run after connect (can be supplied '
+ 'multiple times)')
+
+ parser.add_argument('-n', '--servod-name', dest='name')
+ parser.add_argument('--servod-rcfile', default=multiservo.DEFAULT_RC_FILE)
+ parser.add_argument('--servod-server')
+ parser.add_argument('-p', '--servod-port', type=int, dest='port')
+ parser.add_argument('-t', '--tty',
+ help='TTY file to connect to (defaults to cpu_uart_pty)')
+
+ opts = parser.parse_args(argv)
+ multiservo.get_env_options(logging, opts)
+ if opts.name:
+ rc = multiservo.parse_rc(logging, opts.servod_rcfile)
+ if opts.name not in rc:
+ raise parser.error('%s not in %s' % (opts.name, opts.servod_rcfile))
+ if not opts.servod_server:
+ opts.servod_server = rc[opts.name]['sn']
+ if not opts.port:
+ opts.port = rc[opts.name].get('port', client.DEFAULT_PORT)
+ if not opts.board and 'board' in rc[opts.name]:
+ opts.board = rc[opts.name]['board']
+ Warning('Inferring board %s from %s... make sure this is correct!',
+ opts.board, opts.servod_rcfile)
+
+ if not opts.servod_server:
+ opts.servod_server = client.DEFAULT_HOST
+ if not opts.port:
+ opts.port = client.DEFAULT_PORT
+
+ return opts
+
+
+def FindSymbols(firmware_dir, board, use):
+ """Find the symbolized depthcharge ELF (may be supplied by -s flag)."""
+ if not firmware_dir:
+ firmware_dir = os.path.join(cros_build_lib.GetSysroot(board), 'firmware')
+ # Allow overriding the file directly just in case our detection screws up
+ if firmware_dir.endswith('.elf'):
+ return firmware_dir
+
+ if 'unified_depthcharge' in use:
+ basename = 'depthcharge.elf'
+ else:
+ basename = 'depthcharge.ro.elf'
+
+ path = os.path.join(firmware_dir, 'depthcharge_gdb', basename)
+ if not os.path.exists(path):
+ path = os.path.join(firmware_dir, basename)
+
+ if os.path.exists(path):
+ Warning('Auto-detected symbol file at %s... make sure that this matches '
+ 'the image on your DUT!', path)
+ return path
+
+ raise ValueError('Could not find %s symbol file!' % basename)
+
+
+# TODO(jwerner): Fine tune |wait| delay or maybe even make it configurable if
+# this causes problems due to load on the host. The callers where this is
+# critical should all have their own timeouts now, though, so it's questionable
+# whether the delay here is even needed at all anymore.
+def ReadAll(fd, wait=0.03):
+ """Read from |fd| until no more data has come for at least |wait| seconds."""
+ data = ''
+ try:
+ while True:
+ time.sleep(wait)
+ data += os.read(fd, 4096)
+ except OSError as e:
+ if e.errno == errno.EAGAIN:
+ Debug(data)
+ return data
+ raise
+
+
+def GdbChecksum(message):
+ """Calculate a remote-GDB style checksum."""
+ chksum = sum([ord(x) for x in message])
+ return ('%.2x' % chksum)[-2:]
+
+
+def TestConnection(fd):
+ """Return True iff there is a resposive GDB stub on the other end of 'fd'."""
+ cmd = 'vUnknownCommand'
+ for _ in xrange(3):
+ os.write(fd, '$%s#%s\n' % (cmd, GdbChecksum(cmd)))
+ reply = ReadAll(fd)
+ if '+$#00' in reply:
+ os.write(fd, '+')
+ Info('TestConnection: Could successfully connect to remote end.')
+ return True
+ Info('TestConnection: Remote end does not respond.')
+ return False
+
+
+def main(argv):
+ opts = ParseArgs(argv)
+ servo = client.ServoClient(host=opts.servod_server, port=opts.port)
+
+ if not opts.tty:
+ try:
+ opts.tty = servo.get('cpu_uart_pty')
+ except (client.ServoClientError, socket.error):
+ Error('Cannot auto-detect TTY file without servod. Use the --tty option.')
+ raise
+ with TerminalFreezer(opts.tty):
+ fd = os.open(opts.tty, os.O_RDWR | os.O_NONBLOCK)
+
+ data = ReadAll(fd)
+ if opts.reboot == 'auto':
+ if TestConnection(fd):
+ opts.reboot = 'no'
+ else:
+ opts.reboot = 'yes'
+
+ if opts.reboot == 'yes':
+ Info('Rebooting DUT...')
+ try:
+ servo.set('warm_reset', 'on')
+ time.sleep(0.1)
+ servo.set('warm_reset', 'off')
+ except (client.ServoClientError, socket.error):
+ Error('Cannot reboot without a Servo board. You have to boot into '
+ 'developer mode and press CTRL+G manually before running fwgdb.')
+ raise
+
+ # Throw away old data to avoid confusion from messages before the reboot
+ data = ''
+ with timeout_util.Timeout(10, 'Could not reboot into developer mode! '
+ '(Confirm that you have GBB_FLAG_FORCE_DEV_SWITCH_ON (0x8) set.)'):
+ while _PTRN_DEVMODE not in data:
+ data += ReadAll(fd)
+
+ # Send a CTRL+G
+ Info('Developer mode detected, pressing CTRL+G...')
+ os.write(fd, chr(ord('G') & 0x1f))
+
+ with timeout_util.Timeout(1, 'Could not enter GDB mode with CTRL+G! '
+ '(Confirm that you flashed an "image.dev.bin" image to this DUT.)'):
+ while _PTRN_GDB not in data:
+ data += ReadAll(fd)
+
+ if not opts.board:
+ matches = re.findall(_PTRN_BOARD, data)
+ if not matches:
+ raise ValueError('Could not auto-detect board! Please use -b option.')
+ opts.board = matches[-1]
+ Info('Auto-detected board as %s from DUT console output.', opts.board)
+
+ if not TestConnection(fd):
+ raise IOError('Could not connect to remote end! Confirm that your DUT is '
+ 'running in GDB mode on %s.' % opts.tty)
+
+ # Eat up leftover data or it will spill back to terminal
+ ReadAll(fd)
+ os.close(fd)
+
+ opts.execute.insert(0, 'target remote %s' % opts.tty)
+ ex_args = sum([['--ex', cmd] for cmd in opts.execute], [])
+
+ chost, use = ParsePortage(opts.board)
+ Info('Launching GDB...')
+ cros_build_lib.RunCommand([chost + '-gdb',
+ '--symbols', FindSymbols(opts.symbols, opts.board, use),
+ '--directory', _SRC_DC,
+ '--directory', _SRC_VB,
+ '--directory', _SRC_LP] + ex_args,
+ ignore_sigint=True, debug_level=logging.WARNING)