• 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 os
10import re
11import shutil
12import subprocess
13import sys
14import tempfile
15
16
17BASE_PATH = os.path.dirname(os.path.abspath(__file__))
18REDUCE_DEBUGLINE_PATH = os.path.join(BASE_PATH, 'reduce_debugline.py')
19_TOOLS_LINUX_PATH = os.path.join(BASE_PATH, os.pardir, 'linux')
20sys.path.insert(0, _TOOLS_LINUX_PATH)
21
22
23from procfs import ProcMaps  # pylint: disable=F0401
24
25
26LOGGER = logging.getLogger('prepare_symbol_info')
27
28
29def _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
62def 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
204def 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
230if __name__ == '__main__':
231  sys.exit(main())
232