| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 2 # Copyright 2017 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 """Tool for analyzing binary size of executables using nm or linker map files. | |
| 7 | |
| 8 Map files can be created by passing "-Map Foo.map" to the linker. If a map file | |
| 9 is unavailable, this tool can also be pointed at an unstripped executable, but | |
| 10 the information does not seem to be as accurate in this case. | |
| 11 | |
| 12 Inspired by SymbolSort for Windows: | |
| 13 https://github.com/adrianstone55/SymbolSort | |
| 14 """ | |
| 15 | |
| 16 import argparse | |
| 17 import atexit | |
| 18 import code | |
| 19 import contextlib | |
| 20 import itertools | |
| 21 import logging | |
| 22 import os | |
| 23 import readline | |
| 24 import subprocess | |
| 25 import sys | |
| 26 | |
| 27 import describe | |
| 28 import file_format | |
| 29 import helpers | |
| 30 import map2size | |
| 31 import match_util | |
| 32 import models | |
| 33 import paths | |
| 34 | |
| 35 | |
| 36 # Number of lines before using less for Print(). | |
| 37 _THRESHOLD_FOR_PAGER = 30 | |
| 38 | |
| 39 | |
| 40 @contextlib.contextmanager | |
| 41 def _LessPipe(): | |
| 42 """Output to `less`. Yields a file object to write to.""" | |
| 43 try: | |
| 44 proc = subprocess.Popen(['less'], stdin=subprocess.PIPE, stdout=sys.stdout) | |
| 45 yield proc.stdin | |
| 46 proc.stdin.close() | |
| 47 proc.wait() | |
| 48 except IOError: | |
| 49 pass # Happens when less is quit before all data is written. | |
| 50 except KeyboardInterrupt: | |
| 51 pass # Assume used to break out of less. | |
| 52 | |
| 53 | |
| 54 def _WriteToStream(lines, use_pager=None, to_file=None): | |
| 55 if to_file: | |
| 56 use_pager = False | |
| 57 if use_pager is None and sys.stdout.isatty(): | |
| 58 # Does not take into account line-wrapping... Oh well. | |
| 59 first_lines = list(itertools.islice(lines, _THRESHOLD_FOR_PAGER)) | |
| 60 if len(first_lines) == _THRESHOLD_FOR_PAGER: | |
| 61 use_pager = True | |
| 62 lines = itertools.chain(first_lines, lines) | |
| 63 | |
| 64 if use_pager: | |
| 65 with _LessPipe() as stdin: | |
| 66 describe.WriteLines(lines, stdin.write) | |
| 67 elif to_file: | |
| 68 with open(to_file, 'w') as file_obj: | |
| 69 describe.WriteLines(lines, file_obj.write) | |
| 70 else: | |
| 71 describe.WriteLines(lines, sys.stdout.write) | |
| 72 | |
| 73 | |
| 74 class _Session(object): | |
| 75 _readline_initialized = False | |
| 76 | |
| 77 def __init__(self, size_infos, lazy_paths): | |
| 78 self._variables = { | |
| 79 'Print': self._PrintFunc, | |
| 80 'Diff': models.Diff, | |
| 81 'Disassemble': self._DisassembleFunc, | |
| 82 'ExpandRegex': match_util.ExpandRegexIdentifierPlaceholder, | |
| 83 'ShowExamples': self._ShowExamplesFunc, | |
| 84 } | |
| 85 self._lazy_paths = lazy_paths | |
| 86 self._size_infos = size_infos | |
| 87 | |
| 88 if len(size_infos) == 1: | |
| 89 self._variables['size_info'] = size_infos[0] | |
| 90 self._variables['symbols'] = size_infos[0].symbols | |
| 91 else: | |
| 92 for i, size_info in enumerate(size_infos): | |
| 93 self._variables['size_info%d' % (i + 1)] = size_info | |
| 94 self._variables['symbols%d' % (i + 1)] = size_info.symbols | |
| 95 | |
| 96 def _PrintFunc(self, obj, verbose=False, recursive=False, use_pager=None, | |
| 97 to_file=None): | |
| 98 """Prints out the given Symbol / SymbolGroup / SymbolDiff / SizeInfo. | |
| 99 | |
| 100 Args: | |
| 101 obj: The object to be printed. | |
| 102 verbose: Show more detailed output. | |
| 103 recursive: Print children of nested SymbolGroups. | |
| 104 use_pager: Pipe output through `less`. Ignored when |obj| is a Symbol. | |
| 105 default is to automatically pipe when output is long. | |
| 106 to_file: Rather than print to stdio, write to the given file. | |
| 107 """ | |
| 108 lines = describe.GenerateLines(obj, verbose=verbose, recursive=recursive) | |
| 109 _WriteToStream(lines, use_pager=use_pager, to_file=to_file) | |
| 110 | |
| 111 def _ElfPathForSymbol(self, symbol): | |
| 112 size_info = None | |
| 113 for size_info in self._size_infos: | |
| 114 if symbol in size_info.symbols: | |
| 115 break | |
| 116 else: | |
| 117 assert False, 'Symbol does not belong to a size_info.' | |
| 118 | |
| 119 filename = size_info.metadata.get(models.METADATA_ELF_FILENAME) | |
| 120 output_dir = self._lazy_paths.output_directory or '' | |
| 121 path = os.path.normpath(os.path.join(output_dir, filename)) | |
| 122 | |
| 123 found_build_id = map2size.BuildIdFromElf( | |
| 124 path, self._lazy_paths.tool_prefix) | |
| 125 expected_build_id = size_info.metadata.get(models.METADATA_ELF_BUILD_ID) | |
| 126 assert found_build_id == expected_build_id, ( | |
| 127 'Build ID does not match for %s' % path) | |
| 128 return path | |
| 129 | |
| 130 def _DisassembleFunc(self, symbol, elf_path=None, use_pager=None, | |
| 131 to_file=None): | |
| 132 """Shows objdump disassembly for the given symbol. | |
| 133 | |
| 134 Args: | |
| 135 symbol: Must be a .text symbol and not a SymbolGroup. | |
| 136 elf_path: Path to the executable containing the symbol. Required only | |
| 137 when auto-detection fails. | |
| 138 """ | |
| 139 assert symbol.address and symbol.section_name == '.text' | |
| 140 if not elf_path: | |
| 141 elf_path = self._ElfPathForSymbol(symbol) | |
| 142 tool_prefix = self._lazy_paths.tool_prefix | |
| 143 args = [tool_prefix + 'objdump', '--disassemble', '--source', | |
| 144 '--line-numbers', '--demangle', | |
| 145 '--start-address=0x%x' % symbol.address, | |
| 146 '--stop-address=0x%x' % symbol.end_address, elf_path] | |
| 147 proc = subprocess.Popen(args, stdout=subprocess.PIPE) | |
| 148 lines = itertools.chain(('Showing disassembly for %r' % symbol, | |
| 149 'Command: %s' % ' '.join(args)), | |
| 150 (l.rstrip() for l in proc.stdout)) | |
| 151 _WriteToStream(lines, use_pager=use_pager, to_file=to_file) | |
| 152 proc.kill() | |
| 153 | |
| 154 def _ShowExamplesFunc(self): | |
| 155 print '\n'.join([ | |
| 156 '# Show pydoc for main types:', | |
| 157 'import models', | |
| 158 'help(models)', | |
| 159 '', | |
| 160 '# Show all attributes of all symbols & per-section totals:', | |
| 161 'Print(size_info, verbose=True)', | |
| 162 '', | |
| 163 '# Show two levels of .text, grouped by first two subdirectories', | |
| 164 'text_syms = symbols.WhereInSection("t")', | |
| 165 'by_path = text_syms.GroupBySourcePath(depth=2)', | |
| 166 'Print(by_path.WhereBiggerThan(1024))', | |
| 167 '', | |
| 168 '# Show all non-vtable generated symbols', | |
| 169 'generated_syms = symbols.WhereIsGenerated()', | |
| 170 'Print(generated_syms.WhereNameMatches(r"vtable").Inverted())', | |
| 171 '', | |
| 172 '# Show all symbols that have "print" in their name or path, except', | |
| 173 '# those within components/.', | |
| 174 '# Note: Could have also used Inverted(), as above.', | |
| 175 '# Note: Use "help(ExpandRegex)" for more about what {{_print_}} does.', | |
| 176 'print_syms = symbols.WhereMatches(r"{{_print_}}")', | |
| 177 'Print(print_syms - print_syms.WherePathMatches(r"^components/"))', | |
| 178 '', | |
| 179 '# Diff two .size files and save result to a file:', | |
| 180 'Print(Diff(size_info1, size_info2), to_file="output.txt")', | |
| 181 '', | |
| 182 ]) | |
| 183 | |
| 184 def _CreateBanner(self): | |
| 185 symbol_info_keys = sorted(m for m in dir(models.SizeInfo) if m[0] != '_') | |
| 186 symbol_keys = sorted(m for m in dir(models.Symbol) if m[0] != '_') | |
| 187 symbol_group_keys = [m for m in dir(models.SymbolGroup) if m[0] != '_'] | |
| 188 symbol_diff_keys = sorted(m for m in dir(models.SymbolDiff) | |
| 189 if m[0] != '_' and m not in symbol_group_keys) | |
| 190 symbol_group_keys = sorted(m for m in symbol_group_keys | |
| 191 if m not in symbol_keys) | |
| 192 functions = sorted(k for k in self._variables if k[0].isupper()) | |
| 193 variables = sorted(k for k in self._variables if k[0].islower()) | |
| 194 return '\n'.join([ | |
| 195 '*' * 80, | |
| 196 'Entering interactive Python shell. Quick reference:', | |
| 197 '', | |
| 198 'SizeInfo: %s' % ', '.join(symbol_info_keys), | |
| 199 'Symbol: %s' % ', '.join(symbol_keys), | |
| 200 'SymbolGroup (extends Symbol): %s' % ', '.join(symbol_group_keys), | |
| 201 'SymbolDiff (extends SymbolGroup): %s' % ', '.join(symbol_diff_keys), | |
| 202 '', | |
| 203 'Functions: %s' % ', '.join('%s()' % f for f in functions), | |
| 204 'Variables: %s' % ', '.join(variables), | |
| 205 '*' * 80, | |
| 206 ]) | |
| 207 | |
| 208 @classmethod | |
| 209 def _InitReadline(cls): | |
| 210 if cls._readline_initialized: | |
| 211 return | |
| 212 cls._readline_initialized = True | |
| 213 # Without initializing readline, arrow keys don't even work! | |
| 214 readline.parse_and_bind('tab: complete') | |
| 215 history_file = os.path.join(os.path.expanduser('~'), | |
| 216 '.binary_size_query_history') | |
| 217 if os.path.exists(history_file): | |
| 218 readline.read_history_file(history_file) | |
| 219 atexit.register(lambda: readline.write_history_file(history_file)) | |
| 220 | |
| 221 def Eval(self, query): | |
| 222 eval_result = eval(query, self._variables) | |
| 223 if eval_result: | |
| 224 self._PrintFunc(eval_result) | |
| 225 | |
| 226 def GoInteractive(self): | |
| 227 _Session._InitReadline() | |
| 228 code.InteractiveConsole(self._variables).interact(self._CreateBanner()) | |
| 229 | |
| 230 | |
| 231 def main(argv): | |
| 232 parser = argparse.ArgumentParser() | |
| 233 parser.add_argument('inputs', nargs='+', | |
| 234 help='Input .size files to load. For a single file, ' | |
| 235 'it will be mapped to variables as: size_info & ' | |
| 236 'symbols (where symbols = size_info.symbols). For ' | |
| 237 'multiple inputs, the names will be size_info1, ' | |
| 238 'symbols1, etc.') | |
| 239 parser.add_argument('--query', | |
| 240 help='Print the result of the given snippet. Example: ' | |
| 241 'symbols.WhereInSection("d").' | |
| 242 'WhereBiggerThan(100)') | |
| 243 paths.AddOptions(parser) | |
| 244 args = helpers.AddCommonOptionsAndParseArgs(parser, argv) | |
| 245 | |
| 246 for path in args.inputs: | |
| 247 if not path.endswith('.size'): | |
| 248 parser.error('All inputs must end with ".size"') | |
| 249 | |
| 250 size_infos = [map2size.LoadAndPostProcessSizeInfo(p) for p in args.inputs] | |
| 251 lazy_paths = paths.LazyPaths(args=args, input_file=args.inputs[0]) | |
| 252 session = _Session(size_infos, lazy_paths) | |
| 253 | |
| 254 if args.query: | |
| 255 logging.info('Running query from command-line.') | |
| 256 session.Eval(args.query) | |
| 257 else: | |
| 258 logging.info('Entering interactive console.') | |
| 259 session.GoInteractive() | |
| 260 | |
| 261 | |
| 262 if __name__ == '__main__': | |
| 263 sys.exit(main(sys.argv)) | |
| OLD | NEW |