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