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