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