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