diff --git a/pkg/native_stack_traces/CHANGELOG.md b/pkg/native_stack_traces/CHANGELOG.md index b96e9658324..bba7560ef6a 100644 --- a/pkg/native_stack_traces/CHANGELOG.md +++ b/pkg/native_stack_traces/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.5.0 + +- Add support for parsing DWARF in Mach-O files and dSYM directories. +- Add `dump` command to replace the old `--dump_debug_file_contents` + flag to `find` and `translate`. + ## 0.4.6 - Upgrade to `package:lints` 2.0. diff --git a/pkg/native_stack_traces/bin/decode.dart b/pkg/native_stack_traces/bin/decode.dart index 12c61ef7211..cb44a02d144 100644 --- a/pkg/native_stack_traces/bin/decode.dart +++ b/pkg/native_stack_traces/bin/decode.dart @@ -18,10 +18,11 @@ ArgParser _createBaseDebugParser(ArgParser parser) => parser ..addFlag('verbose', abbr: 'v', negatable: false, - help: 'Translate all frames, not just user or library code frames') - ..addFlag('dump_debug_file_contents', - negatable: false, - help: 'Dump all the parsed information from the debugging file'); + 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)) @@ -48,6 +49,7 @@ final ArgParser _findParser = final ArgParser _helpParser = ArgParser(allowTrailingOptions: true); final ArgParser _argParser = ArgParser(allowTrailingOptions: true) + ..addCommand('dump', _dumpParser) ..addCommand('help', _helpParser) ..addCommand('find', _findParser) ..addCommand('translate', _translateParser) @@ -123,12 +125,22 @@ ${_argParser.usage} Options specific to the find command: ${_findParser.usage}'''; +final String _dumpUsage = ''' +Usage: decode dump [options] + +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 = { null: _mainUsage, '': _mainUsage, 'help': _helpUsage, 'translate': _translateUsage, 'find': _findUsage, + 'dump': _dumpUsage, }; const int _badUsageExitCode = 1; @@ -162,15 +174,16 @@ Dwarf? _loadFromFile(String? original, Function(String) usageError) { return null; } final filename = path.canonicalize(path.normalize(original)); - if (!io.File(filename).existsSync()) { + 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; } - final dwarf = Dwarf.fromFile(filename); - if (dwarf == null) { - usageError('file "$original" does not contain debugging information'); - } - return dwarf; } void find(ArgResults options) { @@ -199,10 +212,6 @@ void find(ArgResults options) { final dwarf = _loadFromFile(options['debug'], usageError); if (dwarf == null) return; - if (options['dump_debug_file_contents']) { - print(dwarf.dumpFileInfo()); - } - if ((options['vm_start'] == null) != (options['isolate_start'] == null)) { return usageError('need both VM start and isolate start'); } @@ -266,9 +275,6 @@ Future translate(ArgResults options) async { if (dwarf == null) { return; } - if (options['dump_debug_file_contents']) { - print(dwarf.dumpFileInfo()); - } final verbose = options['verbose']; final output = options['output'] != null @@ -291,6 +297,27 @@ Future translate(ArgResults options) async { await output.close(); } +Future 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 usageError("'${options.rest.first}' contains no DWARF information"); + } + + 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 main(List arguments) async { ArgResults options; @@ -310,5 +337,7 @@ Future main(List arguments) async { return find(options.command!); case 'translate': return await translate(options.command!); + case 'dump': + return await dump(options.command!); } } diff --git a/pkg/native_stack_traces/lib/src/convert.dart b/pkg/native_stack_traces/lib/src/convert.dart index 0bc8f2470a9..34c17c35584 100644 --- a/pkg/native_stack_traces/lib/src/convert.dart +++ b/pkg/native_stack_traces/lib/src/convert.dart @@ -178,18 +178,21 @@ Iterable collectPCOffsets(Iterable lines) sync* { class DwarfStackTraceDecoder extends StreamTransformerBase { final Dwarf _dwarf; final bool _includeInternalFrames; + StackTraceHeader? _header; DwarfStackTraceDecoder(this._dwarf, {bool includeInternalFrames = false}) : _includeInternalFrames = includeInternalFrames; + PCOffset? offsetOf(int address) => _header?.offsetOf(address); + @override Stream bind(Stream stream) async* { var depth = 0; - StackTraceHeader? header; + _header = null; await for (final line in stream) { final parsedHeader = _parseInstructionsLine(line); if (parsedHeader != null) { - header = parsedHeader; + _header = parsedHeader; depth = 0; yield line; continue; @@ -197,7 +200,7 @@ class DwarfStackTraceDecoder extends StreamTransformerBase { // If at any point we can't get appropriate information for the current // line as a stack trace line, then just pass the line through unchanged. final lineMatch = _traceLineRE.firstMatch(line); - final offset = _retrievePCOffset(header, lineMatch); + final offset = _retrievePCOffset(_header, lineMatch); final callInfo = offset?.callInfoFrom(_dwarf, includeInternalFrames: _includeInternalFrames); if (callInfo == null) { diff --git a/pkg/native_stack_traces/lib/src/dwarf.dart b/pkg/native_stack_traces/lib/src/dwarf.dart index 93a2af4cfb7..665f0c9bdc9 100644 --- a/pkg/native_stack_traces/lib/src/dwarf.dart +++ b/pkg/native_stack_traces/lib/src/dwarf.dart @@ -6,8 +6,9 @@ import 'dart:collection'; import 'dart:math'; import 'dart:typed_data'; -import 'constants.dart' as constants; +import 'dwarf_container.dart'; import 'elf.dart'; +import 'macho.dart'; import 'reader.dart'; int _initialLengthValue(Reader reader) { @@ -102,12 +103,14 @@ enum _AttributeForm { reference4, sectionOffset, string, + strp, } const _attributeForms = { 0x01: _AttributeForm.address, 0x08: _AttributeForm.string, 0x0c: _AttributeForm.flag, + 0x0e: _AttributeForm.strp, 0x0f: _AttributeForm.constant, 0x13: _AttributeForm.reference4, 0x17: _AttributeForm.sectionOffset, @@ -116,6 +119,7 @@ const _attributeForms = { const _attributeFormStrings = <_AttributeForm, String>{ _AttributeForm.address: 'DW_FORM_addr', _AttributeForm.string: 'DW_FORM_string', + _AttributeForm.strp: 'DW_FORM_strp', _AttributeForm.flag: 'DW_FORM_flag', _AttributeForm.constant: 'DW_FORM_udata', _AttributeForm.reference4: 'DW_FORM_ref4', @@ -145,6 +149,9 @@ class _Attribute { switch (form) { case _AttributeForm.string: return reader.readNullTerminatedString(); + case _AttributeForm.strp: + final offset = reader.readBytes(4); + return header.stringTable[offset]!; case _AttributeForm.flag: return reader.readByte() != 0; case _AttributeForm.address: @@ -161,6 +168,7 @@ class _Attribute { String valueToString(Object value, [CompilationUnit? unit]) { switch (form) { case _AttributeForm.string: + case _AttributeForm.strp: return value as String; case _AttributeForm.flag: return value.toString(); @@ -431,14 +439,16 @@ class CompilationUnitHeader { final int addressSize; // ignore: library_private_types_in_public_api final _AbbreviationsTable abbreviations; + final DwarfContainerStringTable stringTable; CompilationUnitHeader._(this.size, this.version, this.abbreviationsOffset, - this.addressSize, this.abbreviations); + this.addressSize, this.abbreviations, this.stringTable); static CompilationUnitHeader? fromReader( Reader reader, // ignore: library_private_types_in_public_api - Map abbreviationsTables) { + Map abbreviationsTables, + DwarfContainerStringTable stringTable) { final size = _initialLengthValue(reader); // An empty unit is an ending marker. if (size == 0) return null; @@ -453,8 +463,8 @@ class CompilationUnitHeader { '0x${paddedHex(abbreviationsOffset, 4)}'); } final addressSize = reader.readByte(); - return CompilationUnitHeader._( - size, version, abbreviationsOffset, addressSize, abbreviationsTable); + return CompilationUnitHeader._(size, version, abbreviationsOffset, + addressSize, abbreviationsTable, stringTable); } void writeToStringBuffer(StringBuffer buffer) { @@ -489,9 +499,10 @@ class CompilationUnit { static CompilationUnit? fromReader( Reader reader, // ignore: library_private_types_in_public_api - Map abbreviationsTables) { - final header = - CompilationUnitHeader.fromReader(reader, abbreviationsTables); + Map abbreviationsTables, + DwarfContainerStringTable stringTable) { + final header = CompilationUnitHeader.fromReader( + reader, abbreviationsTables, stringTable); if (header == null) return null; final referenceTable = Map.fromEntries(reader.readRepeatedWithOffsets( @@ -563,10 +574,11 @@ class DebugInfo { static DebugInfo fromReader( Reader reader, // ignore: library_private_types_in_public_api - Map abbreviationsTable) { + Map abbreviationsTable, + DwarfContainerStringTable stringTable) { final units = reader - .readRepeated( - (r) => CompilationUnit.fromReader(reader, abbreviationsTable)) + .readRepeated((r) => + CompilationUnit.fromReader(reader, abbreviationsTable, stringTable)) .toList(); return DebugInfo._(units); } @@ -914,14 +926,18 @@ class LineNumberProgram { void applySpecialOpcode(int opcode) { final adjustedOpcode = opcode - header.opcodeBase; - state.address = adjustedOpcode ~/ header.lineRange; - state.line += header.lineBase + (adjustedOpcode % header.lineRange); + final addrDiff = (adjustedOpcode ~/ header.lineRange) * + header.minimumInstructionLength; + final lineDiff = header.lineBase + (adjustedOpcode % header.lineRange); + state.address += addrDiff; + state.line += lineDiff; } while (!reader.done) { final opcode = reader.readByte(); if (opcode >= header.opcodeBase) { applySpecialOpcode(opcode); + yield state.clone(); continue; } switch (opcode) { @@ -975,7 +991,8 @@ class LineNumberProgram { state.basicBlock = true; break; case 8: // DW_LNS_const_add_pc - applySpecialOpcode(255); + state.address += ((255 - header.opcodeBase) ~/ header.lineRange) * + header.minimumInstructionLength; break; case 9: // DW_LNS_fixed_advance_pc state.address += reader.readBytes(2); @@ -1200,12 +1217,19 @@ class PCOffset { other is PCOffset && offset == other.offset && section == other.section; @override - String toString() => 'PCOffset($section, $offset)'; + String toString() => 'PCOffset($section, 0x${offset.toRadixString(16)})'; +} + +class UnimplementedStringTable extends DwarfContainerStringTable { + @override + String? operator [](int index) { + throw 'No string table found in DWARF information'; + } } /// The DWARF debugging information for a Dart snapshot. class Dwarf { - final Elf _elf; + final DwarfContainer _container; final Map _abbreviationsTables; final DebugInfo _debugInfo; final LineNumberInfo _lineNumberInfo; @@ -1218,17 +1242,38 @@ class Dwarf { /// DWARF information. final int isolateStartAddress; - Dwarf._(this._elf, this._abbreviationsTables, this._debugInfo, + Dwarf._(this._container, this._abbreviationsTables, this._debugInfo, this._lineNumberInfo, this.vmStartAddress, this.isolateStartAddress); + static Dwarf fromDwarfContainer(Reader reader, DwarfContainer container) { + final abbrevReader = container.abbreviationsTableReader(reader); + final abbreviationsTables = Map.fromEntries( + abbrevReader.readRepeatedWithOffsets(_AbbreviationsTable.fromReader)); + + final lineNumberInfo = + LineNumberInfo.fromReader(container.lineNumberInfoReader(reader)); + + final stringTable = container.stringTable ?? UnimplementedStringTable(); + final debugInfo = DebugInfo.fromReader( + container.debugInfoReader(reader), abbreviationsTables, stringTable); + + return Dwarf._(container, abbreviationsTables, debugInfo, lineNumberInfo, + container.vmStartAddress, container.isolateStartAddress); + } + /// Attempts to load the DWARF debugging information from the reader. /// /// Returns a [Dwarf] object if the load succeeds, otherwise returns null. static Dwarf? fromReader(Reader reader) { - // Currently, the only DWARF-containing format we recognize is ELF. final elf = Elf.fromReader(reader); - if (elf == null) return null; - return Dwarf._loadSectionsFromElf(reader, elf); + if (elf != null) { + return Dwarf.fromDwarfContainer(reader, elf); + } + final macho = MachO.fromReader(reader); + if (macho != null) { + return Dwarf.fromDwarfContainer(reader, macho); + } + return null; } /// Attempts to load the DWARF debugging information from the given bytes. @@ -1241,54 +1286,12 @@ class Dwarf { /// /// Returns a [Dwarf] object if the load succeeds, otherwise returns null. static Dwarf? fromFile(String path) => - Dwarf.fromReader(Reader.fromFile(path)); - - static Dwarf _loadSectionsFromElf(Reader reader, Elf elf) { - final abbrevSection = elf.namedSections('.debug_abbrev').single; - final abbrevReader = abbrevSection.refocusedCopy(reader); - final abbreviationsTables = Map.fromEntries( - abbrevReader.readRepeatedWithOffsets(_AbbreviationsTable.fromReader)); - - final lineNumberSection = elf.namedSections('.debug_line').single; - final lineNumberInfo = - LineNumberInfo.fromReader(lineNumberSection.refocusedCopy(reader)); - - final infoSection = elf.namedSections('.debug_info').single; - final debugInfo = DebugInfo.fromReader( - infoSection.refocusedCopy(reader), abbreviationsTables); - - final vmStartSymbol = elf.dynamicSymbolFor(constants.vmSymbolName); - if (vmStartSymbol == null) { - throw FormatException( - 'Expected a dynamic symbol with name ${constants.vmSymbolName}'); - } - final vmStartAddress = vmStartSymbol.value; - - final isolateStartSymbol = - elf.dynamicSymbolFor(constants.isolateSymbolName); - if (isolateStartSymbol == null) { - throw FormatException( - 'Expected a dynamic symbol with name ${constants.isolateSymbolName}'); - } - final isolateStartAddress = isolateStartSymbol.value; - - return Dwarf._(elf, abbreviationsTables, debugInfo, lineNumberInfo, - vmStartAddress, isolateStartAddress); - } + Dwarf.fromReader(Reader.fromFile(MachO.handleDSYM(path))); /// The build ID for the debugging information. /// /// Returns null if there is no build ID information recorded. - String? get buildId { - final sections = _elf.namedSections(constants.buildIdSectionName); - if (sections.isEmpty) return null; - final note = sections.single as Note; - if (note.type != constants.buildIdNoteType) return null; - if (note.name != constants.buildIdNoteName) return null; - return note.description - .map((i) => i.toRadixString(16).padLeft(2, '0')) - .join(); - } + String? get buildId => _container.buildId; /// The call information for the given virtual address. There may be /// multiple [CallInfo] objects returned for a single virtual address when @@ -1302,7 +1305,7 @@ class Dwarf { {bool includeInternalFrames = false}) { var calls = _debugInfo.callInfo(_lineNumberInfo, address); if (calls == null) { - final symbol = _elf.staticSymbolAt(address); + final symbol = _container.staticSymbolAt(address); if (symbol != null) { final offset = address - symbol.value; calls = [StubCallInfo(name: symbol.name, offset: offset)]; @@ -1358,7 +1361,7 @@ class Dwarf { String dumpFileInfo() { final buffer = StringBuffer(); - _elf.writeToStringBuffer(buffer); + _container.writeToStringBuffer(buffer); buffer.writeln(); writeToStringBuffer(buffer); return buffer.toString(); diff --git a/pkg/native_stack_traces/lib/src/dwarf_container.dart b/pkg/native_stack_traces/lib/src/dwarf_container.dart new file mode 100644 index 00000000000..feb3b573412 --- /dev/null +++ b/pkg/native_stack_traces/lib/src/dwarf_container.dart @@ -0,0 +1,30 @@ +// Copyright (c) 2022, 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 'reader.dart'; + +abstract class DwarfContainerStringTable { + String? operator [](int index); +} + +abstract class DwarfContainerSymbol { + int get value; + String get name; +} + +abstract class DwarfContainer { + Reader debugInfoReader(Reader containerReader); + Reader lineNumberInfoReader(Reader containerReader); + Reader abbreviationsTableReader(Reader containerReader); + DwarfContainerSymbol? staticSymbolAt(int address); + + int get vmStartAddress; + int get isolateStartAddress; + + String? get buildId; + + DwarfContainerStringTable? get stringTable; + + void writeToStringBuffer(StringBuffer buffer); +} diff --git a/pkg/native_stack_traces/lib/src/elf.dart b/pkg/native_stack_traces/lib/src/elf.dart index c29b1e412c6..4d69efc8dc2 100644 --- a/pkg/native_stack_traces/lib/src/elf.dart +++ b/pkg/native_stack_traces/lib/src/elf.dart @@ -6,6 +6,8 @@ import 'dart:typed_data'; +import 'constants.dart' as constants; +import 'dwarf_container.dart'; import 'reader.dart'; int _readElfBytes(Reader reader, int bytes, int alignment) { @@ -90,10 +92,12 @@ class ElfHeader { this.sectionHeaderStringsIndex); static ElfHeader? fromReader(Reader reader) { + final start = reader.offset; final fileSize = reader.length; for (final sigByte in _ELFMAG.codeUnits) { if (reader.readByte() != sigByte) { + reader.seek(start, absolute: true); return null; } } @@ -266,7 +270,11 @@ class ProgramHeaderEntry { static const _PT_NULL = 0; static const _PT_LOAD = 1; static const _PT_DYNAMIC = 2; + static const _PT_NOTE = 4; static const _PT_PHDR = 6; + static const _PT_GNU_EH_FRAME = 0x6474e550; + static const _PT_GNU_STACK = 0x6474e551; + static const _PT_GNU_RELRO = 0x6474e552; ProgramHeaderEntry._(this.type, this.flags, this.offset, this.vaddr, this.paddr, this.filesz, this.memsz, this.align, this.wordSize); @@ -296,7 +304,11 @@ class ProgramHeaderEntry { _PT_NULL: 'PT_NULL', _PT_LOAD: 'PT_LOAD', _PT_DYNAMIC: 'PT_DYNAMIC', + _PT_NOTE: 'PT_NOTE', _PT_PHDR: 'PT_PHDR', + _PT_GNU_EH_FRAME: 'PT_GNU_EH_FRAME', + _PT_GNU_STACK: 'PT_GNU_STACK', + _PT_GNU_RELRO: 'PT_GNU_RELRO', }; static String _typeToString(int type) => @@ -646,7 +658,7 @@ class Note extends Section { } /// A map from table offsets to strings, used to store names of ELF objects. -class StringTable extends Section { +class StringTable extends Section implements DwarfContainerStringTable { final Map _entries; StringTable._(entry, this._entries) : super._(entry); @@ -658,8 +670,22 @@ class StringTable extends Section { return StringTable._(entry, entries); } - String? operator [](int index) => _entries[index]; - bool containsKey(int index) => _entries.containsKey(index); + @override + String? operator [](int index) { + // Fast case: Index is for the start of a null terminated string. + if (_entries.containsKey(index)) { + return _entries[index]; + } + // We can index into null terminated string entries for suffixes of + // that string, so do a linear search to find the appropriate entry. + for (final kv in _entries.entries) { + final start = index - kv.key; + if (start >= 0 && start <= kv.value.length) { + return kv.value.substring(start); + } + } + return null; + } @override void writeToStringBuffer(StringBuffer buffer) { @@ -680,6 +706,7 @@ class StringTable extends Section { enum SymbolBinding { STB_LOCAL, STB_GLOBAL, + STB_WEAK, } enum SymbolType { @@ -696,15 +723,17 @@ enum SymbolVisibility { } /// A symbol in an ELF file, which names a portion of the virtual address space. -class Symbol { +class Symbol implements DwarfContainerSymbol { final int nameIndex; final int info; final int other; final int sectionIndex; + @override final int value; final int size; final int _wordSize; - late String name; + @override + late final String name; Symbol._(this.nameIndex, this.info, this.other, this.sectionIndex, this.value, this.size, this._wordSize); @@ -731,14 +760,6 @@ class Symbol { nameIndex, info, other, sectionIndex, value, size, wordSize); } - void _cacheNameFromStringTable(StringTable table) { - final nameFromTable = table[nameIndex]; - if (nameFromTable == null) { - throw FormatException('Index $nameIndex not found in string table'); - } - name = nameFromTable; - } - SymbolBinding get bind => SymbolBinding.values[info >> 4]; SymbolType get type => SymbolType.values[info & 0x0f]; SymbolVisibility get visibility => SymbolVisibility.values[other & 0x03]; @@ -755,6 +776,9 @@ class Symbol { case SymbolBinding.STB_LOCAL: buffer.write(' a local'); break; + case SymbolBinding.STB_WEAK: + buffer.write(' a weak'); + break; } switch (visibility) { case SymbolVisibility.STV_DEFAULT: @@ -804,8 +828,13 @@ class SymbolTable extends Section { void _cacheNames(StringTable stringTable) { _nameCache.clear(); for (final symbol in _entries) { - symbol._cacheNameFromStringTable(stringTable); - _nameCache[symbol.name] = symbol; + final index = symbol.nameIndex; + final name = stringTable[index]; + if (name == null) { + throw FormatException('Index $index not found in string table'); + } + symbol.name = name; + _nameCache[name] = symbol; } } @@ -932,7 +961,7 @@ class DynamicTable extends Section { } /// Information parsed from an Executable and Linking Format (ELF) file. -class Elf { +class Elf implements DwarfContainer { final ElfHeader _header; final ProgramHeader _programHeader; final SectionHeader _sectionHeader; @@ -984,16 +1013,24 @@ class Elf { /// Reverse lookup of the static symbol that contains the given virtual /// address. Returns null if no static symbol matching the address is found. + @override Symbol? staticSymbolAt(int address) { + Symbol? bestSym; for (final section in namedSections('.symtab')) { final table = section as SymbolTable; for (final symbol in table.values) { final start = symbol.value; - final end = start + symbol.size; - if (start <= address && address < end) return symbol; + if (start > address) continue; + // If given a non-zero extent of a symbol, make sure the address is + // within the extent. + if (symbol.size > 0 && (start + symbol.size <= address)) continue; + // Pick the symbol with a start closest to the given address. + if (bestSym == null || (bestSym.value < start)) { + bestSym = symbol; + } } } - return null; + return bestSym; } /// Creates an [Elf] from the data pointed to by [reader]. @@ -1077,6 +1114,55 @@ class Elf { header, programHeader, sectionHeader, sections, sectionsByName); } + @override + Reader abbreviationsTableReader(Reader containerReader) => + namedSections('.debug_abbrev').single.refocusedCopy(containerReader); + + @override + Reader lineNumberInfoReader(Reader containerReader) => + namedSections('.debug_line').single.refocusedCopy(containerReader); + + @override + Reader debugInfoReader(Reader containerReader) => + namedSections('.debug_info').single.refocusedCopy(containerReader); + + @override + int get vmStartAddress { + final vmStartSymbol = dynamicSymbolFor(constants.vmSymbolName); + if (vmStartSymbol == null) { + throw FormatException( + 'Expected a dynamic symbol with name ${constants.vmSymbolName}'); + } + return vmStartSymbol.value; + } + + @override + int get isolateStartAddress { + final isolateStartSymbol = dynamicSymbolFor(constants.isolateSymbolName); + if (isolateStartSymbol == null) { + throw FormatException( + 'Expected a dynamic symbol with name ${constants.isolateSymbolName}'); + } + return isolateStartSymbol.value; + } + + @override + String? get buildId { + final sections = namedSections(constants.buildIdSectionName); + if (sections.isEmpty) return null; + final note = sections.single as Note; + if (note.type != constants.buildIdNoteType) return null; + if (note.name != constants.buildIdNoteName) return null; + return note.description + .map((i) => i.toRadixString(16).padLeft(2, '0')) + .join(); + } + + // Currently we don't handle DWARF uses of string tables in ELF files. + @override + DwarfContainerStringTable? get stringTable => null; + + @override void writeToStringBuffer(StringBuffer buffer) { buffer ..writeln('-----------------------------------------------------') diff --git a/pkg/native_stack_traces/lib/src/macho.dart b/pkg/native_stack_traces/lib/src/macho.dart new file mode 100644 index 00000000000..d46a5076dc7 --- /dev/null +++ b/pkg/native_stack_traces/lib/src/macho.dart @@ -0,0 +1,548 @@ +// Copyright (c) 2022, 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. + +// ignore_for_file: constant_identifier_names + +import 'dart:typed_data'; + +import 'package:path/path.dart' as path; + +import 'constants.dart' as constants; +import 'dwarf_container.dart'; +import 'reader.dart'; + +int _readMachOUint8(Reader reader) => reader.readByte(signed: false); + +int _readMachOUint16(Reader reader) => reader.readBytes(2, signed: false); + +int _readMachOUint32(Reader reader) => reader.readBytes(4, signed: false); + +int _readMachOUword(Reader reader) => + reader.readBytes(reader.wordSize, signed: false); + +class StringTable implements DwarfContainerStringTable { + final Map _stringsByOffset; + + StringTable._(this._stringsByOffset); + + static StringTable fromReader(Reader reader) => StringTable._(Map.fromEntries( + reader.readRepeatedWithOffsets((r) => r.readNullTerminatedString()))); + + @override + String? operator [](int index) { + // Fast case: Index is for the start of a null terminated string. + if (_stringsByOffset.containsKey(index)) { + return _stringsByOffset[index]; + } + // We can index into null terminated string entries for suffixes of + // that string, so do a linear search to find the appropriate entry. + for (final kv in _stringsByOffset.entries) { + final start = index - kv.key; + if (start >= 0 && start <= kv.value.length) { + return kv.value.substring(start); + } + } + return null; + } + + void writeToStringBuffer(StringBuffer buffer) { + for (final k in _stringsByOffset.keys) { + buffer + ..write(k.toString().padLeft(8, ' ')) + ..write(' => ') + ..writeln(_stringsByOffset[k]); + } + } + + @override + String toString() { + final buffer = StringBuffer(); + writeToStringBuffer(buffer); + return buffer.toString(); + } +} + +class Symbol implements DwarfContainerSymbol { + final int index; + final int type; + final int sect; + final int desc; + @override + final int value; + @override + late final String name; + + Symbol._(this.index, this.type, this.sect, this.desc, this.value); + + static Symbol fromReader(Reader reader) { + final index = _readMachOUint32(reader); + final type = _readMachOUint8(reader); + final sect = _readMachOUint8(reader); + final desc = _readMachOUint16(reader); + final value = _readMachOUword(reader); + return Symbol._(index, type, sect, desc, value); + } +} + +class SymbolTable { + final Map _symbols; + + SymbolTable._(this._symbols); + + static SymbolTable fromReader( + Reader reader, int nsyms, StringTable stringTable) { + final symbols = {}; + for (int i = 0; i < nsyms; i++) { + final symbol = Symbol.fromReader(reader); + final index = symbol.index; + final name = stringTable[index]; + if (name == null) { + throw FormatException('Index $index not found in string table'); + } + symbol.name = name; + symbols[name] = symbol; + } + return SymbolTable._(symbols); + } + + Iterable get keys => _symbols.keys; + Iterable get values => _symbols.values; + + Symbol? operator [](String name) => _symbols[name]; + + bool containsKey(String name) => _symbols.containsKey(name); +} + +class LoadCommand { + final int cmd; + final int cmdsize; + + LoadCommand._(this.cmd, this.cmdsize); + + static const LC_SEGMENT = 0x1; + static const LC_SYMTAB = 0x2; + static const LC_SEGMENT_64 = 0x19; + + static LoadCommand fromReader(Reader reader) { + final start = reader.offset; // cmdsize includes size of cmd and cmdsize. + final cmd = _readMachOUint32(reader); + final cmdsize = _readMachOUint32(reader); + LoadCommand command = LoadCommand._(cmd, cmdsize); + switch (cmd) { + case LC_SEGMENT: + case LC_SEGMENT_64: + command = SegmentCommand.fromReader(reader, cmd, cmdsize); + break; + case LC_SYMTAB: + command = SymbolTableCommand.fromReader(reader, cmd, cmdsize); + break; + default: + break; + } + reader.seek(start + cmdsize, absolute: true); + return command; + } + + void writeToStringBuffer(StringBuffer buffer) { + buffer + ..write('Uninterpreted command 0x') + ..write(cmd.toRadixString(16)) + ..write(' of size ') + ..writeln(cmdsize); + } + + @override + String toString() { + StringBuffer buffer = StringBuffer(); + writeToStringBuffer(buffer); + return buffer.toString(); + } +} + +class SegmentCommand extends LoadCommand { + final String segname; + final int vmaddr; + final int vmsize; + final int fileoff; + final int filesize; + final int maxprot; + final int initprot; + final int nsects; + final int flags; + final Map sections; + + SegmentCommand._( + int cmd, + int cmdsize, + this.segname, + this.vmaddr, + this.vmsize, + this.fileoff, + this.filesize, + this.maxprot, + this.initprot, + this.nsects, + this.flags, + this.sections) + : super._(cmd, cmdsize); + + static SegmentCommand fromReader(Reader reader, int cmd, int cmdsize) { + final segname = reader.readFixedLengthNullTerminatedString(16); + final vmaddr = _readMachOUword(reader); + final vmsize = _readMachOUword(reader); + final fileoff = _readMachOUword(reader); + final filesize = _readMachOUword(reader); + final maxprot = _readMachOUint32(reader); + final initprot = _readMachOUint32(reader); + final nsects = _readMachOUint32(reader); + final flags = _readMachOUint32(reader); + final sections = {}; + for (int i = 0; i < nsects; i++) { + final section = Section.fromReader(reader); + sections[section.sectname] = section; + } + return SegmentCommand._(cmd, cmdsize, segname, vmaddr, vmsize, fileoff, + filesize, maxprot, initprot, nsects, flags, sections); + } + + @override + void writeToStringBuffer(StringBuffer buffer) { + buffer + ..write('Segment "') + ..write(segname) + ..write('" of size ') + ..write(filesize) + ..write(' at offset 0x') + ..writeln(fileoff.toRadixString(16)); + buffer.writeln('Sections:'); + for (final section in sections.values) { + section.writeToStringBuffer(buffer); + buffer.writeln(); + } + } +} + +class Section { + String sectname; + String segname; + int addr; + int size; + int offset; + int align; + int reloff; + int nreloc; + int flags; + int reserved1; + int reserved2; + int? reserved3; + + Section._( + this.sectname, + this.segname, + this.addr, + this.size, + this.offset, + this.align, + this.reloff, + this.nreloc, + this.flags, + this.reserved1, + this.reserved2, + this.reserved3); + + static Section fromReader(Reader reader) { + final sectname = reader.readFixedLengthNullTerminatedString(16); + final segname = reader.readFixedLengthNullTerminatedString(16); + final addr = _readMachOUword(reader); + final size = _readMachOUword(reader); + final offset = _readMachOUint32(reader); + final align = _readMachOUint32(reader); + final reloff = _readMachOUint32(reader); + final nreloc = _readMachOUint32(reader); + final flags = _readMachOUint32(reader); + final reserved1 = _readMachOUint32(reader); + final reserved2 = _readMachOUint32(reader); + final reserved3 = (reader.wordSize == 8) ? _readMachOUint32(reader) : null; + return Section._(sectname, segname, addr, size, offset, align, reloff, + nreloc, flags, reserved1, reserved2, reserved3); + } + + Reader refocus(Reader reader) => reader.refocusedCopy(offset, size); + + void writeToStringBuffer(StringBuffer buffer) { + buffer + ..write('Section "') + ..write(sectname) + ..write('" of size ') + ..write(size) + ..write(' at offset 0x') + ..write(paddedHex(offset, 4)); + } + + @override + String toString() { + StringBuffer buffer = StringBuffer(); + writeToStringBuffer(buffer); + return buffer.toString(); + } +} + +class SymbolTableCommand extends LoadCommand { + final int _symoff; + final int _nsyms; + final int _stroff; + final int _strsize; + + SymbolTableCommand._(int cmd, int cmdsize, this._symoff, this._nsyms, + this._stroff, this._strsize) + : super._(cmd, cmdsize); + + static SymbolTableCommand fromReader(Reader reader, int cmd, int cmdsize) { + final symoff = _readMachOUint32(reader); + final nsyms = _readMachOUint32(reader); + final stroff = _readMachOUint32(reader); + final strsize = _readMachOUint32(reader); + return SymbolTableCommand._(cmd, cmdsize, symoff, nsyms, stroff, strsize); + } + + SymbolTable load(Reader reader) { + final stringTable = + StringTable.fromReader(reader.refocusedCopy(_stroff, _strsize)); + return SymbolTable.fromReader( + reader.refocusedCopy(_symoff), _nsyms, stringTable); + } + + @override + void writeToStringBuffer(StringBuffer buffer) { + buffer + ..write('Symbol table with ') + ..write(_nsyms) + ..write(' symbols of size ') + ..writeln(cmdsize); + } +} + +class MachOHeader { + final int magic; + final int cputype; + final int cpusubtype; + final int filetype; + final int ncmds; + final int sizeofcmds; + final int flags; + final int? reserved; + final int size; + + MachOHeader._(this.magic, this.cputype, this.cpusubtype, this.filetype, + this.ncmds, this.sizeofcmds, this.flags, this.reserved, this.size); + + static const _MH_MAGIC = 0xfeedface; + static const _MH_CIGAM = 0xcefaedfe; + static const _MH_MAGIC_64 = 0xfeedfacf; + static const _MH_CIGAM_64 = 0xcffaedfe; + + static MachOHeader? fromReader(Reader reader) { + final start = reader.offset; + // Initially assume host endianness. + reader.endian = Endian.host; + final magic = _readMachOUint32(reader); + if (magic == _MH_MAGIC || magic == _MH_CIGAM) { + reader.wordSize = 4; + } else if (magic == _MH_MAGIC_64 || magic == _MH_CIGAM_64) { + reader.wordSize = 8; + } else { + // Not an expected magic value, so not a supported Mach-O file. + return null; + } + if (magic == _MH_CIGAM || magic == _MH_CIGAM_64) { + reader.endian = Endian.host == Endian.big ? Endian.little : Endian.big; + } + final cputype = _readMachOUint32(reader); + final cpusubtype = _readMachOUint32(reader); + final filetype = _readMachOUint32(reader); + final ncmds = _readMachOUint32(reader); + final sizeofcmds = _readMachOUint32(reader); + final flags = _readMachOUint32(reader); + final reserved = reader.wordSize == 8 ? _readMachOUint32(reader) : null; + final size = reader.offset - start; + return MachOHeader._(magic, cputype, cpusubtype, filetype, ncmds, + sizeofcmds, flags, reserved, size); + } + + void writeToStringBuffer(StringBuffer buffer) { + buffer + ..write('Magic: 0x') + ..writeln(paddedHex(magic, 4)); + buffer + ..write('Cpu Type: 0x') + ..writeln(paddedHex(cputype, 4)); + buffer + ..write('Cpu Subtype: 0x') + ..writeln(paddedHex(cpusubtype, 4)); + buffer + ..write('Filetype: 0x') + ..writeln(paddedHex(filetype, 4)); + buffer + ..write('Number of commands: ') + ..writeln(ncmds); + buffer + ..write('Size of commands: ') + ..writeln(sizeofcmds); + buffer + ..write('Flags: 0x') + ..writeln(paddedHex(flags, 4)); + if (reserved != null) { + buffer + ..write('Reserved: 0x') + ..writeln(paddedHex(reserved!, 4)); + } + } + + @override + String toString() { + final buffer = StringBuffer(); + writeToStringBuffer(buffer); + return buffer.toString(); + } +} + +class MachO implements DwarfContainer { + final MachOHeader _header; + final List _commands; + final SymbolTable _symbolTable; + final SegmentCommand _dwarfSegment; + final StringTable? _dwarfStringTable; + + MachO._(this._header, this._commands, this._symbolTable, this._dwarfSegment, + this._dwarfStringTable); + + static MachO? fromReader(Reader machOReader) { + // MachO files contain absolute offsets from the start of the file, so + // make sure we have a reader that a) makes no assumptions about the + // endianness or word size, since we'll read those in the header and b) + // has an internal offset of 0 so absolute offsets can be used directly. + final reader = Reader.fromTypedData(ByteData.sublistView(machOReader.bdata, + machOReader.bdata.offsetInBytes + machOReader.offset)); + final header = MachOHeader.fromReader(reader); + if (header == null) return null; + + final commandReader = + reader.refocusedCopy(reader.offset, header.sizeofcmds); + final commands = + List.of(commandReader.readRepeated(LoadCommand.fromReader)); + assert(commands.length == header.ncmds); + + final symbolTable = + commands.whereType().single.load(reader); + + final dwarfSegment = commands + .whereType() + .firstWhere((sc) => sc!.segname == '__DWARF', orElse: () => null); + if (dwarfSegment == null) { + print("No DWARF information in Mach-O file"); + return null; + } + + final dwarfStringTableSection = dwarfSegment.sections['__debug_str']; + StringTable? dwarfStringTable; + if (dwarfStringTableSection != null) { + dwarfStringTable = + StringTable.fromReader(dwarfStringTableSection.refocus(reader)); + } + + // Set the wordSize and endian of the original reader before returning. + machOReader.wordSize = reader.wordSize; + machOReader.endian = reader.endian; + + return MachO._( + header, commands, symbolTable, dwarfSegment, dwarfStringTable); + } + + static String handleDSYM(String fileName) { + if (!fileName.endsWith('.dSYM')) { + return fileName; + } + var baseName = path.basename(fileName); + baseName = baseName.substring(0, baseName.length - '.dSYM'.length); + return path.join(fileName, 'Contents', 'Resources', 'DWARF', baseName); + } + + static MachO? fromFile(String fileName) => + MachO.fromReader(Reader.fromFile(MachO.handleDSYM(fileName))); + + @override + Reader abbreviationsTableReader(Reader reader) => + _dwarfSegment.sections['__debug_abbrev']!.refocus(reader); + @override + Reader lineNumberInfoReader(Reader reader) => + _dwarfSegment.sections['__debug_line']!.refocus(reader); + @override + Reader debugInfoReader(Reader reader) => + _dwarfSegment.sections['__debug_info']!.refocus(reader); + + @override + int get vmStartAddress { + if (!_symbolTable.containsKey(constants.vmSymbolName)) { + throw FormatException( + 'Expected a dynamic symbol with name ${constants.vmSymbolName}'); + } + return _symbolTable[constants.vmSymbolName]!.value; + } + + @override + int get isolateStartAddress { + if (!_symbolTable.containsKey(constants.isolateSymbolName)) { + throw FormatException( + 'Expected a dynamic symbol with name ${constants.isolateSymbolName}'); + } + return _symbolTable[constants.isolateSymbolName]!.value; + } + + @override + String? get buildId => null; + + @override + DwarfContainerStringTable? get stringTable => _dwarfStringTable; + + @override + Symbol? staticSymbolAt(int address) { + Symbol? bestSym; + for (final symbol in _symbolTable.values) { + if (symbol.value > address) continue; + // Pick the symbol with a value closest to the given address. + if (bestSym == null || (bestSym.value < symbol.value)) { + bestSym = symbol; + } + } + return bestSym; + } + + @override + void writeToStringBuffer(StringBuffer buffer) { + buffer + ..writeln('----------------------------------------') + ..writeln(' Header') + ..writeln('----------------------------------------') + ..writeln(''); + _header.writeToStringBuffer(buffer); + buffer + ..writeln('') + ..writeln('') + ..writeln('----------------------------------------') + ..writeln(' Load commands') + ..writeln('----------------------------------------') + ..writeln(''); + for (final command in _commands) { + command.writeToStringBuffer(buffer); + buffer.writeln(''); + } + } + + @override + String toString() { + final buffer = StringBuffer(); + writeToStringBuffer(buffer); + return buffer.toString(); + } +} diff --git a/pkg/native_stack_traces/lib/src/reader.dart b/pkg/native_stack_traces/lib/src/reader.dart index 815887878ae..99148d307b4 100644 --- a/pkg/native_stack_traces/lib/src/reader.dart +++ b/pkg/native_stack_traces/lib/src/reader.dart @@ -37,9 +37,15 @@ class Reader { bdata = ByteData.sublistView(File(path).readAsBytesSync()); /// Returns a reader focused on a different portion of the underlying buffer. - Reader refocusedCopy(int pos, int size) { + /// If size is not provided, then the new reader extends to the end of the + /// buffer. + Reader refocusedCopy(int pos, [int? size]) { assert(pos >= 0 && pos < bdata.buffer.lengthInBytes); - assert(size >= 0 && (pos + size) <= bdata.buffer.lengthInBytes); + if (size != null) { + assert(size >= 0 && (pos + size) <= bdata.buffer.lengthInBytes); + } else { + size = bdata.buffer.lengthInBytes - pos; + } return Reader.fromTypedData(ByteData.view(bdata.buffer, pos, size), wordSize: _wordSize, endian: _endian); } @@ -51,7 +57,7 @@ class Reader { void seek(int offset, {bool absolute = false}) { final newOffset = (absolute ? 0 : _offset) + offset; - assert(newOffset >= 0 && newOffset < bdata.lengthInBytes); + assert(newOffset >= 0 && newOffset <= bdata.lengthInBytes); _offset = newOffset; } @@ -85,16 +91,26 @@ class Reader { int readByte({bool signed = false}) => readBytes(1, signed: signed); int readWord() => readBytes(wordSize); - String readNullTerminatedString() { - final start = bdata.offsetInBytes + _offset; - for (var i = 0; _offset + i < bdata.lengthInBytes; i++) { - if (bdata.getUint8(_offset + i) == 0) { - _offset += i + 1; - return String.fromCharCodes(bdata.buffer.asUint8List(start, i)); + String readNullTerminatedString({int? maxSize}) { + final start = _offset; + int end = maxSize != null ? _offset + maxSize : bdata.lengthInBytes; + for (; _offset < end; _offset++) { + if (bdata.getUint8(_offset) == 0) { + end = _offset; + _offset++; // Move reader past null terminator. + break; } } return String.fromCharCodes( - bdata.buffer.asUint8List(start, bdata.lengthInBytes - _offset)); + bdata.buffer.asUint8List(bdata.offsetInBytes + start, end - start)); + } + + String readFixedLengthNullTerminatedString(int maxSize) { + final start = _offset; + final str = readNullTerminatedString(maxSize: maxSize); + // Ensure reader points past fixed space, not at end of string within it. + _offset = start + maxSize; + return str; } int readLEB128EncodedInteger({bool signed = false}) { diff --git a/runtime/lib/object.cc b/runtime/lib/object.cc index 31d6f2557d8..13534d8de3f 100644 --- a/runtime/lib/object.cc +++ b/runtime/lib/object.cc @@ -318,6 +318,13 @@ DEFINE_NATIVE_ENTRY(Internal_deoptimizeFunctionsOnStack, 0, 0) { return Object::null(); } +DEFINE_NATIVE_ENTRY(Internal_randomAddressInsideAllocateObjectStub, 0, 0) { + auto& stub = Code::Handle( + zone, isolate->group()->object_store()->allocate_object_stub()); + const uword random_offset = isolate->random()->NextUInt32() % stub.Size(); + return Smi::New(stub.EntryPoint() + random_offset); +} + static bool ExtractInterfaceTypeArgs(Zone* zone, const Class& instance_cls, const TypeArguments& instance_type_args, diff --git a/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_program.dart b/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_program.dart index c422f18a08f..9d291b367fa 100644 --- a/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_program.dart +++ b/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_program.dart @@ -4,6 +4,8 @@ // Test that the full stacktrace in an error object matches the stacktrace // handed to the catch clause. +import 'dart:_internal' show VMInternalsForTesting; + import "package:expect/expect.dart"; class C { @@ -15,5 +17,6 @@ bar(c) => c * 4; foo(c) => bar(c); main() { + print(VMInternalsForTesting.randomAddressInsideAllocateObjectStub()); var a = foo(new C()); } diff --git a/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_test.dart b/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_test.dart index d8cb33f0cd8..b85c9927621 100644 --- a/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_test.dart +++ b/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_test.dart @@ -6,9 +6,8 @@ // compile-time will be used at runtime (irrespective if other values were // passed to the runtime). -// OtherResources=use_dwarf_stack_traces_flag_program.dart - import "dart:async"; +import "dart:convert"; import "dart:io"; import 'package:expect/expect.dart'; @@ -38,8 +37,11 @@ main(List args) async { } await withTempDir('dwarf-flag-test', (String tempDir) async { - final cwDir = path.dirname(Platform.script.toFilePath()); - final script = path.join(cwDir, 'use_dwarf_stack_traces_flag_program.dart'); + // We have to use the program in its original location so it can use + // the dart:_internal library (as opposed to adding it as an OtherResources + // option to the test). + final script = path.join(sdkDir, 'runtime', 'tests', 'vm', 'dart', + 'use_dwarf_stack_traces_flag_program.dart'); final scriptDill = path.join(tempDir, 'flag_program.dill'); // Compile script to Kernel IR. @@ -76,123 +78,252 @@ main(List args) async { ]); // Run the resulting Dwarf-AOT compiled script. - final dwarfTrace1 = await runError(aotRuntime, [ - '--dwarf-stack-traces-mode', - scriptDwarfSnapshot, - scriptDill, - ]); - final dwarfTrace2 = await runError(aotRuntime, [ + + final output1 = await runTestProgram(aotRuntime, + ['--dwarf-stack-traces-mode', scriptDwarfSnapshot, scriptDill]); + final output2 = await runTestProgram(aotRuntime, [ '--no-dwarf-stack-traces-mode', scriptDwarfSnapshot, - scriptDill, + scriptDill ]); // Run the resulting non-Dwarf-AOT compiled script. - final nonDwarfTrace1 = await runError(aotRuntime, [ + final nonDwarfTrace1 = (await runTestProgram(aotRuntime, [ '--dwarf-stack-traces-mode', scriptNonDwarfSnapshot, scriptDill, - ]); - final nonDwarfTrace2 = await runError(aotRuntime, [ + ])) + .trace; + final nonDwarfTrace2 = (await runTestProgram(aotRuntime, [ '--no-dwarf-stack-traces-mode', scriptNonDwarfSnapshot, scriptDill, - ]); + ])) + .trace; // Ensure the result is based off the flag passed to gen_snapshot, not // the one passed to the runtime. Expect.deepEquals(nonDwarfTrace1, nonDwarfTrace2); - // For DWARF stack traces, we can't guarantee that the stack traces are - // textually equal on all platforms, but if we retrieve the PC offsets - // out of the stack trace, those should be equal. - final tracePCOffsets1 = collectPCOffsets(dwarfTrace1); - final tracePCOffsets2 = collectPCOffsets(dwarfTrace2); - Expect.deepEquals(tracePCOffsets1, tracePCOffsets2); + // Check with DWARF from separate debugging information. + await compareTraces(nonDwarfTrace1, output1, output2, scriptDwarfDebugInfo); + // Check with DWARF in generated snapshot. + await compareTraces(nonDwarfTrace1, output1, output2, scriptDwarfSnapshot); - // Check that translating the DWARF stack trace (without internal frames) - // matches the symbolic stack trace. - final dwarf = Dwarf.fromFile(scriptDwarfDebugInfo)!; + if (Platform.isLinux || Platform.isMacOS) { + final scriptAssembly = path.join(tempDir, 'dwarf_assembly.S'); + final scriptDwarfAssemblyDebugInfo = + path.join(tempDir, 'dwarf_assembly_info.so'); + final scriptDwarfAssemblySnapshot = + path.join(tempDir, 'dwarf_assembly.so'); + // We get a separate .dSYM bundle on MacOS. + final scriptDwarfAssemblyDebugSnapshot = + scriptDwarfAssemblySnapshot + (Platform.isMacOS ? '.dSYM' : ''); - // Check that build IDs match for traces. - Expect.isNotNull(dwarf.buildId); + await run(genSnapshot, [ + // We test --dwarf-stack-traces-mode, not --dwarf-stack-traces, because + // the latter is a handler that sets the former and also may change + // other flags. This way, we limit the difference between the two + // snapshots and also directly test the flag saved as a VM global flag. + '--dwarf-stack-traces-mode', + '--save-debugging-info=$scriptDwarfAssemblyDebugInfo', + '--snapshot-kind=app-aot-assembly', + '--assembly=$scriptAssembly', + scriptDill, + ]); + + await assembleSnapshot(scriptAssembly, scriptDwarfAssemblySnapshot, + debug: true); + + // Run the resulting Dwarf-AOT compiled script. + final assemblyOutput1 = await runTestProgram(aotRuntime, [ + '--dwarf-stack-traces-mode', + scriptDwarfAssemblySnapshot, + scriptDill, + ]); + final assemblyOutput2 = await runTestProgram(aotRuntime, [ + '--no-dwarf-stack-traces-mode', + scriptDwarfAssemblySnapshot, + scriptDill, + ]); + + // Check with DWARF in assembled snapshot. + await compareTraces(nonDwarfTrace1, assemblyOutput1, assemblyOutput2, + scriptDwarfAssemblyDebugSnapshot, + fromAssembly: true); + // Check with DWARF from separate debugging information. + await compareTraces(nonDwarfTrace1, assemblyOutput1, assemblyOutput2, + scriptDwarfAssemblyDebugInfo, + fromAssembly: true); + } + }); +} + +class DwarfTestOutput { + final List trace; + final int allocateObjectAddress; + + DwarfTestOutput(this.trace, this.allocateObjectAddress); +} + +Future compareTraces(List nonDwarfTrace, DwarfTestOutput output1, + DwarfTestOutput output2, String dwarfPath, + {bool fromAssembly = false}) async { + // For DWARF stack traces, we can't guarantee that the stack traces are + // textually equal on all platforms, but if we retrieve the PC offsets + // out of the stack trace, those should be equal. + final tracePCOffsets1 = collectPCOffsets(output1.trace); + final tracePCOffsets2 = collectPCOffsets(output2.trace); + Expect.deepEquals(tracePCOffsets1, tracePCOffsets2); + + // Check that translating the DWARF stack trace (without internal frames) + // matches the symbolic stack trace. + print("Reading DWARF info from ${dwarfPath}"); + final dwarf = Dwarf.fromFile(dwarfPath); + Expect.isNotNull(dwarf); + + // Check that build IDs match for traces from running ELF snapshots. + if (!fromAssembly) { + Expect.isNotNull(dwarf!.buildId); print('Dwarf build ID: "${dwarf.buildId!}"'); // We should never generate an all-zero build ID. Expect.notEquals(dwarf.buildId, "00000000000000000000000000000000"); // This is a common failure case as well, when HashBitsContainer ends up // hashing over seemingly empty sections. Expect.notEquals(dwarf.buildId, "01000000010000000100000001000000"); - final buildId1 = buildId(dwarfTrace1); + final buildId1 = buildId(output1.trace); Expect.isFalse(buildId1.isEmpty); print('Trace 1 build ID: "${buildId1}"'); Expect.equals(dwarf.buildId, buildId1); - final buildId2 = buildId(dwarfTrace2); + final buildId2 = buildId(output2.trace); Expect.isFalse(buildId2.isEmpty); print('Trace 2 build ID: "${buildId2}"'); Expect.equals(dwarf.buildId, buildId2); + } - final translatedDwarfTrace1 = await Stream.fromIterable(dwarfTrace1) - .transform(DwarfStackTraceDecoder(dwarf)) - .toList(); + final decoder = DwarfStackTraceDecoder(dwarf!); + // Run on the second trace just for the side effect of getting the header + // information so we can get the PC offset of the output address. + await Stream.fromIterable(output2.trace).transform(decoder).toList(); + final allocateObjectPCOffset2 = + decoder.offsetOf(output2.allocateObjectAddress); + final translatedDwarfTrace1 = + await Stream.fromIterable(output1.trace).transform(decoder).toList(); + final allocateObjectPCOffset1 = + decoder.offsetOf(output1.allocateObjectAddress); - final translatedStackFrames = onlySymbolicFrameLines(translatedDwarfTrace1); - final originalStackFrames = onlySymbolicFrameLines(nonDwarfTrace1); + print('Offset of first stub address ' + '0x${output1.allocateObjectAddress.toRadixString(16)}' + ' is $allocateObjectPCOffset1'); + print('Offset of second stub address ' + '0x${output2.allocateObjectAddress.toRadixString(16)}' + ' is $allocateObjectPCOffset2'); - print('Stack frames from translated non-symbolic stack trace:'); - translatedStackFrames.forEach(print); - print(''); + Expect.isNotNull(allocateObjectPCOffset1); + Expect.isNotNull(allocateObjectPCOffset2); + final allocateObjectRelocatedAddress1 = + dwarf.virtualAddressOf(allocateObjectPCOffset1!); + final allocateObjectRelocatedAddress2 = + dwarf.virtualAddressOf(allocateObjectPCOffset2!); - print('Stack frames from original symbolic stack trace:'); - originalStackFrames.forEach(print); - print(''); + final allocateObjectCallInfo1 = dwarf.callInfoFor( + allocateObjectRelocatedAddress1, + includeInternalFrames: true); + final allocateObjectCallInfo2 = dwarf.callInfoFor( + allocateObjectRelocatedAddress2, + includeInternalFrames: true); - Expect.isTrue(translatedStackFrames.length > 0); - Expect.isTrue(originalStackFrames.length > 0); + Expect.isNotNull(allocateObjectCallInfo1); + Expect.isNotNull(allocateObjectCallInfo2); + Expect.equals(allocateObjectCallInfo1!.length, 1); + Expect.equals(allocateObjectCallInfo2!.length, 1); + Expect.isTrue( + allocateObjectCallInfo1.first is StubCallInfo, 'is not a StubCall'); + Expect.isTrue( + allocateObjectCallInfo2.first is StubCallInfo, 'is not a StubCall'); + final stubCall1 = allocateObjectCallInfo1.first as StubCallInfo; + final stubCall2 = allocateObjectCallInfo2.first as StubCallInfo; + Expect.equals(stubCall1.name, stubCall2.name); + Expect.contains('AllocateObject', stubCall1.name); + Expect.contains('AllocateObject', stubCall2.name); - // In symbolic mode, we don't store column information to avoid an increase - // in size of CodeStackMaps. Thus, we need to strip any columns from the - // translated non-symbolic stack to compare them via equality. - final columnStrippedTranslated = removeColumns(translatedStackFrames); + print("Successfully matched AllocateObject stub addresses"); + print(""); - print('Stack frames from translated non-symbolic stack trace, no columns:'); - columnStrippedTranslated.forEach(print); - print(''); + final translatedStackFrames = onlySymbolicFrameLines(translatedDwarfTrace1); + final originalStackFrames = onlySymbolicFrameLines(nonDwarfTrace); - Expect.deepEquals(columnStrippedTranslated, originalStackFrames); + print('Stack frames from translated non-symbolic stack trace:'); + translatedStackFrames.forEach(print); + print(''); - // Since we compiled directly to ELF, there should be a DSO base address - // in the stack trace header and 'virt' markers in the stack frames. + print('Stack frames from original symbolic stack trace:'); + originalStackFrames.forEach(print); + print(''); - // The offsets of absolute addresses from their respective DSO base - // should be the same for both traces. - final dsoBase1 = dsoBaseAddresses(dwarfTrace1).single; - final dsoBase2 = dsoBaseAddresses(dwarfTrace2).single; + Expect.isTrue(translatedStackFrames.length > 0); + Expect.isTrue(originalStackFrames.length > 0); - final absTrace1 = absoluteAddresses(dwarfTrace1); - final absTrace2 = absoluteAddresses(dwarfTrace2); + // In symbolic mode, we don't store column information to avoid an increase + // in size of CodeStackMaps. Thus, we need to strip any columns from the + // translated non-symbolic stack to compare them via equality. + final columnStrippedTranslated = removeColumns(translatedStackFrames); - final relocatedFromDso1 = absTrace1.map((a) => a - dsoBase1); - final relocatedFromDso2 = absTrace2.map((a) => a - dsoBase2); + print('Stack frames from translated non-symbolic stack trace, no columns:'); + columnStrippedTranslated.forEach(print); + print(''); - Expect.deepEquals(relocatedFromDso1, relocatedFromDso2); + Expect.deepEquals(columnStrippedTranslated, originalStackFrames); - // The relocated addresses marked with 'virt' should match between the - // different runs, and they should also match the relocated address - // calculated from the PCOffset for each frame as well as the relocated - // address for each frame calculated using the respective DSO base. - final virtTrace1 = explicitVirtualAddresses(dwarfTrace1); - final virtTrace2 = explicitVirtualAddresses(dwarfTrace2); + // Since we compiled directly to ELF, there should be a DSO base address + // in the stack trace header and 'virt' markers in the stack frames. - Expect.deepEquals(virtTrace1, virtTrace2); + // The offsets of absolute addresses from their respective DSO base + // should be the same for both traces. + final dsoBase1 = dsoBaseAddresses(output1.trace).single; + final dsoBase2 = dsoBaseAddresses(output2.trace).single; - Expect.deepEquals( - virtTrace1, tracePCOffsets1.map((o) => o.virtualAddressIn(dwarf))); - Expect.deepEquals( - virtTrace2, tracePCOffsets2.map((o) => o.virtualAddressIn(dwarf))); + final absTrace1 = absoluteAddresses(output1.trace); + final absTrace2 = absoluteAddresses(output2.trace); - Expect.deepEquals(virtTrace1, relocatedFromDso1); - Expect.deepEquals(virtTrace2, relocatedFromDso2); - }); + final relocatedFromDso1 = absTrace1.map((a) => a - dsoBase1); + final relocatedFromDso2 = absTrace2.map((a) => a - dsoBase2); + + Expect.deepEquals(relocatedFromDso1, relocatedFromDso2); + + // We don't print 'virt' relocated addresses when running assembled snapshots. + if (fromAssembly) return; + + // The relocated addresses marked with 'virt' should match between the + // different runs, and they should also match the relocated address + // calculated from the PCOffset for each frame as well as the relocated + // address for each frame calculated using the respective DSO base. + final virtTrace1 = explicitVirtualAddresses(output1.trace); + final virtTrace2 = explicitVirtualAddresses(output2.trace); + + Expect.deepEquals(virtTrace1, virtTrace2); + + Expect.deepEquals( + virtTrace1, tracePCOffsets1.map((o) => o.virtualAddressIn(dwarf))); + Expect.deepEquals( + virtTrace2, tracePCOffsets2.map((o) => o.virtualAddressIn(dwarf))); + + Expect.deepEquals(virtTrace1, relocatedFromDso1); + Expect.deepEquals(virtTrace2, relocatedFromDso2); +} + +Future runTestProgram( + String executable, List args) async { + final result = await runHelper(executable, args); + + if (result.exitCode == 0) { + throw 'Command did not fail with non-zero exit code'; + } + Expect.isTrue(result.stdout.isNotEmpty); + Expect.isTrue(result.stderr.isNotEmpty); + + return DwarfTestOutput( + LineSplitter.split(result.stderr).toList(), int.parse(result.stdout)); } final _buildIdRE = RegExp(r"build_id: '([a-f\d]+)'"); diff --git a/runtime/tests/vm/dart/use_flag_test_helper.dart b/runtime/tests/vm/dart/use_flag_test_helper.dart index 431fce51195..5cd09d9ab53 100644 --- a/runtime/tests/vm/dart/use_flag_test_helper.dart +++ b/runtime/tests/vm/dart/use_flag_test_helper.dart @@ -38,7 +38,8 @@ String? get clangBuildToolsDir { return Directory(clangDir).existsSync() ? clangDir : null; } -Future assembleSnapshot(String assemblyPath, String snapshotPath) async { +Future assembleSnapshot(String assemblyPath, String snapshotPath, + {bool debug = false}) async { if (!Platform.isLinux && !Platform.isMacOS) { throw "Unsupported platform ${Platform.operatingSystem} for assembling"; } @@ -62,6 +63,11 @@ Future assembleSnapshot(String assemblyPath, String snapshotPath) async { if (Platform.isMacOS) { shared = '-dynamiclib'; + if (buildDir.endsWith('ARM64')) { + // ld: dynamic main executables must link with libSystem.dylib for + // architecture arm64 + ldFlags.add('-lSystem'); + } // Tell Mac linker to give up generating eh_frame from dwarf. ldFlags.add('-Wl,-no_compact_unwind'); } else if (buildDir.endsWith('SIMARM')) { @@ -73,6 +79,9 @@ Future assembleSnapshot(String assemblyPath, String snapshotPath) async { if (buildDir.endsWith('X64') || buildDir.endsWith('SIMARM64')) { ccFlags.add('-m64'); } + if (debug) { + ccFlags.add('-g'); + } await run(cc, [ ...ccFlags, @@ -158,9 +167,7 @@ Future> runOutput(String executable, List args) async { Expect.isTrue(result.stdout.isNotEmpty); Expect.isTrue(result.stderr.isEmpty); - return await Stream.value(result.stdout as String) - .transform(const LineSplitter()) - .toList(); + return LineSplitter.split(result.stdout).toList(growable: false); } Future> runError(String executable, List args) async { @@ -172,9 +179,7 @@ Future> runError(String executable, List args) async { Expect.isTrue(result.stdout.isEmpty); Expect.isTrue(result.stderr.isNotEmpty); - return await Stream.value(result.stderr as String) - .transform(const LineSplitter()) - .toList(); + return LineSplitter.split(result.stderr).toList(growable: false); } const keepTempKey = 'KEEP_TEMPORARY_DIRECTORIES'; diff --git a/runtime/tests/vm/dart/use_resolve_dwarf_paths_flag_test.dart b/runtime/tests/vm/dart/use_resolve_dwarf_paths_flag_test.dart index 30d90d5df43..9962f9738b3 100644 --- a/runtime/tests/vm/dart/use_resolve_dwarf_paths_flag_test.dart +++ b/runtime/tests/vm/dart/use_resolve_dwarf_paths_flag_test.dart @@ -5,7 +5,7 @@ // This test checks that --resolve-dwarf-paths outputs absolute and relative // paths in DWARF information. -// OtherResources=use_dwarf_stack_traces_flag_program.dart +// OtherResources=use_save_debugging_info_flag_program.dart import "dart:async"; import "dart:io"; @@ -38,7 +38,8 @@ main(List args) async { await withTempDir('dwarf-flag-test', (String tempDir) async { final cwDir = path.dirname(Platform.script.toFilePath()); - final script = path.join(cwDir, 'use_dwarf_stack_traces_flag_program.dart'); + final script = + path.join(cwDir, 'use_save_debugging_info_flag_program.dart'); final scriptDill = path.join(tempDir, 'flag_program.dill'); // Compile script to Kernel IR. diff --git a/runtime/tests/vm/dart/use_save_debugging_info_flag_program.dart b/runtime/tests/vm/dart/use_save_debugging_info_flag_program.dart new file mode 100644 index 00000000000..51d3c4bd491 --- /dev/null +++ b/runtime/tests/vm/dart/use_save_debugging_info_flag_program.dart @@ -0,0 +1,19 @@ +// Copyright (c) 2022, 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. +// Test that the full stacktrace in an error object matches the stacktrace +// handed to the catch clause. + +import "package:expect/expect.dart"; + +class C { + // operator*(o) is missing to trigger a noSuchMethodError when a C object + // is used in the multiplication below. +} + +bar(c) => c * 4; +foo(c) => bar(c); + +main() { + var a = foo(new C()); +} diff --git a/runtime/tests/vm/dart/use_save_debugging_info_flag_test.dart b/runtime/tests/vm/dart/use_save_debugging_info_flag_test.dart index 2bd55a27c01..f79ada16747 100644 --- a/runtime/tests/vm/dart/use_save_debugging_info_flag_test.dart +++ b/runtime/tests/vm/dart/use_save_debugging_info_flag_test.dart @@ -6,7 +6,7 @@ // for stripped ELF output, and that using the debugging information to look // up stripped stack trace information matches the non-stripped version. -// OtherResources=use_dwarf_stack_traces_flag_program.dart +// OtherResources=use_save_debugging_info_flag_program.dart import "dart:io"; import "dart:math"; @@ -41,8 +41,8 @@ main(List args) async { await withTempDir('save-debug-info-flag-test', (String tempDir) async { final cwDir = path.dirname(Platform.script.toFilePath()); - // We can just reuse the program for the use_dwarf_stack_traces test. - final script = path.join(cwDir, 'use_dwarf_stack_traces_flag_program.dart'); + final script = + path.join(cwDir, 'use_save_debugging_info_flag_program.dart'); final scriptDill = path.join(tempDir, 'flag_program.dill'); // Compile script to Kernel IR. diff --git a/runtime/tests/vm/dart/use_strip_flag_test.dart b/runtime/tests/vm/dart/use_strip_flag_test.dart index 5025c84e170..6dbd53b0c41 100644 --- a/runtime/tests/vm/dart/use_strip_flag_test.dart +++ b/runtime/tests/vm/dart/use_strip_flag_test.dart @@ -6,7 +6,7 @@ // ELF and assembly output. This test is currently very weak, in that it just // checks that the stripped version is strictly smaller than the unstripped one. -// OtherResources=use_dwarf_stack_traces_flag_program.dart +// OtherResources=use_save_debugging_info_flag_program.dart import "dart:io"; @@ -34,8 +34,9 @@ main(List args) async { await withTempDir('strip-flag-test', (String tempDir) async { final cwDir = path.dirname(Platform.script.toFilePath()); - // We can just reuse the program for the use_dwarf_stack_traces test. - final script = path.join(cwDir, 'use_dwarf_stack_traces_flag_program.dart'); + // We can just reuse the program for the use_save_debugging_info_flag test. + final script = + path.join(cwDir, 'use_save_debugging_info_flag_program.dart'); final scriptDill = path.join(tempDir, 'flag_program.dill'); // Compile script to Kernel IR. diff --git a/runtime/tests/vm/dart/use_trace_precompiler_flag_test.dart b/runtime/tests/vm/dart/use_trace_precompiler_flag_test.dart index 7ea2d60de37..8d9123fe58f 100644 --- a/runtime/tests/vm/dart/use_trace_precompiler_flag_test.dart +++ b/runtime/tests/vm/dart/use_trace_precompiler_flag_test.dart @@ -5,7 +5,7 @@ // This test ensures that --trace-precompiler runs without issue and prints // valid JSON for reasons to retain objects. -// OtherResources=use_dwarf_stack_traces_flag_program.dart +// OtherResources=use_save_debugging_info_flag_program.dart import "dart:convert"; import "dart:io"; @@ -37,8 +37,9 @@ main(List args) async { await withTempDir('trace-precompiler-flag-test', (String tempDir) async { final cwDir = path.dirname(Platform.script.toFilePath()); - // We can just reuse the program for the use_dwarf_stack_traces test. - final script = path.join(cwDir, 'use_dwarf_stack_traces_flag_program.dart'); + // We can just reuse the program for the use_save_debugging_info_flag test. + final script = + path.join(cwDir, 'use_save_debugging_info_flag_program.dart'); final scriptDill = path.join(tempDir, 'flag_program.dill'); // Compile script to Kernel IR. diff --git a/runtime/tests/vm/dart_2/use_dwarf_stack_traces_flag_program.dart b/runtime/tests/vm/dart_2/use_dwarf_stack_traces_flag_program.dart index a4e399ed0fa..8e6838fefa4 100644 --- a/runtime/tests/vm/dart_2/use_dwarf_stack_traces_flag_program.dart +++ b/runtime/tests/vm/dart_2/use_dwarf_stack_traces_flag_program.dart @@ -6,6 +6,8 @@ // @dart = 2.9 +import 'dart:_internal' show VMInternalsForTesting; + import "package:expect/expect.dart"; class C { @@ -17,5 +19,6 @@ bar(c) => c * 4; foo(c) => bar(c); main() { + print(VMInternalsForTesting.randomAddressInsideAllocateObjectStub()); var a = foo(new C()); } diff --git a/runtime/tests/vm/dart_2/use_dwarf_stack_traces_flag_test.dart b/runtime/tests/vm/dart_2/use_dwarf_stack_traces_flag_test.dart index 5850250145f..7bd3716dfa9 100644 --- a/runtime/tests/vm/dart_2/use_dwarf_stack_traces_flag_test.dart +++ b/runtime/tests/vm/dart_2/use_dwarf_stack_traces_flag_test.dart @@ -8,9 +8,8 @@ // compile-time will be used at runtime (irrespective if other values were // passed to the runtime). -// OtherResources=use_dwarf_stack_traces_flag_program.dart - import "dart:async"; +import "dart:convert"; import "dart:io"; import 'package:expect/expect.dart'; @@ -40,8 +39,11 @@ main(List args) async { } await withTempDir('dwarf-flag-test', (String tempDir) async { - final cwDir = path.dirname(Platform.script.toFilePath()); - final script = path.join(cwDir, 'use_dwarf_stack_traces_flag_program.dart'); + // We have to use the program in its original location so it can use + // the dart:_internal library (as opposed to adding it as an OtherResources + // option to the test). + final script = path.join(sdkDir, 'runtime', 'tests', 'vm', 'dart', + 'use_dwarf_stack_traces_flag_program.dart'); final scriptDill = path.join(tempDir, 'flag_program.dill'); // Compile script to Kernel IR. @@ -78,46 +80,112 @@ main(List args) async { ]); // Run the resulting Dwarf-AOT compiled script. - final dwarfTrace1 = await runError(aotRuntime, [ - '--dwarf-stack-traces-mode', - scriptDwarfSnapshot, - scriptDill, - ]); - final dwarfTrace2 = await runError(aotRuntime, [ + + final output1 = await runTestProgram(aotRuntime, + ['--dwarf-stack-traces-mode', scriptDwarfSnapshot, scriptDill]); + final output2 = await runTestProgram(aotRuntime, [ '--no-dwarf-stack-traces-mode', scriptDwarfSnapshot, - scriptDill, + scriptDill ]); // Run the resulting non-Dwarf-AOT compiled script. - final nonDwarfTrace1 = await runError(aotRuntime, [ + final nonDwarfTrace1 = (await runTestProgram(aotRuntime, [ '--dwarf-stack-traces-mode', scriptNonDwarfSnapshot, scriptDill, - ]); - final nonDwarfTrace2 = await runError(aotRuntime, [ + ])) + .trace; + final nonDwarfTrace2 = (await runTestProgram(aotRuntime, [ '--no-dwarf-stack-traces-mode', scriptNonDwarfSnapshot, scriptDill, - ]); + ])) + .trace; // Ensure the result is based off the flag passed to gen_snapshot, not // the one passed to the runtime. Expect.deepEquals(nonDwarfTrace1, nonDwarfTrace2); - // For DWARF stack traces, we can't guarantee that the stack traces are - // textually equal on all platforms, but if we retrieve the PC offsets - // out of the stack trace, those should be equal. - final tracePCOffsets1 = collectPCOffsets(dwarfTrace1); - final tracePCOffsets2 = collectPCOffsets(dwarfTrace2); - Expect.deepEquals(tracePCOffsets1, tracePCOffsets2); + // Check with DWARF from separate debugging information. + await compareTraces(nonDwarfTrace1, output1, output2, scriptDwarfDebugInfo); + // Check with DWARF in generated snapshot. + await compareTraces(nonDwarfTrace1, output1, output2, scriptDwarfSnapshot); - // Check that translating the DWARF stack trace (without internal frames) - // matches the symbolic stack trace. - final dwarf = Dwarf.fromFile(scriptDwarfDebugInfo); - Expect.isNotNull(dwarf); + if (Platform.isLinux || Platform.isMacOS) { + final scriptAssembly = path.join(tempDir, 'dwarf_assembly.S'); + final scriptDwarfAssemblyDebugInfo = + path.join(tempDir, 'dwarf_assembly_info.so'); + final scriptDwarfAssemblySnapshot = + path.join(tempDir, 'dwarf_assembly.so'); + // We get a separate .dSYM bundle on MacOS. + final scriptDwarfAssemblyDebugSnapshot = + scriptDwarfAssemblySnapshot + (Platform.isMacOS ? '.dSYM' : ''); - // Check that build IDs match for traces. + await run(genSnapshot, [ + // We test --dwarf-stack-traces-mode, not --dwarf-stack-traces, because + // the latter is a handler that sets the former and also may change + // other flags. This way, we limit the difference between the two + // snapshots and also directly test the flag saved as a VM global flag. + '--dwarf-stack-traces-mode', + '--save-debugging-info=$scriptDwarfAssemblyDebugInfo', + '--snapshot-kind=app-aot-assembly', + '--assembly=$scriptAssembly', + scriptDill, + ]); + + await assembleSnapshot(scriptAssembly, scriptDwarfAssemblySnapshot, + debug: true); + + // Run the resulting Dwarf-AOT compiled script. + final assemblyOutput1 = await runTestProgram(aotRuntime, [ + '--dwarf-stack-traces-mode', + scriptDwarfAssemblySnapshot, + scriptDill, + ]); + final assemblyOutput2 = await runTestProgram(aotRuntime, [ + '--no-dwarf-stack-traces-mode', + scriptDwarfAssemblySnapshot, + scriptDill, + ]); + + // Check with DWARF in assembled snapshot. + await compareTraces(nonDwarfTrace1, assemblyOutput1, assemblyOutput2, + scriptDwarfAssemblyDebugSnapshot, + fromAssembly: true); + // Check with DWARF from separate debugging information. + await compareTraces(nonDwarfTrace1, assemblyOutput1, assemblyOutput2, + scriptDwarfAssemblyDebugInfo, + fromAssembly: true); + } + }); +} + +class DwarfTestOutput { + final List trace; + final int allocateObjectAddress; + + DwarfTestOutput(this.trace, this.allocateObjectAddress); +} + +Future compareTraces(List nonDwarfTrace, DwarfTestOutput output1, + DwarfTestOutput output2, String dwarfPath, + {bool fromAssembly = false}) async { + // For DWARF stack traces, we can't guarantee that the stack traces are + // textually equal on all platforms, but if we retrieve the PC offsets + // out of the stack trace, those should be equal. + final tracePCOffsets1 = collectPCOffsets(output1.trace); + final tracePCOffsets2 = collectPCOffsets(output2.trace); + Expect.deepEquals(tracePCOffsets1, tracePCOffsets2); + + // Check that translating the DWARF stack trace (without internal frames) + // matches the symbolic stack trace. + print("Reading DWARF info from ${dwarfPath}"); + final dwarf = Dwarf.fromFile(dwarfPath); + Expect.isNotNull(dwarf); + + // Check that build IDs match for traces from running ELF snapshots. + if (!fromAssembly) { Expect.isNotNull(dwarf.buildId); print('Dwarf build ID: "${dwarf.buildId}"'); // We should never generate an all-zero build ID. @@ -125,77 +193,139 @@ main(List args) async { // This is a common failure case as well, when HashBitsContainer ends up // hashing over seemingly empty sections. Expect.notEquals(dwarf.buildId, "01000000010000000100000001000000"); - final buildId1 = buildId(dwarfTrace1); + final buildId1 = buildId(output1.trace); Expect.isFalse(buildId1.isEmpty); print('Trace 1 build ID: "${buildId1}"'); Expect.equals(dwarf.buildId, buildId1); - final buildId2 = buildId(dwarfTrace2); + final buildId2 = buildId(output2.trace); Expect.isFalse(buildId2.isEmpty); print('Trace 2 build ID: "${buildId2}"'); Expect.equals(dwarf.buildId, buildId2); + } - final translatedDwarfTrace1 = await Stream.fromIterable(dwarfTrace1) - .transform(DwarfStackTraceDecoder(dwarf)) - .toList(); + final decoder = DwarfStackTraceDecoder(dwarf); + // Run on the second trace just for the side effect of getting the header + // information so we can get the PC offset of the output address. + await Stream.fromIterable(output2.trace).transform(decoder).toList(); + final allocateObjectPCOffset2 = + decoder.offsetOf(output2.allocateObjectAddress); + final translatedDwarfTrace1 = + await Stream.fromIterable(output1.trace).transform(decoder).toList(); + final allocateObjectPCOffset1 = + decoder.offsetOf(output1.allocateObjectAddress); - final translatedStackFrames = onlySymbolicFrameLines(translatedDwarfTrace1); - final originalStackFrames = onlySymbolicFrameLines(nonDwarfTrace1); + print('Offset of first stub address ' + '0x${output1.allocateObjectAddress.toRadixString(16)}' + ' is $allocateObjectPCOffset1'); + print('Offset of second stub address ' + '0x${output2.allocateObjectAddress.toRadixString(16)}' + ' is $allocateObjectPCOffset2'); - print('Stack frames from translated non-symbolic stack trace:'); - translatedStackFrames.forEach(print); - print(''); + Expect.isNotNull(allocateObjectPCOffset1); + Expect.isNotNull(allocateObjectPCOffset2); + final allocateObjectRelocatedAddress1 = + dwarf.virtualAddressOf(allocateObjectPCOffset1); + final allocateObjectRelocatedAddress2 = + dwarf.virtualAddressOf(allocateObjectPCOffset2); - print('Stack frames from original symbolic stack trace:'); - originalStackFrames.forEach(print); - print(''); + final allocateObjectCallInfo1 = dwarf.callInfoFor( + allocateObjectRelocatedAddress1, + includeInternalFrames: true); + final allocateObjectCallInfo2 = dwarf.callInfoFor( + allocateObjectRelocatedAddress2, + includeInternalFrames: true); - Expect.isTrue(translatedStackFrames.length > 0); - Expect.isTrue(originalStackFrames.length > 0); + Expect.isNotNull(allocateObjectCallInfo1); + Expect.isNotNull(allocateObjectCallInfo2); + Expect.equals(allocateObjectCallInfo1.length, 1); + Expect.equals(allocateObjectCallInfo2.length, 1); + Expect.isTrue( + allocateObjectCallInfo1.first is StubCallInfo, 'is not a StubCall'); + Expect.isTrue( + allocateObjectCallInfo2.first is StubCallInfo, 'is not a StubCall'); + final stubCall1 = allocateObjectCallInfo1.first as StubCallInfo; + final stubCall2 = allocateObjectCallInfo2.first as StubCallInfo; + Expect.equals(stubCall1.name, stubCall2.name); + Expect.contains('AllocateObject', stubCall1.name); + Expect.contains('AllocateObject', stubCall2.name); - // In symbolic mode, we don't store column information to avoid an increase - // in size of CodeStackMaps. Thus, we need to strip any columns from the - // translated non-symbolic stack to compare them via equality. - final columnStrippedTranslated = removeColumns(translatedStackFrames); + print("Successfully matched AllocateObject stub addresses"); + print(""); - print('Stack frames from translated non-symbolic stack trace, no columns:'); - columnStrippedTranslated.forEach(print); - print(''); + final translatedStackFrames = onlySymbolicFrameLines(translatedDwarfTrace1); + final originalStackFrames = onlySymbolicFrameLines(nonDwarfTrace); - Expect.deepEquals(columnStrippedTranslated, originalStackFrames); + print('Stack frames from translated non-symbolic stack trace:'); + translatedStackFrames.forEach(print); + print(''); - // Since we compiled directly to ELF, there should be a DSO base address - // in the stack trace header and 'virt' markers in the stack frames. + print('Stack frames from original symbolic stack trace:'); + originalStackFrames.forEach(print); + print(''); - // The offsets of absolute addresses from their respective DSO base - // should be the same for both traces. - final dsoBase1 = dsoBaseAddresses(dwarfTrace1).single; - final dsoBase2 = dsoBaseAddresses(dwarfTrace2).single; + Expect.isTrue(translatedStackFrames.length > 0); + Expect.isTrue(originalStackFrames.length > 0); - final absTrace1 = absoluteAddresses(dwarfTrace1); - final absTrace2 = absoluteAddresses(dwarfTrace2); + // In symbolic mode, we don't store column information to avoid an increase + // in size of CodeStackMaps. Thus, we need to strip any columns from the + // translated non-symbolic stack to compare them via equality. + final columnStrippedTranslated = removeColumns(translatedStackFrames); - final relocatedFromDso1 = absTrace1.map((a) => a - dsoBase1); - final relocatedFromDso2 = absTrace2.map((a) => a - dsoBase2); + print('Stack frames from translated non-symbolic stack trace, no columns:'); + columnStrippedTranslated.forEach(print); + print(''); - Expect.deepEquals(relocatedFromDso1, relocatedFromDso2); + Expect.deepEquals(columnStrippedTranslated, originalStackFrames); - // The relocated addresses marked with 'virt' should match between the - // different runs, and they should also match the relocated address - // calculated from the PCOffset for each frame as well as the relocated - // address for each frame calculated using the respective DSO base. - final virtTrace1 = explicitVirtualAddresses(dwarfTrace1); - final virtTrace2 = explicitVirtualAddresses(dwarfTrace2); + // Since we compiled directly to ELF, there should be a DSO base address + // in the stack trace header and 'virt' markers in the stack frames. - Expect.deepEquals(virtTrace1, virtTrace2); + // The offsets of absolute addresses from their respective DSO base + // should be the same for both traces. + final dsoBase1 = dsoBaseAddresses(output1.trace).single; + final dsoBase2 = dsoBaseAddresses(output2.trace).single; - Expect.deepEquals( - virtTrace1, tracePCOffsets1.map((o) => o.virtualAddressIn(dwarf))); - Expect.deepEquals( - virtTrace2, tracePCOffsets2.map((o) => o.virtualAddressIn(dwarf))); + final absTrace1 = absoluteAddresses(output1.trace); + final absTrace2 = absoluteAddresses(output2.trace); - Expect.deepEquals(virtTrace1, relocatedFromDso1); - Expect.deepEquals(virtTrace2, relocatedFromDso2); - }); + final relocatedFromDso1 = absTrace1.map((a) => a - dsoBase1); + final relocatedFromDso2 = absTrace2.map((a) => a - dsoBase2); + + Expect.deepEquals(relocatedFromDso1, relocatedFromDso2); + + // We don't print 'virt' relocated addresses when running assembled snapshots. + if (fromAssembly) return; + + // The relocated addresses marked with 'virt' should match between the + // different runs, and they should also match the relocated address + // calculated from the PCOffset for each frame as well as the relocated + // address for each frame calculated using the respective DSO base. + final virtTrace1 = explicitVirtualAddresses(output1.trace); + final virtTrace2 = explicitVirtualAddresses(output2.trace); + + Expect.deepEquals(virtTrace1, virtTrace2); + + Expect.deepEquals( + virtTrace1, tracePCOffsets1.map((o) => o.virtualAddressIn(dwarf))); + Expect.deepEquals( + virtTrace2, tracePCOffsets2.map((o) => o.virtualAddressIn(dwarf))); + + Expect.deepEquals(virtTrace1, relocatedFromDso1); + Expect.deepEquals(virtTrace2, relocatedFromDso2); +} + +Future runTestProgram( + String executable, List args) async { + final result = await runHelper(executable, args); + + if (result.exitCode == 0) { + throw 'Command did not fail with non-zero exit code'; + } + Expect.isTrue(result.stdout.isNotEmpty); + Expect.isTrue(result.stderr.isNotEmpty); + + return DwarfTestOutput( + LineSplitter.split(result.stderr).toList(), int.parse(result.stdout)); } final _buildIdRE = RegExp(r"build_id: '([a-f\d]+)'"); diff --git a/runtime/tests/vm/dart_2/use_flag_test_helper.dart b/runtime/tests/vm/dart_2/use_flag_test_helper.dart index b408f42adb5..8fdf89bad54 100644 --- a/runtime/tests/vm/dart_2/use_flag_test_helper.dart +++ b/runtime/tests/vm/dart_2/use_flag_test_helper.dart @@ -40,7 +40,8 @@ String get clangBuildToolsDir { return Directory(clangDir).existsSync() ? clangDir : null; } -Future assembleSnapshot(String assemblyPath, String snapshotPath) async { +Future assembleSnapshot(String assemblyPath, String snapshotPath, + {bool debug = false}) async { if (!Platform.isLinux && !Platform.isMacOS) { throw "Unsupported platform ${Platform.operatingSystem} for assembling"; } @@ -63,6 +64,11 @@ Future assembleSnapshot(String assemblyPath, String snapshotPath) async { if (Platform.isMacOS) { shared = '-dynamiclib'; + if (buildDir.endsWith('ARM64')) { + // ld: dynamic main executables must link with libSystem.dylib for + // architecture arm64 + ldFlags.add('-lSystem'); + } // Tell Mac linker to give up generating eh_frame from dwarf. ldFlags.add('-Wl,-no_compact_unwind'); } else if (buildDir.endsWith('SIMARM')) { @@ -74,6 +80,9 @@ Future assembleSnapshot(String assemblyPath, String snapshotPath) async { if (buildDir.endsWith('X64') || buildDir.endsWith('SIMARM64')) { ccFlags.add('-m64'); } + if (debug) { + ccFlags.add('-g'); + } await run(cc, [ ...ccFlags, @@ -158,9 +167,7 @@ Future> runOutput(String executable, List args) async { Expect.isTrue(result.stdout.isNotEmpty); Expect.isTrue(result.stderr.isEmpty); - return await Stream.value(result.stdout as String) - .transform(const LineSplitter()) - .toList(); + return LineSplitter.split(result.stdout).toList(growable: false); } Future> runError(String executable, List args) async { @@ -172,9 +179,7 @@ Future> runError(String executable, List args) async { Expect.isTrue(result.stdout.isEmpty); Expect.isTrue(result.stderr.isNotEmpty); - return await Stream.value(result.stderr as String) - .transform(const LineSplitter()) - .toList(); + return LineSplitter.split(result.stderr).toList(growable: false); } const keepTempKey = 'KEEP_TEMPORARY_DIRECTORIES'; diff --git a/runtime/tests/vm/dart_2/use_resolve_dwarf_paths_flag_test.dart b/runtime/tests/vm/dart_2/use_resolve_dwarf_paths_flag_test.dart index cd0ed3943d5..1f9df8d2506 100644 --- a/runtime/tests/vm/dart_2/use_resolve_dwarf_paths_flag_test.dart +++ b/runtime/tests/vm/dart_2/use_resolve_dwarf_paths_flag_test.dart @@ -7,7 +7,7 @@ // This test checks that --resolve-dwarf-paths outputs absolute and relative // paths in DWARF information. -// OtherResources=use_dwarf_stack_traces_flag_program.dart +// OtherResources=use_save_debugging_info_flag_program.dart import "dart:async"; import "dart:io"; @@ -40,7 +40,8 @@ main(List args) async { await withTempDir('dwarf-flag-test', (String tempDir) async { final cwDir = path.dirname(Platform.script.toFilePath()); - final script = path.join(cwDir, 'use_dwarf_stack_traces_flag_program.dart'); + final script = + path.join(cwDir, 'use_save_debugging_info_flag_program.dart'); final scriptDill = path.join(tempDir, 'flag_program.dill'); // Compile script to Kernel IR. diff --git a/runtime/tests/vm/dart_2/use_save_debugging_info_flag_program.dart b/runtime/tests/vm/dart_2/use_save_debugging_info_flag_program.dart new file mode 100644 index 00000000000..c01efdbb09b --- /dev/null +++ b/runtime/tests/vm/dart_2/use_save_debugging_info_flag_program.dart @@ -0,0 +1,21 @@ +// Copyright (c) 2022, 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. +// Test that the full stacktrace in an error object matches the stacktrace +// handed to the catch clause. + +// @dart = 2.9 + +import "package:expect/expect.dart"; + +class C { + // operator*(o) is missing to trigger a noSuchMethodError when a C object + // is used in the multiplication below. +} + +bar(c) => c * 4; +foo(c) => bar(c); + +main() { + var a = foo(new C()); +} diff --git a/runtime/tests/vm/dart_2/use_save_debugging_info_flag_test.dart b/runtime/tests/vm/dart_2/use_save_debugging_info_flag_test.dart index db548f44ab4..ddee6b2ed66 100644 --- a/runtime/tests/vm/dart_2/use_save_debugging_info_flag_test.dart +++ b/runtime/tests/vm/dart_2/use_save_debugging_info_flag_test.dart @@ -8,7 +8,7 @@ // for stripped ELF output, and that using the debugging information to look // up stripped stack trace information matches the non-stripped version. -// OtherResources=use_dwarf_stack_traces_flag_program.dart +// OtherResources=use_save_debugging_info_flag_program.dart import "dart:io"; import "dart:math"; @@ -43,8 +43,8 @@ main(List args) async { await withTempDir('save-debug-info-flag-test', (String tempDir) async { final cwDir = path.dirname(Platform.script.toFilePath()); - // We can just reuse the program for the use_dwarf_stack_traces test. - final script = path.join(cwDir, 'use_dwarf_stack_traces_flag_program.dart'); + final script = + path.join(cwDir, 'use_save_debugging_info_flag_program.dart'); final scriptDill = path.join(tempDir, 'flag_program.dill'); // Compile script to Kernel IR. diff --git a/runtime/tests/vm/dart_2/use_strip_flag_test.dart b/runtime/tests/vm/dart_2/use_strip_flag_test.dart index a552ea0b9af..e145e86c007 100644 --- a/runtime/tests/vm/dart_2/use_strip_flag_test.dart +++ b/runtime/tests/vm/dart_2/use_strip_flag_test.dart @@ -8,7 +8,7 @@ // ELF and assembly output. This test is currently very weak, in that it just // checks that the stripped version is strictly smaller than the unstripped one. -// OtherResources=use_dwarf_stack_traces_flag_program.dart +// OtherResources=use_save_debugging_info_flag_program.dart import "dart:io"; @@ -36,8 +36,9 @@ main(List args) async { await withTempDir('strip-flag-test', (String tempDir) async { final cwDir = path.dirname(Platform.script.toFilePath()); - // We can just reuse the program for the use_dwarf_stack_traces test. - final script = path.join(cwDir, 'use_dwarf_stack_traces_flag_program.dart'); + // We can just reuse the program for the use_save_debugging_info_flag test. + final script = + path.join(cwDir, 'use_save_debugging_info_flag_program.dart'); final scriptDill = path.join(tempDir, 'flag_program.dill'); // Compile script to Kernel IR. diff --git a/runtime/tests/vm/dart_2/use_trace_precompiler_flag_test.dart b/runtime/tests/vm/dart_2/use_trace_precompiler_flag_test.dart index 4a054e875f5..0cb879aff38 100644 --- a/runtime/tests/vm/dart_2/use_trace_precompiler_flag_test.dart +++ b/runtime/tests/vm/dart_2/use_trace_precompiler_flag_test.dart @@ -7,7 +7,7 @@ // This test ensures that --trace-precompiler runs without issue and prints // valid JSON for reasons to retain objects. -// OtherResources=use_dwarf_stack_traces_flag_program.dart +// OtherResources=use_save_debugging_info_flag_program.dart import "dart:convert"; import "dart:io"; @@ -39,8 +39,9 @@ main(List args) async { await withTempDir('trace-precompiler-flag-test', (String tempDir) async { final cwDir = path.dirname(Platform.script.toFilePath()); - // We can just reuse the program for the use_dwarf_stack_traces test. - final script = path.join(cwDir, 'use_dwarf_stack_traces_flag_program.dart'); + // We can just reuse the program for the use_save_debugging_info_flag test. + final script = + path.join(cwDir, 'use_save_debugging_info_flag_program.dart'); final scriptDill = path.join(tempDir, 'flag_program.dill'); // Compile script to Kernel IR. diff --git a/runtime/vm/bootstrap_natives.h b/runtime/vm/bootstrap_natives.h index 6e826798142..e9bc548d64a 100644 --- a/runtime/vm/bootstrap_natives.h +++ b/runtime/vm/bootstrap_natives.h @@ -330,6 +330,7 @@ namespace dart { V(Internal_writeIntoOneByteString, 3) \ V(Internal_writeIntoTwoByteString, 3) \ V(Internal_deoptimizeFunctionsOnStack, 0) \ + V(Internal_randomAddressInsideAllocateObjectStub, 0) \ V(InvocationMirror_unpackTypeArguments, 2) \ V(NoSuchMethodError_existingMethodSignature, 3) \ V(Uri_isWindowsPlatform, 0) \ diff --git a/sdk/lib/_internal/vm/lib/internal_patch.dart b/sdk/lib/_internal/vm/lib/internal_patch.dart index c02b2f80903..934db516e3d 100644 --- a/sdk/lib/_internal/vm/lib/internal_patch.dart +++ b/sdk/lib/_internal/vm/lib/internal_patch.dart @@ -179,6 +179,10 @@ abstract class VMInternalsForTesting { @pragma("vm:external-name", "Internal_deoptimizeFunctionsOnStack") external static void deoptimizeFunctionsOnStack(); + + // Used to verify that PC addresses in stubs can be named using DWARF info. + @pragma("vm:external-name", "Internal_randomAddressInsideAllocateObjectStub") + external static int randomAddressInsideAllocateObjectStub(); } @patch