Home | History | Annotate | Download | only in find_runtime_symbols
      1 #!/usr/bin/env python
      2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 import hashlib
      7 import json
      8 import logging
      9 import optparse
     10 import os
     11 import re
     12 import shutil
     13 import subprocess
     14 import sys
     15 import tempfile
     16 
     17 
     18 BASE_PATH = os.path.dirname(os.path.abspath(__file__))
     19 REDUCE_DEBUGLINE_PATH = os.path.join(BASE_PATH, 'reduce_debugline.py')
     20 _TOOLS_LINUX_PATH = os.path.join(BASE_PATH, os.pardir, 'linux')
     21 sys.path.insert(0, _TOOLS_LINUX_PATH)
     22 
     23 
     24 from procfs import ProcMaps  # pylint: disable=F0401
     25 
     26 
     27 LOGGER = logging.getLogger('prepare_symbol_info')
     28 
     29 
     30 def _dump_command_result(command, output_dir_path, basename, suffix):
     31   handle_out, filename_out = tempfile.mkstemp(
     32       suffix=suffix, prefix=basename + '.', dir=output_dir_path)
     33   handle_err, filename_err = tempfile.mkstemp(
     34       suffix=suffix + '.err', prefix=basename + '.', dir=output_dir_path)
     35   error = False
     36   try:
     37     subprocess.check_call(
     38         command, stdout=handle_out, stderr=handle_err, shell=True)
     39   except (OSError, subprocess.CalledProcessError):
     40     error = True
     41   finally:
     42     os.close(handle_err)
     43     os.close(handle_out)
     44 
     45   if os.path.exists(filename_err):
     46     if LOGGER.getEffectiveLevel() <= logging.DEBUG:
     47       with open(filename_err, 'r') as f:
     48         for line in f:
     49           LOGGER.debug(line.rstrip())
     50     os.remove(filename_err)
     51 
     52   if os.path.exists(filename_out) and (
     53       os.path.getsize(filename_out) == 0 or error):
     54     os.remove(filename_out)
     55     return None
     56 
     57   if not os.path.exists(filename_out):
     58     return None
     59 
     60   return filename_out
     61 
     62 
     63 def prepare_symbol_info(maps_path,
     64                         output_dir_path=None,
     65                         alternative_dirs=None,
     66                         use_tempdir=False,
     67                         use_source_file_name=False):
     68   """Prepares (collects) symbol information files for find_runtime_symbols.
     69 
     70   1) If |output_dir_path| is specified, it tries collecting symbol information
     71   files in the given directory |output_dir_path|.
     72   1-a) If |output_dir_path| doesn't exist, create the directory and use it.
     73   1-b) If |output_dir_path| is an empty directory, use it.
     74   1-c) If |output_dir_path| is a directory which has 'files.json', assumes that
     75        files are already collected and just ignores it.
     76   1-d) Otherwise, depends on |use_tempdir|.
     77 
     78   2) If |output_dir_path| is not specified, it tries to create a new directory
     79   depending on 'maps_path'.
     80 
     81   If it cannot create a new directory, creates a temporary directory depending
     82   on |use_tempdir|.  If |use_tempdir| is False, returns None.
     83 
     84   Args:
     85       maps_path: A path to a file which contains '/proc/<pid>/maps'.
     86       alternative_dirs: A mapping from a directory '/path/on/target' where the
     87           target process runs to a directory '/path/on/host' where the script
     88           reads the binary.  Considered to be used for Android binaries.
     89       output_dir_path: A path to a directory where files are prepared.
     90       use_tempdir: If True, it creates a temporary directory when it cannot
     91           create a new directory.
     92       use_source_file_name: If True, it adds reduced result of 'readelf -wL'
     93           to find source file names.
     94 
     95   Returns:
     96       A pair of a path to the prepared directory and a boolean representing
     97       if it created a temporary directory or not.
     98   """
     99   alternative_dirs = alternative_dirs or {}
    100   if not output_dir_path:
    101     matched = re.match('^(.*)\.maps$', os.path.basename(maps_path))
    102     if matched:
    103       output_dir_path = matched.group(1) + '.pre'
    104   if not output_dir_path:
    105     matched = re.match('^/proc/(.*)/maps$', os.path.realpath(maps_path))
    106     if matched:
    107       output_dir_path = matched.group(1) + '.pre'
    108   if not output_dir_path:
    109     output_dir_path = os.path.basename(maps_path) + '.pre'
    110   # TODO(dmikurube): Find another candidate for output_dir_path.
    111 
    112   used_tempdir = False
    113   LOGGER.info('Data for profiling will be collected in "%s".' % output_dir_path)
    114   if os.path.exists(output_dir_path):
    115     if os.path.isdir(output_dir_path) and not os.listdir(output_dir_path):
    116       LOGGER.warn('Using an empty existing directory "%s".' % output_dir_path)
    117     else:
    118       LOGGER.warn('A file or a directory exists at "%s".' % output_dir_path)
    119       if os.path.exists(os.path.join(output_dir_path, 'files.json')):
    120         LOGGER.warn('Using the existing directory "%s".' % output_dir_path)
    121         return output_dir_path, used_tempdir
    122       else:
    123         if use_tempdir:
    124           output_dir_path = tempfile.mkdtemp()
    125           used_tempdir = True
    126           LOGGER.warn('Using a temporary directory "%s".' % output_dir_path)
    127         else:
    128           LOGGER.warn('The directory "%s" is not available.' % output_dir_path)
    129           return None, used_tempdir
    130   else:
    131     LOGGER.info('Creating a new directory "%s".' % output_dir_path)
    132     try:
    133       os.mkdir(output_dir_path)
    134     except OSError:
    135       LOGGER.warn('A directory "%s" cannot be created.' % output_dir_path)
    136       if use_tempdir:
    137         output_dir_path = tempfile.mkdtemp()
    138         used_tempdir = True
    139         LOGGER.warn('Using a temporary directory "%s".' % output_dir_path)
    140       else:
    141         LOGGER.warn('The directory "%s" is not available.' % output_dir_path)
    142         return None, used_tempdir
    143 
    144   shutil.copyfile(maps_path, os.path.join(output_dir_path, 'maps'))
    145 
    146   with open(maps_path, mode='r') as f:
    147     maps = ProcMaps.load_file(f)
    148 
    149   LOGGER.debug('Listing up symbols.')
    150   files = {}
    151   for entry in maps.iter(ProcMaps.executable):
    152     LOGGER.debug('  %016x-%016x +%06x %s' % (
    153         entry.begin, entry.end, entry.offset, entry.name))
    154     binary_path = entry.name
    155     for target_path, host_path in alternative_dirs.iteritems():
    156       if entry.name.startswith(target_path):
    157         binary_path = entry.name.replace(target_path, host_path, 1)
    158     if not (ProcMaps.EXECUTABLE_PATTERN.match(binary_path) or
    159             (os.path.isfile(binary_path) and os.access(binary_path, os.X_OK))):
    160       continue
    161     nm_filename = _dump_command_result(
    162         'nm -n --format bsd %s | c++filt' % binary_path,
    163         output_dir_path, os.path.basename(binary_path), '.nm')
    164     if not nm_filename:
    165       continue
    166     readelf_e_filename = _dump_command_result(
    167         'readelf -eW %s' % binary_path,
    168         output_dir_path, os.path.basename(binary_path), '.readelf-e')
    169     if not readelf_e_filename:
    170       continue
    171     readelf_debug_decodedline_file = None
    172     if use_source_file_name:
    173       readelf_debug_decodedline_file = _dump_command_result(
    174           'readelf -wL %s | %s' % (binary_path, REDUCE_DEBUGLINE_PATH),
    175           output_dir_path, os.path.basename(binary_path), '.readelf-wL')
    176 
    177     files[entry.name] = {}
    178     files[entry.name]['nm'] = {
    179         'file': os.path.basename(nm_filename),
    180         'format': 'bsd',
    181         'mangled': False}
    182     files[entry.name]['readelf-e'] = {
    183         'file': os.path.basename(readelf_e_filename)}
    184     if readelf_debug_decodedline_file:
    185       files[entry.name]['readelf-debug-decodedline-file'] = {
    186           'file': os.path.basename(readelf_debug_decodedline_file)}
    187 
    188     files[entry.name]['size'] = os.stat(binary_path).st_size
    189 
    190     with open(binary_path, 'rb') as entry_f:
    191       md5 = hashlib.md5()
    192       sha1 = hashlib.sha1()
    193       chunk = entry_f.read(1024 * 1024)
    194       while chunk:
    195         md5.update(chunk)
    196         sha1.update(chunk)
    197         chunk = entry_f.read(1024 * 1024)
    198       files[entry.name]['sha1'] = sha1.hexdigest()
    199       files[entry.name]['md5'] = md5.hexdigest()
    200 
    201   with open(os.path.join(output_dir_path, 'files.json'), 'w') as f:
    202     json.dump(files, f, indent=2, sort_keys=True)
    203 
    204   LOGGER.info('Collected symbol information at "%s".' % output_dir_path)
    205   return output_dir_path, used_tempdir
    206 
    207 
    208 def main():
    209   if not sys.platform.startswith('linux'):
    210     sys.stderr.write('This script work only on Linux.')
    211     return 1
    212 
    213   option_parser = optparse.OptionParser(
    214       '%s /path/to/maps [/path/to/output_data_dir/]' % sys.argv[0])
    215   option_parser.add_option('--alternative-dirs', dest='alternative_dirs',
    216                            metavar='/path/on/target@/path/on/host[:...]',
    217                            help='Read files in /path/on/host/ instead of '
    218                            'files in /path/on/target/.')
    219   option_parser.add_option('--verbose', dest='verbose', action='store_true',
    220                            help='Enable verbose mode.')
    221   options, args = option_parser.parse_args(sys.argv)
    222   alternative_dirs_dict = {}
    223   if options.alternative_dirs:
    224     for alternative_dir_pair in options.alternative_dirs.split(':'):
    225       target_path, host_path = alternative_dir_pair.split('@', 1)
    226       alternative_dirs_dict[target_path] = host_path
    227 
    228   LOGGER.setLevel(logging.DEBUG)
    229   handler = logging.StreamHandler()
    230   if options.verbose:
    231     handler.setLevel(logging.DEBUG)
    232   else:
    233     handler.setLevel(logging.INFO)
    234   formatter = logging.Formatter('%(message)s')
    235   handler.setFormatter(formatter)
    236   LOGGER.addHandler(handler)
    237 
    238   if len(args) < 2:
    239     option_parser.error('Argument error.')
    240     return 1
    241   elif len(args) == 2:
    242     result, _ = prepare_symbol_info(args[1],
    243                                     alternative_dirs=alternative_dirs_dict)
    244   else:
    245     result, _ = prepare_symbol_info(args[1], args[2],
    246                                     alternative_dirs=alternative_dirs_dict)
    247 
    248   return not result
    249 
    250 
    251 if __name__ == '__main__':
    252   sys.exit(main())
    253