| // Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:async'; | |
| import 'dart:convert'; | |
| import 'dart:io' as io; | |
| import 'package:args/args.dart' show ArgParser, ArgResults; | |
| import 'package:native_stack_traces/native_stack_traces.dart'; | |
| import 'package:native_stack_traces/src/macho.dart' show CpuType; | |
| import 'package:path/path.dart' as path; | |
| ArgParser _createBaseDebugParser(ArgParser parser) => parser | |
| ..addOption('debug', | |
| abbr: 'd', | |
| help: 'Filename containing debugging information (REQUIRED)', | |
| valueHelp: 'FILE') | |
| ..addFlag('verbose', | |
| abbr: 'v', | |
| negatable: false, | |
| help: 'Translate all frames, not just user or library code frames'); | |
| final ArgParser _dumpParser = ArgParser(allowTrailingOptions: true) | |
| ..addOption('output', | |
| abbr: 'o', help: 'Filename for generated output', valueHelp: 'FILE'); | |
| final ArgParser _translateParser = | |
| _createBaseDebugParser(ArgParser(allowTrailingOptions: true)) | |
| ..addMultiOption('unit_debug', | |
| abbr: 'u', | |
| help: 'filename containing debugging information' | |
| ' for a deferred loading unit', | |
| valueHelp: 'FILE') | |
| ..addMultiOption('unit_id_debug', | |
| help: 'ID and filename containing debugging information' | |
| ' for a deferred loading unit', | |
| valueHelp: 'N=FILE') | |
| ..addOption('input', | |
| abbr: 'i', help: 'Filename for processed input', valueHelp: 'FILE') | |
| ..addOption('output', | |
| abbr: 'o', help: 'Filename for generated output', valueHelp: 'FILE'); | |
| final ArgParser _findParser = | |
| _createBaseDebugParser(ArgParser(allowTrailingOptions: true)) | |
| ..addMultiOption('location', | |
| abbr: 'l', help: 'PC address to find', valueHelp: 'PC') | |
| ..addFlag('force_hexadecimal', | |
| abbr: 'x', | |
| negatable: false, | |
| help: 'Always parse integers as hexadecimal') | |
| ..addOption('architecture', | |
| abbr: 'a', | |
| help: 'Architecture on which the program is run', | |
| allowed: CpuType.values.map((v) => v.dartName), | |
| valueHelp: 'ARCH') | |
| ..addOption('vm_start', | |
| help: 'Absolute address for start of VM instructions', | |
| valueHelp: 'PC') | |
| ..addOption('isolate_start', | |
| help: 'Absolute address for start of isolate instructions', | |
| valueHelp: 'PC'); | |
| final ArgParser _helpParser = ArgParser(allowTrailingOptions: true); | |
| final ArgParser _argParser = ArgParser(allowTrailingOptions: true) | |
| ..addCommand('dump', _dumpParser) | |
| ..addCommand('help', _helpParser) | |
| ..addCommand('find', _findParser) | |
| ..addCommand('translate', _translateParser) | |
| ..addFlag('help', | |
| abbr: 'h', | |
| negatable: false, | |
| help: 'Print usage information for this or a particular subcommand'); | |
| final String _mainUsage = ''' | |
| Usage: decode <command> [options] ... | |
| Commands: | |
| ${_argParser.commands.keys.join("\n")} | |
| Options shared by all commands: | |
| ${_argParser.usage}'''; | |
| final String _helpUsage = ''' | |
| Usage: decode help [<command>] | |
| Returns usage for the decode utility or a particular command. | |
| Commands: | |
| ${_argParser.commands.keys.join("\n")}'''; | |
| final String _translateUsage = ''' | |
| Usage: decode translate [options] | |
| The translate command takes text that includes non-symbolic stack traces | |
| generated by the VM when executing a snapshot compiled with the | |
| --dwarf-stack-traces flag. It outputs almost the same text, but with any | |
| non-symbolic stack traces converted to symbolic stack traces that contain | |
| function names, file names, and line numbers. | |
| If there are deferred loading units and their loading unit ids are known, then | |
| the debugging information for the deferred loading units can be specified | |
| using the --unit_id_debug command line option. E.g., if the debugging | |
| information for loading unit 5 is in debug_5.so and the information for | |
| loading unit 6 in debug_6.so, then the following command line arguments can | |
| be used: | |
| --unit_id_debug 5=debug_5.so --unit_id_debug 6=debug_6.so | |
| or | |
| --unit_id_debug 5=debug_5.so,6=debug_6.so | |
| If the loading unit ids are not known, but the build IDs in the debugging | |
| information match those of the deferred units, then the --unit_debug command | |
| line option (which can be abbreviated as -u) can be used: | |
| -u debug_5.so -u debug_6.so | |
| or | |
| -u debug_5.so,debug_6.so | |
| Options shared by all commands: | |
| ${_argParser.usage} | |
| Options specific to the translate command: | |
| ${_translateParser.usage}'''; | |
| final String _findUsage = ''' | |
| Usage: decode find [options] <PC> ... | |
| The find command looks up program counter (PC) addresses, either given as | |
| arguments on the command line or via the -l/--location option. For each | |
| successful PC lookup, it outputs the call information in one of two formats: | |
| - If the location corresponds to a call site in Dart source code, the call | |
| information includes the file, function, and line number information. | |
| - If it corresponds to a Dart VM stub, the call information includes the dynamic | |
| symbol name for the instructions payload and an offset into that payload. | |
| The -l option may be provided multiple times, or a single use of the -l option | |
| may be given multiple arguments separated by commas. | |
| PC addresses can be provided in one of two formats: | |
| - An integer, e.g. 0x2a3f or 15049 | |
| - A static symbol in the VM snapshot plus an integer offset, e.g., | |
| _kDartIsolateSnapshotInstructions+1523 or _kDartVMSnapshotInstructions+0x403f | |
| Integers without an "0x" prefix that do not includes hexadecimal digits are | |
| assumed to be decimal unless the -x/--force_hexadecimal flag is used. | |
| By default, integer PC addresses are assumed to be virtual addresses valid for | |
| the given debugging information. Otherwise, use both the --vm_start and | |
| --isolate_start arguments to provide the appropriate starting addresses of the | |
| VM and isolate instructions sections. | |
| Options shared by all commands: | |
| ${_argParser.usage} | |
| Options specific to the find command: | |
| ${_findParser.usage}'''; | |
| final String _dumpUsage = ''' | |
| Usage: decode dump [options] <snapshot> | |
| The dump command dumps the DWARF information in the given snapshot to either | |
| standard output or a given output file. | |
| Options specific to the dump command: | |
| ${_dumpParser.usage}'''; | |
| final _usages = <String?, String>{ | |
| null: _mainUsage, | |
| '': _mainUsage, | |
| 'help': _helpUsage, | |
| 'translate': _translateUsage, | |
| 'find': _findUsage, | |
| 'dump': _dumpUsage, | |
| }; | |
| const int _badUsageExitCode = 1; | |
| void errorWithUsage(String message, {String? command}) { | |
| print('Error: $message.\n'); | |
| print(_usages[command]); | |
| io.exitCode = _badUsageExitCode; | |
| } | |
| void help(ArgResults options) { | |
| void usageError(String message) => errorWithUsage(message, command: 'help'); | |
| switch (options.rest.length) { | |
| case 0: | |
| return print(_usages['help']); | |
| case 1: | |
| { | |
| final usage = _usages[options.rest.first]; | |
| if (usage != null) return print(usage); | |
| return usageError('invalid command ${options.rest.first}'); | |
| } | |
| default: | |
| return usageError('too many arguments'); | |
| } | |
| } | |
| Dwarf? _loadFromFile(String? original, Function(String) usageError) { | |
| if (original == null) { | |
| usageError('must provide -d/--debug'); | |
| return null; | |
| } | |
| final filename = path.canonicalize(path.normalize(original)); | |
| try { | |
| final dwarf = Dwarf.fromFile(filename); | |
| if (dwarf == null) { | |
| usageError('file "$original" does not contain debugging information'); | |
| } | |
| return dwarf; | |
| } on io.FileSystemException { | |
| usageError('debug file "$original" does not exist'); | |
| return null; | |
| } | |
| } | |
| void find(ArgResults options) { | |
| final bool verbose = options['verbose']; | |
| final bool forceHexadecimal = options['force_hexadecimal']; | |
| void usageError(String message) => errorWithUsage(message, command: 'find'); | |
| int? tryParseIntAddress(String s) { | |
| if (!forceHexadecimal && !s.startsWith('0x')) { | |
| final decimal = int.tryParse(s); | |
| if (decimal != null) return decimal; | |
| } | |
| return int.tryParse(s.startsWith('0x') ? s.substring(2) : s, radix: 16); | |
| } | |
| PCOffset? convertAddress(StackTraceHeader header, String s) { | |
| final parsedOffset = | |
| tryParseSymbolOffset(s, forceHexadecimal: forceHexadecimal); | |
| if (parsedOffset != null) return parsedOffset; | |
| final address = tryParseIntAddress(s); | |
| if (address != null) return header.offsetOf(address); | |
| return null; | |
| } | |
| final dwarf = _loadFromFile(options['debug'], usageError); | |
| if (dwarf == null) return; | |
| if ((options['vm_start'] == null) != (options['isolate_start'] == null)) { | |
| return usageError('need both VM start and isolate start'); | |
| } | |
| var vmStart = dwarf.vmStartAddress(); | |
| if (options['vm_start'] != null) { | |
| final address = tryParseIntAddress(options['vm_start']); | |
| if (address == null) { | |
| return usageError('could not parse VM start address ' | |
| '${options['vm_start']}'); | |
| } | |
| vmStart = address; | |
| } | |
| var isolateStart = dwarf.isolateStartAddress(); | |
| if (options['isolate_start'] != null) { | |
| final address = tryParseIntAddress(options['isolate_start']); | |
| if (address == null) { | |
| return usageError('could not parse isolate start address ' | |
| '${options['isolate_start']}'); | |
| } | |
| isolateStart = address; | |
| } | |
| if (isolateStart == null) { | |
| return usageError('no isolate start address found, one must be specified ' | |
| 'with --isolate_start'); | |
| } | |
| final arch = options['architecture']; | |
| final header = | |
| StackTraceHeader.fromStarts(isolateStart, vmStart, architecture: arch); | |
| final locations = <PCOffset>[]; | |
| for (final String s in [ | |
| ...(options['location'] as List<String>), | |
| ...options.rest, | |
| ]) { | |
| final location = convertAddress(header, s); | |
| if (location == null) return usageError('could not parse PC address $s'); | |
| locations.add(location); | |
| } | |
| if (locations.isEmpty) return usageError('no PC addresses to find'); | |
| for (final offset in locations) { | |
| final addr = dwarf.virtualAddressOf(offset); | |
| final frames = dwarf | |
| .callInfoForPCOffset(offset, includeInternalFrames: verbose) | |
| ?.map((CallInfo c) => ' $c'); | |
| final addrString = | |
| addr > 0 ? '0x${addr.toRadixString(16)}' : addr.toString(); | |
| print('For virtual address $addrString:'); | |
| if (frames == null) { | |
| print(' Invalid virtual address.'); | |
| } else if (frames.isEmpty) { | |
| print(' Not a call from user or library code.'); | |
| } else { | |
| frames.forEach(print); | |
| } | |
| } | |
| } | |
| final RegExp _unitDebugRE = RegExp(r'(\d+)=(.*)'); | |
| Future<void> translate(ArgResults options) async { | |
| void usageError(String message) => | |
| errorWithUsage(message, command: 'translate'); | |
| final dwarf = _loadFromFile(options['debug'], usageError); | |
| if (dwarf == null) { | |
| return; | |
| } | |
| final verbose = options['verbose']; | |
| final output = options['output'] != null | |
| ? io.File(path.canonicalize(path.normalize(options['output']))) | |
| .openWrite() | |
| : io.stdout; | |
| final input = options['input'] != null | |
| ? io.File(path.canonicalize(path.normalize(options['input']))).openRead() | |
| : io.stdin; | |
| Map<int, Dwarf>? dwarfByUnitId; | |
| if (options['unit_id_debug'] != null) { | |
| dwarfByUnitId = <int, Dwarf>{}; | |
| for (final unitArg in options['unit_id_debug']!) { | |
| final match = _unitDebugRE.firstMatch(unitArg); | |
| if (match == null) { | |
| usageError('Expected N=FILE where N is an integer, got $unitArg'); | |
| return; | |
| } | |
| final unitId = int.parse(match[1]!); | |
| final filename = match[2]!; | |
| final dwarf = _loadFromFile(filename, usageError); | |
| if (dwarf == null) return; | |
| dwarfByUnitId[unitId] = dwarf; | |
| } | |
| } | |
| List<Dwarf>? unitDwarfs; | |
| if (options['unit_debug'] != null) { | |
| unitDwarfs = <Dwarf>[]; | |
| for (final filename in options['unit_debug']!) { | |
| final dwarf = _loadFromFile(filename, usageError); | |
| if (dwarf == null) return; | |
| unitDwarfs.add(dwarf); | |
| } | |
| } | |
| final convertedStream = input | |
| .transform(utf8.decoder) | |
| .transform(const LineSplitter()) | |
| .transform(DwarfStackTraceDecoder( | |
| dwarf, | |
| includeInternalFrames: verbose, | |
| dwarfByUnitId: dwarfByUnitId, | |
| unitDwarfs: unitDwarfs, | |
| )) | |
| .map((s) => '$s\n') | |
| .transform(utf8.encoder); | |
| await output.addStream(convertedStream); | |
| await output.flush(); | |
| await output.close(); | |
| } | |
| Future<void> dump(ArgResults options) async { | |
| void usageError(String message) => errorWithUsage(message, command: 'dump'); | |
| if (options.rest.isEmpty) { | |
| return usageError('must provide a path to an ELF file or dSYM directory ' | |
| 'that contains DWARF information'); | |
| } | |
| final dwarf = _loadFromFile(options.rest.first, usageError); | |
| if (dwarf == null) { | |
| return; | |
| } | |
| final output = options['output'] != null | |
| ? io.File(path.canonicalize(path.normalize(options['output']))) | |
| .openWrite() | |
| : io.stdout; | |
| output.write(dwarf.dumpFileInfo()); | |
| await output.flush(); | |
| await output.close(); | |
| } | |
| Future<void> main(List<String> arguments) async { | |
| ArgResults options; | |
| try { | |
| options = _argParser.parse(arguments); | |
| } on FormatException catch (e) { | |
| return errorWithUsage(e.message); | |
| } | |
| if (options['help']) return print(_usages[options.command?.name]); | |
| if (options.command == null) return errorWithUsage('no command provided'); | |
| switch (options.command!.name) { | |
| case 'help': | |
| return help(options.command!); | |
| case 'find': | |
| return find(options.command!); | |
| case 'translate': | |
| return await translate(options.command!); | |
| case 'dump': | |
| return await dump(options.command!); | |
| } | |
| } |