blob: 41b9221ebd85dfde340fd61a1656c89507f6e24e [file] [log] [blame]
# Copyright 2014 The ChromiumOS Authors
# 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 glob
import logging
import os
import re
import socket
import time
from chromite.third_party.pyelftools.elftools.elf.elffile import ELFFile
from chromite.lib import build_target_lib
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import timeout_util
# Need to do this before Servo import
cros_build_lib.AssertInsideChroot()
# pylint: disable=import-error,wrong-import-position
from servo import client
from servo import servo_parsing
from servo import terminal_freezer
# pylint: enable=import-error,wrong-import-position
_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_GDB = b"Ready for GDB connection"
_PTRN_BOARD = (
b"Starting(?: read-only| read/write)? depthcharge on ([A-Za-z_]+)..."
)
def GetGdbForElf(elf):
"""Return the correct C compiler prefix for the target ELF file."""
with open(elf, "rb") as fp:
return {
"EM_386": "x86_64-cros-linux-gnu-gdb",
"EM_X86_64": "x86_64-cros-linux-gnu-gdb",
"EM_ARM": "armv7a-cros-linux-gnueabihf-gdb",
"EM_AARCH64": "aarch64-cros-linux-gnu-gdb",
}[ELFFile(fp).header.e_machine]
def ParseArgs(argv):
"""Parse and validate command line arguments."""
description = "Debug depthcharge using GDB"
parser = servo_parsing.ServodClientParser(description=description)
parser.add_argument(
"-b", "--board", help="The board overlay name (auto-detect by default)"
)
parser.add_argument(
"-c",
"--cgdb",
action="store_true",
help="Use cgdb curses interface rather than plain gdb",
)
parser.add_argument(
"-y",
"--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(
"-t", "--tty", help="TTY file to connect to (defaults to cpu_uart_pty)"
)
opts = parser.parse_args(argv)
return opts
def FindSymbols(firmware_dir, board):
"""Find the symbolized depthcharge ELF (may be supplied by -y flag)."""
# Allow overriding the file directly just in case our detection screws up.
if firmware_dir and firmware_dir.endswith(".elf"):
return firmware_dir
if not firmware_dir:
# Unified builds have the format
# /build/<board|family>/firmware/<build_target|model>/. The board in
# depthcharge corresponds to the build_target in unified builds. For
# this reason we need to glob all boards to find the correct
# build_target.
unified_build_dirs = glob.glob("/build/*/firmware/%s" % board)
if len(unified_build_dirs) == 1:
firmware_dir = unified_build_dirs[0]
elif len(unified_build_dirs) > 1:
raise ValueError(
"Multiple boards were found (%s). Use -y to specify manually"
% (", ".join(unified_build_dirs))
)
if not firmware_dir:
firmware_dir = os.path.join(
build_target_lib.get_default_sysroot_path(board), "firmware"
)
# Very old firmware you might still find on GoldenEye had dev.ro.elf.
basenames = ["dev.elf", "dev.ro.elf"]
for basename in basenames:
path = os.path.join(firmware_dir, "depthcharge", basename)
if not os.path.exists(path):
path = os.path.join(firmware_dir, basename)
if os.path.exists(path):
logging.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 depthcharge symbol file (dev.elf)! "
"(You can use -y to supply it manually.)"
)
# 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 data has come for at least |wait| seconds."""
data = []
try:
while True:
time.sleep(wait)
new_data = os.read(fd, 4096)
if not new_data:
break
data.append(new_data)
except OSError as e:
if e.errno != errno.EAGAIN:
raise
data = b"".join(data)
if data:
logging.debug(data.decode("ascii", errors="replace"))
return data
def GdbChecksum(message):
"""Calculate a remote-GDB style checksum."""
chksum = sum(message)
return (b"%.2x" % chksum)[-2:]
def TestConnection(fd):
"""Return True if there's a responsive GDB stub on the other end of |fd|."""
cmd = b"vUnknownCommand"
for _ in range(3):
os.write(fd, b"$%s#%s\n" % (cmd, GdbChecksum(cmd)))
reply = ReadAll(fd)
if b"+$#00" in reply:
os.write(fd, b"+")
logging.info(
"TestConnection: Could successfully connect to remote end."
)
return True
logging.info("TestConnection: Remote end does not respond.")
return False
def main(argv) -> None:
opts = ParseArgs(argv)
servo = client.ServoClient(host=opts.host, port=opts.port)
if not opts.tty:
try:
opts.tty = servo.get("cpu_uart_pty")
except (client.ServoClientError, socket.error):
logging.error(
"Cannot auto-detect TTY file without servod. Use the --tty "
"option."
)
raise
with terminal_freezer.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":
logging.info("Rebooting DUT...")
try:
servo.set("warm_reset", "on")
time.sleep(0.1)
servo.set("warm_reset", "off")
except (client.ServoClientError, socket.error):
logging.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 = b""
msg = "Could not reboot into depthcharge!"
with timeout_util.Timeout(10, msg):
while not re.search(_PTRN_BOARD, data):
data += ReadAll(fd)
msg = (
"Could not enter GDB mode with CTRL+G! "
'(Confirm that you flashed an "image.dev.bin" image to this '
"DUT, and that you have GBB_FLAG_FORCE_DEV_SWITCH_ON (0x8) "
"set.)"
)
with timeout_util.Timeout(5, msg):
while not re.search(_PTRN_GDB, data):
# Some delay to avoid spamming the console too hard while
# not being long enough to cause a user-visible slowdown.
time.sleep(0.5)
# Send a CTRL+G to tell depthcharge to trap into GDB.
logging.debug("[Ctrl+G]")
os.write(fd, b"\x07")
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].decode().lower()
logging.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], [])
elf = FindSymbols(opts.symbols, opts.board)
gdb_cmd = GetGdbForElf(elf)
gdb_args = [
"--symbols",
elf,
"--directory",
_SRC_DC,
"--directory",
_SRC_VB,
"--directory",
_SRC_LP,
] + ex_args
if opts.cgdb:
full_cmd = ["cgdb", "-d", gdb_cmd, "--"] + gdb_args
else:
full_cmd = [gdb_cmd] + gdb_args
logging.info("Launching GDB...")
cros_build_lib.run(
full_cmd, ignore_sigint=True, debug_level=logging.WARNING
)