blob: 8af941eb11681ed654d1fc1cd8ce4265483affe9 [file] [log] [blame]
# 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.
"""Wrapper for running gdb.
This handles the fun details like running against the right sysroot, via
qemu, bind mounts, etc...
from __future__ import print_function
import argparse
import contextlib
import errno
import os
import sys
import tempfile
from chromite.lib import commandline
from chromite.lib import cros_build_lib
from chromite.lib import namespaces
from chromite.lib import osutils
from chromite.lib import retry_util
GDB = '/usr/bin/gdb'
class BoardSpecificGdb(object):
"""Framework for running gdb."""
_BIND_MOUNT_PATHS = ('dev', 'dev/pts', 'proc', 'mnt/host/source', 'sys')
def __init__(self, board, argv):
self.board = board
self.sysroot = cros_build_lib.GetSysroot(board=self.board)
self.prompt = '(%s-gdb) ' % self.board = False
self.run_as_root = False # May add an option to change this later.
self.args = argv
if not os.path.isdir(self.sysroot):
raise AssertionError('Sysroot does not exist: %s' % self.sysroot)
def removeSysrootPrefix(self, path):
"""Returns the given path with any sysroot prefix removed."""
# If the sysroot is /, then the paths are already normalized.
if self.sysroot != '/' and path.startswith(self.sysroot):
path = path.replace(self.sysroot, '', 1)
return path
def GetNonRootAccount():
"""Return details about the non-root account we want to use.
A tuple of (username, uid, gid, home).
return (
os.environ.get('SUDO_USER', 'nobody'),
int(os.environ.get('SUDO_UID', '65534')),
int(os.environ.get('SUDO_GID', '65534')),
# Should we find a better home?
def LockDb(db):
"""Lock an account database.
We use the same algorithm as shadow/user.eclass. This way we don't race
and corrupt things in parallel.
lock = '%s.lock' % db
_, tmplock = tempfile.mkstemp(prefix='%s.platform.' % lock)
# First try forever to grab the lock.
retry = lambda e: e.errno == errno.EEXIST
# Retry quickly at first, but slow down over time.
retry_util.GenericRetry(retry, 60,, tmplock, lock, sleep=0.1)
except Exception:
print('error: could not grab lock %s' % lock)
# Yield while holding the lock, but try to clean it no matter what.
yield lock
def SetupUser(self):
"""Propogate the user name<->id mapping from outside the chroot.
Some unittests use getpwnam($USER), as does bash. If the account
is not registered in the sysroot, they get back errors.
MAGIC_GECOS = 'Added by your friendly platform test helper; do not modify'
# This is kept in sync with what sdk_lib/ generates.
SDK_GECOS = 'ChromeOS Developer'
user, uid, gid, home = self.GetNonRootAccount()
if user == 'nobody':
passwd_db = os.path.join(self.sysroot, 'etc', 'passwd')
with self.LockDb(passwd_db):
data = osutils.ReadFile(passwd_db)
accts = data.splitlines()
for acct in accts:
passwd = acct.split(':')
if passwd[0] == user:
# Did the sdk make this account?
if passwd[4] == SDK_GECOS:
# Don't modify it (see below) since we didn't create it.
# Did we make this account?
if passwd[4] != MAGIC_GECOS:
raise RuntimeError('your passwd db (%s) has unmanaged acct %s' %
(passwd_db, user))
# Maybe we should see if it needs to be updated? Like if they
# changed UIDs? But we don't really check that elsewhere ...
acct = '%(name)s:x:%(uid)s:%(gid)s:%(gecos)s:%(homedir)s:%(shell)s' % {
'name': user,
'uid': uid,
'gid': gid,
'gecos': MAGIC_GECOS,
'homedir': home,
'shell': '/bin/bash',
with open(passwd_db, 'a') as f:
if data[-1] != '\n':
f.write('%s\n' % acct)
def run(self):
"""Runs the debugger in a proper environment (e.g. qemu)."""
for mount in self._BIND_MOUNT_PATHS:
path = os.path.join(self.sysroot, mount)
osutils.Mount('/' + mount, path, 'none', osutils.MS_BIND)
cmd = GDB
argv = self.args[:]
if argv:
argv[0] = self.removeSysrootPrefix(argv[0])
# Some programs expect to find data files via $CWD, so doing a chroot
# and dropping them into / would make them fail.
cwd = self.removeSysrootPrefix(os.getcwd())
print('chroot: %s' % self.sysroot)
print('cwd: %s' % cwd)
if argv:
print('cmd: {%s} %s' % (cmd, ' '.join(map(repr, argv))))
# The TERM the user is leveraging might not exist in the sysroot.
# Force a sane default that supports standard color sequences.
os.environ['TERM'] = 'ansi'
# Some progs want this like bash else they get super confused.
os.environ['PWD'] = cwd
if not self.run_as_root:
_, uid, gid, home = self.GetNonRootAccount()
os.environ['HOME'] = home
gdb_commands = (
'set sysroot /',
'set solib-absolute-prefix /',
'set solib-search-path /',
'set debug-file-directory /usr/lib/debug',
'set prompt %s' % self.prompt
args = [cmd] + ['--eval-command=%s' % x for x in gdb_commands] + self.args
sys.exit(os.execvp(cmd, args))
def _ReExecuteIfNeeded(argv):
"""Re-execute gdb as root.
We often need to do things as root, so make sure we're that. Like chroot
for proper library environment or do bind mounts.
Also unshare the mount namespace so as to ensure that doing bind mounts for
tests don't leak out to the normal chroot. Also unshare the UTS namespace
so changes to `hostname` do not impact the host.
if os.geteuid() != 0:
cmd = ['sudo', '-E', '--'] + argv
os.execvp(cmd[0], cmd)
namespaces.SimpleUnshare(net=True, pid=True)
def main(argv):
parser = commandline.ArgumentParser(description=__doc__)
parser.add_argument('--board', required=True,
help='board to debug for')
parser.add_argument('gdb_args', nargs=argparse.REMAINDER)
options = parser.parse_args(argv)
# Once we've finished sanity checking args, make sure we're root.
_ReExecuteIfNeeded([sys.argv[0]] + argv)
gdb = BoardSpecificGdb(options.board, options.gdb_args)