b1b3e34b88
Dart VM changes: Note that the following changes are backwards compatible in the case that a Dart program has no deferred loading units (i.e., the Dart program is contained in a single shared object snapshot). When there are non-root loading units, the non-symbol stack trace header now includes information about loading units as follows: loading_unit: N, build_id: S, dso_base: A, instructions: A where N is an integer, S is a string of hex digits (0-9a-f), and A is a word-sized address printed as a hex string (without prefix). In addition, all non-symbolic stack frames for isolate instructions include a unit field, including those for the root loading unit, e.g., #NN abs <address> unit <id> virt <address> <symbol>+<offset> If there are no non-root loading units, then the non-symbolic stack trace is unchanged from its previous format. Adds a build ID to split deferred loading unit snapshots. Fixes: https://github.com/dart-lang/sdk/issues/43516 If separate debugging information is requested, the loading unit manifest includes a 'debugPath' field for each loading unit, which contains the path to its separate debugging information. Removes the attempt to store the relocated address of the instructions section when running from an assembled snapshot in the initialized BSS. Adds OS::GetAppDSOBase, which takes a pointer to the instructions section and returns a pointer to its loaded shared object in memory. For compiled-to-ELF snapshots, it does this using the relocated address of the instructions in the Image, and for assembled snapshots, it delegates to NativeSymbolResolver::LookupSharedObject. ----- Changes to package:native_stack_traces: PCOffset now has two new fields: * int? unitId: the unit ID of the loading unit, when available. * String? buildId: the build ID of the loading unit, when available. For PCOffsets in the VM section, the unitId and buildId are those of the root loading unit. The constructor for the DwarfStackTraceDecoder now takes two additional optional named arguments: * Map<int, Dwarf>? dwarfByUnitId: A map associating loading unit IDs with the appropriate Dwarf object. May or may not contain an entry for the root loading unit. * Iterable<Dwarf>? unitDwarfs: An iterable container holding Dwarf objects. May or may not contain an entry for the root loading unit. The Dwarf object that is passed to the DwarfStackTraceDecoder as a positional argument is used for all lookups within the root loading unit. If the dwarfByUnitId or unitDwarfs arguments contain an entry for the root loading unit, it should be the same as the positional argument. When decoding a non-symbolic stack frame with a non-root loading unit id, the decoder first looks in the map for the appropriate Dwarf object. If one is not found, the decoder uses the build ID for the loading unit to find the appropriate Dwarf object in the iterable container. If an appropriate Dwarf object cannot be found in either manner, the non-symbolic stack frame is emitted without change. The native_stack_traces:decode executable now takes two additional multi-options for the translate command: * -u, --unit_debug: Takes a path to the associated DWARF information. * --unit_id_debug: Takes N=FILE, where N is the loading unit ID and FILE is a path to the associated DWARF information. The arguments to -u are collected into an iterable container to be passed as the unitDwarfs argument to the DwarfStackTraceDecoder, and the arguments to --unit-id-debug are collected into a map to be passed as the dwarfByUnitId argument. TEST=vm/dart/use_dwarf_stack_traces_flag_deferred Issue: https://github.com/dart-lang/sdk/issues/53902 Change-Id: I210d4f69e4ae9fd37275a96beb1aac55c5e9d080 Cq-Include-Trybots: luci.dart.try:vm-aot-dwarf-linux-product-x64-try,vm-aot-linux-release-x64-try,vm-aot-linux-debug-x64-try,vm-aot-mac-release-arm64-try,vm-aot-mac-product-arm64-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/362380 Reviewed-by: Ryan Macnak <rmacnak@google.com> Commit-Queue: Tess Strickland <sstrickl@google.com>
420 lines
14 KiB
Dart
420 lines
14 KiB
Dart
// 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;
|
|
}
|
|
if (vmStart == null) {
|
|
return usageError('no VM start address found, one must be specified '
|
|
'with --vm_start');
|
|
}
|
|
|
|
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!);
|
|
}
|
|
}
|