• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
6import hashlib
7import json
8import logging
9import optparse
10import os
11import re
12import shutil
13import subprocess
14import sys
15import tempfile
16
17
18BASE_PATH = os.path.dirname(os.path.abspath(__file__))
19REDUCE_DEBUGLINE_PATH = os.path.join(BASE_PATH, 'reduce_debugline.py')
20_TOOLS_LINUX_PATH = os.path.join(BASE_PATH, os.pardir, 'linux')
21sys.path.insert(0, _TOOLS_LINUX_PATH)
22
23
24from procfs import ProcMaps  # pylint: disable=F0401
25
26
27LOGGER = logging.getLogger('prepare_symbol_info')
28
29
30def _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
63def 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
208def 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
251if __name__ == '__main__':
252  sys.exit(main())
253