Files
sdk/pkg/native_stack_traces/bin/decode.dart
T
Ryan Macnak d36adbacaf [vm] Remove the VM isolate.
The former contents of the VM isolate are now included into each isolate group. This makes each isolate group's heap independent, and in particular allows each heap to be allocated to a separate pointer cage (not done in this CL).

The duplicated stubs that allowed PC relative calls are removed, since the originals can now be the target of PC relative calls.

The bootstrapping needing to load an AppJIT or AppAOT snapshot is reduced to allocating the oddballs. The code is entirely dropped in the AOT runtime, but the JIT runtime still has it to allow for flags to affect the compilation of the stub code. Further refactoring might be able to remove this for the JIT runtime too, with only gen_snapshot knowing how to bootstrap.

Class serialization no longer distinguishes predefined classes.

The page containing null is marked as never-evacuate. null, false and true must not move because the compiler relies on their low bits having certain patterns for some optimizations. (Previously, the entire VM isolate heap never moved.)

Compaction is disabled for IA32. Due to register pressure, some stub calls must not use a scratch register and embed the address of Code.

The page containing the call-through-safepoint stub is frozen when running with --write-protect-code and the stub is created at runtime (instead of loaded from an AppJIT or AppAOT snapshot). This stub must remain executable even during a safepoint, as a foreign call might during return during a safepoint and only block after the stub directs it to the runtime.

The snapshot symbols are renamed to kDartSnapshotData and kDartSnapshotText. There is no need to distinguish the VM isolate's snapshot, and snaphots are per isolate group not per isolate. Aliases with the old names are added to ease migration.

Some global flags that were automatically set based on the VM isolate's snapshot are now isolate group flags and automatically set by the isolate group's snapshot.

TEST=ci
Change-Id: Iee82016057d609112e9b021d178fc3d4d18b5044
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/500621
Reviewed-by: Alexander Markov <alexmarkov@google.com>
Reviewed-by: Tess Strickland <sstrickl@google.com>
SLSA-Policy-Verified: SLSA Policy Verification Service <devtools-gerritcodereview-exitgate@google.com>
Commit-Queue: Ryan Macnak <rmacnak@google.com>
2026-05-18 11:35:03 -07:00

416 lines
13 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;
}
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!);
}
}