# Copyright (c) 2013 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. """Generate minidump symbols for use by the Crash server. Note: This should be run inside the chroot. This produces files in the breakpad format required by minidump_stackwalk and the crash server to dump stack information. Basically it scans all the split .debug files in /build/$BOARD/usr/lib/debug/ and converts them over using the `dump_syms` programs. Those plain text .sym files are then stored in /build/$BOARD/usr/lib/debug/breakpad/. If you want to actually upload things, see upload_symbols.py. """ from __future__ import print_function import collections import ctypes import multiprocessing import os import tempfile from chromite.lib import commandline from chromite.lib import cros_build_lib from chromite.lib import cros_logging as logging from chromite.lib import osutils from chromite.lib import parallel from chromite.lib import signals SymbolHeader = collections.namedtuple('SymbolHeader', ('cpu', 'id', 'name', 'os',)) def ReadSymsHeader(sym_file): """Parse the header of the symbol file The first line of the syms file will read like: MODULE Linux arm F4F6FA6CCBDEF455039C8DE869C8A2F40 blkid https://code.google.com/p/google-breakpad/wiki/SymbolFiles Args: sym_file: The symbol file to parse Returns: A SymbolHeader object Raises: ValueError if the first line of |sym_file| is invalid """ with cros_build_lib.Open(sym_file) as f: header = f.readline().split() if header[0] != 'MODULE' or len(header) != 5: raise ValueError('header of sym file is invalid') return SymbolHeader(os=header[1], cpu=header[2], id=header[3], name=header[4]) def GenerateBreakpadSymbol(elf_file, debug_file=None, breakpad_dir=None, board=None, strip_cfi=False, num_errors=None): """Generate the symbols for |elf_file| using |debug_file| Args: elf_file: The file to dump symbols for debug_file: Split debug file to use for symbol information breakpad_dir: The dir to store the output symbol file in board: If |breakpad_dir| is not specified, use |board| to find it strip_cfi: Do not generate CFI data num_errors: An object to update with the error count (needs a .value member) Returns: The number of errors that were encountered. """ if breakpad_dir is None: breakpad_dir = FindBreakpadDir(board) if num_errors is None: num_errors = ctypes.c_int() cmd_base = ['dump_syms'] if strip_cfi: cmd_base += ['-c'] # Some files will not be readable by non-root (e.g. set*id /bin/su). needs_sudo = not os.access(elf_file, os.R_OK) def _DumpIt(cmd_args): if needs_sudo: run_command = cros_build_lib.SudoRunCommand else: run_command = cros_build_lib.RunCommand return run_command( cmd_base + cmd_args, redirect_stderr=True, log_stdout_to_file=temp.name, error_code_ok=True, debug_level=logging.DEBUG) def _CrashCheck(ret, msg): if ret < 0: logging.PrintBuildbotStepWarnings() logging.warning('dump_syms crashed with %s; %s', signals.StrSignal(-ret), msg) osutils.SafeMakedirs(breakpad_dir) with tempfile.NamedTemporaryFile(dir=breakpad_dir, bufsize=0) as temp: if debug_file: # Try to dump the symbols using the debug file like normal. cmd_args = [elf_file, os.path.dirname(debug_file)] result = _DumpIt(cmd_args) if result.returncode: # Sometimes dump_syms can crash because there's too much info. # Try dumping and stripping the extended stuff out. At least # this way we'll get the extended symbols. http://crbug.com/266064 _CrashCheck(result.returncode, 'retrying w/out CFI') cmd_args = ['-c', '-r'] + cmd_args result = _DumpIt(cmd_args) _CrashCheck(result.returncode, 'retrying w/out debug') basic_dump = result.returncode else: basic_dump = True if basic_dump: # If that didn't work (no debug, or dump_syms still failed), try # dumping just the file itself directly. result = _DumpIt([elf_file]) if result.returncode: # A lot of files (like kernel files) contain no debug information, # do not consider such occurrences as errors. logging.PrintBuildbotStepWarnings() _CrashCheck(result.returncode, 'giving up entirely') if 'file contains no debugging information' in result.error: logging.warning('no symbols found for %s', elf_file) else: num_errors.value += 1 logging.error('dumping symbols for %s failed:\n%s', elf_file, result.error) return num_errors.value # Move the dumped symbol file to the right place: # /build/$BOARD/usr/lib/debug/breakpad///.sym header = ReadSymsHeader(temp) logging.info('Dumped %s as %s : %s', elf_file, header.name, header.id) sym_file = os.path.join(breakpad_dir, header.name, header.id, header.name + '.sym') osutils.SafeMakedirs(os.path.dirname(sym_file)) os.rename(temp.name, sym_file) os.chmod(sym_file, 0o644) temp.delete = False return num_errors.value def GenerateBreakpadSymbols(board, breakpad_dir=None, strip_cfi=False, generate_count=None, sysroot=None, num_processes=None, clean_breakpad=False, exclude_dirs=(), file_list=None): """Generate symbols for this board. If |file_list| is None, symbols are generated for all executables, otherwise only for the files included in |file_list|. TODO(build): This should be merged with buildbot_commands.GenerateBreakpadSymbols() once we rewrite cros_generate_breakpad_symbols in python. Args: board: The board whose symbols we wish to generate breakpad_dir: The full path to the breakpad directory where symbols live strip_cfi: Do not generate CFI data generate_count: If set, only generate this many symbols (meant for testing) sysroot: The root where to find the corresponding ELFs num_processes: Number of jobs to run in parallel clean_breakpad: Should we `rm -rf` the breakpad output dir first; note: we do not do any locking, so do not run more than one in parallel when True exclude_dirs: List of dirs (relative to |sysroot|) to not search file_list: Only generate symbols for files in this list. Each file must be a full path (including |sysroot| prefix). TODO(build): Support paths w/o |sysroot|. Returns: The number of errors that were encountered. """ if breakpad_dir is None: breakpad_dir = FindBreakpadDir(board) if sysroot is None: sysroot = cros_build_lib.GetSysroot(board=board) if clean_breakpad: logging.info('cleaning out %s first', breakpad_dir) osutils.RmDir(breakpad_dir, ignore_missing=True, sudo=True) # Make sure non-root can write out symbols as needed. osutils.SafeMakedirs(breakpad_dir, sudo=True) if not os.access(breakpad_dir, os.W_OK): cros_build_lib.SudoRunCommand(['chown', '-R', str(os.getuid()), breakpad_dir]) debug_dir = FindDebugDir(board) exclude_paths = [os.path.join(debug_dir, x) for x in exclude_dirs] if file_list is None: file_list = [] file_filter = dict.fromkeys([os.path.normpath(x) for x in file_list], False) logging.info('generating breakpad symbols using %s', debug_dir) # Let's locate all the debug_files and elfs first along with the debug file # sizes. This way we can start processing the largest files first in parallel # with the small ones. # If |file_list| was given, ignore all other files. targets = [] for root, dirs, files in os.walk(debug_dir): if root in exclude_paths: logging.info('Skipping excluded dir %s', root) del dirs[:] continue for debug_file in files: debug_file = os.path.join(root, debug_file) # Turn /build/$BOARD/usr/lib/debug/sbin/foo.debug into # /build/$BOARD/sbin/foo. elf_file = os.path.join(sysroot, debug_file[len(debug_dir) + 1:-6]) if file_filter: if elf_file in file_filter: file_filter[elf_file] = True elif debug_file in file_filter: file_filter[debug_file] = True else: continue # Filter out files based on common issues with the debug file. if not debug_file.endswith('.debug'): continue elif debug_file.endswith('.ko.debug'): logging.debug('Skipping kernel module %s', debug_file) continue elif os.path.islink(debug_file): # The build-id stuff is common enough to filter out by default. if '/.build-id/' in debug_file: msg = logging.debug else: msg = logging.warning msg('Skipping symbolic link %s', debug_file) continue # Filter out files based on common issues with the elf file. if not os.path.exists(elf_file): # Sometimes we filter out programs from /usr/bin but leave behind # the .debug file. logging.warning('Skipping missing %s', elf_file) continue targets.append((os.path.getsize(debug_file), elf_file, debug_file)) bg_errors = multiprocessing.Value('i') if file_filter: files_not_found = [x for x, found in file_filter.iteritems() if not found] bg_errors.value += len(files_not_found) if files_not_found: logging.error('Failed to find requested files: %s', files_not_found) # Now start generating symbols for the discovered elfs. with parallel.BackgroundTaskRunner(GenerateBreakpadSymbol, breakpad_dir=breakpad_dir, board=board, strip_cfi=strip_cfi, num_errors=bg_errors, processes=num_processes) as queue: for _, elf_file, debug_file in sorted(targets, reverse=True): if generate_count == 0: break queue.put([elf_file, debug_file]) if generate_count is not None: generate_count -= 1 if generate_count == 0: break return bg_errors.value def FindDebugDir(board): """Given a |board|, return the path to the split debug dir for it""" sysroot = cros_build_lib.GetSysroot(board=board) return os.path.join(sysroot, 'usr', 'lib', 'debug') def FindBreakpadDir(board): """Given a |board|, return the path to the breakpad dir for it""" return os.path.join(FindDebugDir(board), 'breakpad') def main(argv): parser = commandline.ArgumentParser(description=__doc__) parser.add_argument('--board', default=None, help='board to generate symbols for') parser.add_argument('--breakpad_root', type='path', default=None, help='root directory for breakpad symbols') parser.add_argument('--exclude-dir', type=str, action='append', default=[], help='directory (relative to |board| root) to not search') parser.add_argument('--generate-count', type=int, default=None, help='only generate # number of symbols') parser.add_argument('--noclean', dest='clean', action='store_false', default=True, help='do not clean out breakpad dir before running') parser.add_argument('--jobs', type=int, default=None, help='limit number of parallel jobs') parser.add_argument('--strip_cfi', action='store_true', default=False, help='do not generate CFI data (pass -c to dump_syms)') parser.add_argument('file_list', nargs='*', default=None, help='generate symbols for only these files ' '(e.g. /build/$BOARD/usr/bin/foo)') opts = parser.parse_args(argv) opts.Freeze() if opts.board is None: cros_build_lib.Die('--board is required') ret = GenerateBreakpadSymbols(opts.board, breakpad_dir=opts.breakpad_root, strip_cfi=opts.strip_cfi, generate_count=opts.generate_count, num_processes=opts.jobs, clean_breakpad=opts.clean, exclude_dirs=opts.exclude_dir, file_list=opts.file_list) if ret: logging.error('encountered %i problem(s)', ret) # Since exit(status) gets masked, clamp it to 1 so we don't inadvertently # return 0 in case we are a multiple of the mask. ret = 1 return ret