def Run(args, on_config_error): # Up-front check for faster error-checking. for path in args.inputs: if not path.endswith('.size') and not path.endswith('.sizediff'): on_config_error('All inputs must end with ".size" or ".sizediff"') size_infos = [] for path in args.inputs: if path.endswith('.sizediff'): size_infos.extend(archive.LoadAndPostProcessDeltaSizeInfo(path)) else: size_infos.append(archive.LoadAndPostProcessSizeInfo(path)) output_directory_finder = path_util.OutputDirectoryFinder( value=args.output_directory, any_path_within_output_directory=args.inputs[0]) linker_name = size_infos[-1].build_config.get( models.BUILD_CONFIG_LINKER_NAME) tool_prefix_finder = path_util.ToolPrefixFinder( value=args.tool_prefix, output_directory=output_directory_finder.Tentative(), linker_name=linker_name) session = _Session(size_infos, output_directory_finder, tool_prefix_finder) if args.query: logging.info('Running query from command-line.') session.Eval(args.query) else: logging.info('Entering interactive console.') session.GoInteractive()
def _ElfPathAndToolPrefixForSymbol(self, size_info, elf_path): tool_prefix = self._tool_prefix_finder.Tentative() orig_tool_prefix = size_info.metadata.get(models.METADATA_TOOL_PREFIX) if orig_tool_prefix: orig_tool_prefix = path_util.FromSrcRootRelative(orig_tool_prefix) if os.path.exists(path_util.GetObjDumpPath(orig_tool_prefix)): tool_prefix = orig_tool_prefix # TODO(agrieve): Would be even better to use objdump --info to check that # the toolchain is for the correct architecture. assert tool_prefix is not None, ( 'Could not determine --tool-prefix. Possible fixes include setting ' '--tool-prefix, or setting --output-directory') def build_id_matches(elf_path): found_build_id = archive.BuildIdFromElf(elf_path, tool_prefix) expected_build_id = size_info.metadata.get( models.METADATA_ELF_BUILD_ID) return found_build_id == expected_build_id filename = size_info.metadata.get(models.METADATA_ELF_FILENAME) paths_to_try = [] if elf_path: paths_to_try.append(elf_path) else: auto_output_directory_finders = [ path_util.OutputDirectoryFinder( any_path_within_output_directory=s.size_path) for s in self._size_infos ] + [self._output_directory_finder] for output_directory_finder in auto_output_directory_finders: output_dir = output_directory_finder.Tentative() if output_dir: # Local build: File is located in output directory. paths_to_try.append( os.path.normpath(os.path.join(output_dir, filename))) # Downloaded build: File is located beside .size file. paths_to_try.append( os.path.normpath( os.path.join(os.path.dirname(size_info.size_path), os.path.basename(filename)))) paths_to_try = [p for p in paths_to_try if os.path.exists(p)] for i, elf_path in enumerate(paths_to_try): if build_id_matches(elf_path): return elf_path, tool_prefix # Show an error only once all paths are tried. if i + 1 == len(paths_to_try): assert False, 'Build ID does not match for %s' % elf_path assert False, ( 'Could not locate ELF file. If binary was built locally, ensure ' '--output-directory is set. If output directory is unavailable, ' 'ensure {} is located beside {}, or pass its path explicitly using ' 'elf_path=').format(os.path.basename(filename), size_info.size_path)
def _DisassembleFunc(self, symbol, elf_path=None, use_pager=None, to_file=None): """Shows objdump disassembly for the given symbol. Args: symbol: Must be a .text symbol and not a SymbolGroup. elf_path: Path to the executable containing the symbol. Required only when auto-detection fails. """ assert not symbol.IsGroup() assert symbol.address and symbol.section_name == models.SECTION_TEXT assert not symbol.IsDelta(), ( 'Cannot disasseble a Diff\'ed symbol. Try ' 'passing .before_symbol or .after_symbol.') size_info = self._SizeInfoForSymbol(symbol) container = symbol.container tool_prefix = self._ToolPrefixForSymbol(size_info) elf_path = self._ElfPathForSymbol(size_info, container, tool_prefix, elf_path) # Always use Android NDK's objdump because llvm-objdump does not print # the target of jump instructions, which is really useful. output_directory_finder = self._output_directory_finder if not output_directory_finder.Tentative(): output_directory_finder = path_util.OutputDirectoryFinder( any_path_within_output_directory=elf_path) if output_directory_finder.Tentative(): tool_prefix = path_util.ToolPrefixFinder( output_directory=output_directory_finder.Finalized(), linker_name='ld').Finalized() args = [ path_util.GetObjDumpPath(tool_prefix), '--disassemble', '--source', '--line-numbers', '--demangle', '--start-address=0x%x' % symbol.address, '--stop-address=0x%x' % symbol.end_address, elf_path, ] # pylint: disable=unexpected-keyword-arg proc = subprocess.Popen(args, stdout=subprocess.PIPE, encoding='utf-8') lines = itertools.chain(('Showing disassembly for %r' % symbol, 'Command: %s' % ' '.join(args)), (l.rstrip() for l in proc.stdout)) _WriteToStream(lines, use_pager=use_pager, to_file=to_file) proc.kill()
def DeduceMainPaths(args, parser): """Computes main paths based on input, and deduces them if needed.""" apk_path = args.apk_file elf_path = args.elf_file map_path = args.map_file any_input = apk_path or elf_path or map_path if not any_input: parser.error('Must pass at least one of --apk-file, --elf-file, --map-file') output_directory_finder = path_util.OutputDirectoryFinder( value=args.output_directory, any_path_within_output_directory=any_input) apk_so_path = None if apk_path: with zipfile.ZipFile(apk_path) as z: lib_infos = [f for f in z.infolist() if f.filename.endswith('.so') and f.file_size > 0] assert lib_infos, 'APK has no .so files.' # TODO(agrieve): Add support for multiple .so files, and take into account # secondary architectures. apk_so_path = max(lib_infos, key=lambda x:x.file_size).filename logging.debug('Sub-apk path=%s', apk_so_path) if not elf_path and output_directory_finder.Tentative(): elf_path = os.path.join( output_directory_finder.Tentative(), 'lib.unstripped', os.path.basename(apk_so_path.replace('crazy.', ''))) logging.debug('Detected --elf-file=%s', elf_path) if map_path: if not map_path.endswith('.map') and not map_path.endswith('.map.gz'): parser.error('Expected --map-file to end with .map or .map.gz') else: map_path = elf_path + '.map' if not os.path.exists(map_path): map_path += '.gz' if not os.path.exists(map_path): parser.error('Could not find .map(.gz)? file. Ensure you have built with ' 'is_official_build=true, or use --map-file to point me a ' 'linker map file.') linker_name = _DetectLinkerName(map_path) tool_prefix_finder = path_util.ToolPrefixFinder( value=args.tool_prefix, output_directory_finder=output_directory_finder, linker_name=linker_name) tool_prefix = tool_prefix_finder.Finalized() output_directory = None if not args.no_source_paths: output_directory = output_directory_finder.Finalized() return (output_directory, tool_prefix, apk_path, apk_so_path, elf_path, map_path)
def _ElfPathForSymbol(self, size_info, container, tool_prefix, elf_path): def build_id_matches(elf_path): found_build_id = readelf.BuildIdFromElf(elf_path, tool_prefix) expected_build_id = container.metadata.get( models.METADATA_ELF_BUILD_ID) return found_build_id == expected_build_id filename = container.metadata.get(models.METADATA_ELF_FILENAME) paths_to_try = [] if elf_path: paths_to_try.append(elf_path) else: auto_output_directory_finders = [ path_util.OutputDirectoryFinder( any_path_within_output_directory=s.size_path) for s in self._size_infos ] + [self._output_directory_finder] for output_directory_finder in auto_output_directory_finders: output_dir = output_directory_finder.Tentative() if output_dir: # Local build: File is located in output directory. paths_to_try.append( os.path.normpath(os.path.join(output_dir, filename))) # Downloaded build: File is located beside .size file. paths_to_try.append( os.path.normpath( os.path.join(os.path.dirname(size_info.size_path), os.path.basename(filename)))) paths_to_try = [p for p in paths_to_try if os.path.exists(p)] for i, path in enumerate(paths_to_try): if build_id_matches(path): return path # Show an error only once all paths are tried. if i + 1 == len(paths_to_try): assert False, 'Build ID does not match for %s' % elf_path assert False, ( 'Could not locate ELF file. If binary was built locally, ensure ' '--output-directory is set. If output directory is unavailable, ' 'ensure {} is located beside {}, or pass its path explicitly using ' 'elf_path=').format(os.path.basename(filename), size_info.size_path) return None
def Run(args, parser): for path in args.inputs: if not path.endswith('.size'): parser.error('All inputs must end with ".size"') size_infos = [archive.LoadAndPostProcessSizeInfo(p) for p in args.inputs] output_directory_finder = path_util.OutputDirectoryFinder( value=args.output_directory, any_path_within_output_directory=args.inputs[0]) tool_prefix_finder = path_util.ToolPrefixFinder( value=args.tool_prefix, output_directory_finder=output_directory_finder) session = _Session(size_infos, output_directory_finder, tool_prefix_finder) if args.query: logging.info('Running query from command-line.') session.Eval(args.query) else: logging.info('Entering interactive console.') session.GoInteractive()
def Run(args, on_config_error): for path in args.inputs: if not path.endswith('.size'): on_config_error('All inputs must end with ".size"') size_infos = [archive.LoadAndPostProcessSizeInfo(p) for p in args.inputs] output_directory_finder = path_util.OutputDirectoryFinder( value=args.output_directory, any_path_within_output_directory=args.inputs[0]) linker_name = size_infos[-1].metadata.get(models.METADATA_LINKER_NAME) tool_prefix_finder = path_util.ToolPrefixFinder( value=args.tool_prefix, output_directory_finder=output_directory_finder, linker_name=linker_name) session = _Session(size_infos, output_directory_finder, tool_prefix_finder) if args.query: logging.info('Running query from command-line.') session.Eval(args.query) else: logging.info('Entering interactive console.') session.GoInteractive()
def Run(args, on_config_error): # Up-front check for faster error-checking. for path in args.inputs: if not path.endswith('.size') and not path.endswith('.sizediff'): on_config_error('All inputs must end with ".size" or ".sizediff"') size_infos = [] for path in args.inputs: if path.endswith('.sizediff'): size_infos.extend(archive.LoadAndPostProcessDeltaSizeInfo(path)) else: size_infos.append(archive.LoadAndPostProcessSizeInfo(path)) output_directory_finder = path_util.OutputDirectoryFinder( value=args.output_directory, any_path_within_output_directory=args.inputs[0]) linker_name = size_infos[-1].build_config.get( models.BUILD_CONFIG_LINKER_NAME) tool_prefix_finder = path_util.ToolPrefixFinder( value=args.tool_prefix, output_directory=output_directory_finder.Tentative(), linker_name=linker_name) session = _Session(size_infos, output_directory_finder, tool_prefix_finder) if args.query: logging.info('Running query from command-line.') session.Eval(args.query) else: logging.info('Entering interactive console.') session.GoInteractive() # Exit without running GC, which can save multiple seconds due the large # number of objects created. It meants atexit and __del__ calls are not # made, but this shouldn't matter for console. sys.stdout.flush() sys.stderr.flush() os._exit(0)
def _DisassembleFunc(self, symbol, elf_path=None, use_pager=None, to_file=None): """Shows objdump disassembly for the given symbol. Args: symbol: Must be a .text symbol and not a SymbolGroup. elf_path: Path to the executable containing the symbol. Required only when auto-detection fails. """ assert not symbol.IsGroup() assert symbol.address and symbol.section_name == models.SECTION_TEXT assert not symbol.IsDelta(), ( 'Cannot disasseble a Diff\'ed symbol. Try ' 'passing .before_symbol or .after_symbol.') size_info = self._SizeInfoForSymbol(symbol) container = symbol.container tool_prefix = self._ToolPrefixForSymbol(size_info) elf_path = self._ElfPathForSymbol(size_info, container, tool_prefix, elf_path) # Always use Android NDK's objdump because llvm-objdump does not print # the target of jump instructions, which is really useful. output_directory_finder = self._output_directory_finder if not output_directory_finder.Tentative(): output_directory_finder = path_util.OutputDirectoryFinder( any_path_within_output_directory=elf_path) if output_directory_finder.Tentative(): tool_prefix = path_util.ToolPrefixFinder( output_directory=output_directory_finder.Finalized(), linker_name='ld').Finalized() # Running objdump from an output directory means that objdump can # interleave source file lines in the disassembly. objdump_pwd = output_directory_finder.Finalized() else: # Output directory is not set, so we cannot load tool_prefix from # build_vars.json, nor resolve the output directory-relative path stored # size_info.metadata. is_android = next( filter(None, (m.get(models.METADATA_APK_FILENAME) for m in size_info.metadata)), None) arch = next( filter(None, (m.get(models.METADATA_ELF_ARCHITECTURE) for m in size_info.metadata)), None) # Hardcode path for arm32. if is_android and arch == 'arm': tool_prefix = path_util.ANDROID_ARM_NDK_TOOL_PREFIX # If we do not know/guess the output directory, run from any directory 2 # levels below src since it is better than a random cwd (because usually # source file paths are relative to an output directory two levels below # src and start with ../../). objdump_pwd = os.path.join(path_util.TOOLS_SRC_ROOT, 'tools', 'binary_size') args = [ os.path.relpath(path_util.GetObjDumpPath(tool_prefix), objdump_pwd), '--disassemble', '--source', '--line-numbers', '--demangle', '--start-address=0x%x' % symbol.address, '--stop-address=0x%x' % symbol.end_address, os.path.relpath(elf_path, objdump_pwd), ] # pylint: disable=unexpected-keyword-arg proc = subprocess.Popen(args, stdout=subprocess.PIPE, encoding='utf-8', cwd=objdump_pwd) lines = itertools.chain(('Showing disassembly for %r' % symbol, 'Command: %s' % ' '.join(args)), (l.rstrip() for l in proc.stdout)) _WriteToStream(lines, use_pager=use_pager, to_file=to_file) proc.kill()
def Run(args, parser): if not args.size_file.endswith('.size'): parser.error('size_file must end with .size') elf_path = args.elf_file map_path = args.map_file apk_path = args.apk_file pak_files = args.pak_file pak_info_file = args.pak_info_file any_input = apk_path or elf_path or map_path if not any_input: parser.error('Most pass at least one of --apk-file, --elf-file, --map-file') output_directory_finder = path_util.OutputDirectoryFinder( value=args.output_directory, any_path_within_output_directory=any_input) if apk_path: with zipfile.ZipFile(apk_path) as z: lib_infos = [f for f in z.infolist() if f.filename.endswith('.so') and f.file_size > 0] assert lib_infos, 'APK has no .so files.' # TODO(agrieve): Add support for multiple .so files, and take into account # secondary architectures. apk_so_path = max(lib_infos, key=lambda x:x.file_size).filename logging.debug('Sub-apk path=%s', apk_so_path) if not elf_path and output_directory_finder.Tentative(): elf_path = os.path.join( output_directory_finder.Tentative(), 'lib.unstripped', os.path.basename(apk_so_path.replace('crazy.', ''))) logging.debug('Detected --elf-file=%s', elf_path) if map_path: if not map_path.endswith('.map') and not map_path.endswith('.map.gz'): parser.error('Expected --map-file to end with .map or .map.gz') else: map_path = elf_path + '.map' if not os.path.exists(map_path): map_path += '.gz' if not os.path.exists(map_path): parser.error('Could not find .map(.gz)? file. Ensure you have built with ' 'is_official_build=true, or use --map-file to point me a ' 'linker map file.') linker_name = _DetectLinkerName(map_path) tool_prefix_finder = path_util.ToolPrefixFinder( value=args.tool_prefix, output_directory_finder=output_directory_finder, linker_name=linker_name) tool_prefix = tool_prefix_finder.Finalized() output_directory = None if not args.no_source_paths: output_directory = output_directory_finder.Finalized() metadata = CreateMetadata(map_path, elf_path, apk_path, tool_prefix, output_directory) if apk_path and elf_path: # Extraction takes around 1 second, so do it in parallel. apk_elf_result = concurrent.ForkAndCall( _ElfInfoFromApk, (apk_path, apk_so_path, tool_prefix)) section_sizes, raw_symbols = CreateSectionSizesAndSymbols( map_path, elf_path, tool_prefix, output_directory, track_string_literals=args.track_string_literals) if apk_path: AddApkInfo(section_sizes, raw_symbols, apk_path, output_directory, metadata, apk_elf_result) elif pak_files and pak_info_file: AddPakSymbolsFromFiles( section_sizes, raw_symbols, pak_files, pak_info_file) size_info = CreateSizeInfo( section_sizes, raw_symbols, metadata=metadata, normalize_names=False) if logging.getLogger().isEnabledFor(logging.INFO): for line in describe.DescribeSizeInfoCoverage(size_info): logging.info(line) logging.info('Recorded info for %d symbols', len(size_info.raw_symbols)) logging.info('Recording metadata: \n %s', '\n '.join(describe.DescribeMetadata(size_info.metadata))) logging.info('Saving result to %s', args.size_file) file_format.SaveSizeInfo(size_info, args.size_file) size_in_mb = os.path.getsize(args.size_file) / 1024.0 / 1024.0 logging.info('Done. File size is %.2fMiB.', size_in_mb)
def Run(args, parser): if not args.size_file.endswith('.size'): parser.error('size_file must end with .size') elf_path = args.elf_file map_path = args.map_file apk_path = args.apk_file any_input = apk_path or elf_path or map_path if not any_input: parser.error( 'Most pass at least one of --apk-file, --elf-file, --map-file') output_directory_finder = path_util.OutputDirectoryFinder( value=args.output_directory, any_path_within_output_directory=any_input) if apk_path: with zipfile.ZipFile(apk_path) as z: lib_infos = [ f for f in z.infolist() if f.filename.endswith('.so') and f.file_size > 0 ] assert lib_infos, 'APK has no .so files.' # TODO(agrieve): Add support for multiple .so files, and take into account # secondary architectures. apk_so_path = max(lib_infos, key=lambda x: x.file_size).filename logging.debug('Sub-apk path=%s', apk_so_path) if not elf_path and output_directory_finder.Tentative(): elf_path = os.path.join( output_directory_finder.Tentative(), 'lib.unstripped', os.path.basename(apk_so_path.replace('crazy.', ''))) logging.debug('Detected --elf-file=%s', elf_path) if map_path: if not map_path.endswith('.map') and not map_path.endswith('.map.gz'): parser.error('Expected --map-file to end with .map or .map.gz') else: map_path = elf_path + '.map' if not os.path.exists(map_path): map_path += '.gz' if not os.path.exists(map_path): parser.error( 'Could not find .map(.gz)? file. Ensure you have built with ' 'is_official_build=true, or use --map-file to point me a ' 'linker map file.') linker_name = _DetectLinkerName(map_path) tool_prefix_finder = path_util.ToolPrefixFinder( value=args.tool_prefix, output_directory_finder=output_directory_finder, linker_name=linker_name) tool_prefix = tool_prefix_finder.Finalized() output_directory = None if not args.no_source_paths: output_directory = output_directory_finder.Finalized() metadata = CreateMetadata(map_path, elf_path, apk_path, tool_prefix, output_directory) if apk_path and elf_path: # Extraction takes around 1 second, so do it in parallel. apk_elf_result = concurrent.ForkAndCall( _ElfInfoFromApk, (apk_path, apk_so_path, tool_prefix)) size_info = CreateSizeInfo( map_path, elf_path, tool_prefix, output_directory, normalize_names=False, track_string_literals=args.track_string_literals) if metadata: size_info.metadata = metadata if apk_path: logging.debug('Extracting section sizes from .so within .apk') unstripped_section_sizes = size_info.section_sizes apk_build_id, size_info.section_sizes = apk_elf_result.get() assert apk_build_id == metadata[models.METADATA_ELF_BUILD_ID], ( 'BuildID for %s within %s did not match the one at %s' % (apk_so_path, apk_path, elf_path)) packed_section_name = None architecture = metadata[models.METADATA_ELF_ARCHITECTURE] # Packing occurs enabled only arm32 & arm64. if architecture == 'arm': packed_section_name = '.rel.dyn' elif architecture == 'arm64': packed_section_name = '.rela.dyn' if packed_section_name: logging.debug('Recording size of unpacked relocations') if packed_section_name not in size_info.section_sizes: logging.warning('Packed section not present: %s', packed_section_name) else: size_info.section_sizes[ '%s (unpacked)' % packed_section_name] = ( unstripped_section_sizes.get(packed_section_name)) logging.info('Recording metadata: \n %s', '\n '.join(describe.DescribeMetadata(size_info.metadata))) logging.info('Saving result to %s', args.size_file) file_format.SaveSizeInfo(size_info, args.size_file) size_in_mb = os.path.getsize(args.size_file) / 1024.0 / 1024.0 logging.info('Done. File size is %.2fMiB.', size_in_mb)