// 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:collection'; import 'dart:math'; import 'dart:typed_data'; import 'constants.dart' as constants; import 'elf.dart'; import 'reader.dart'; int _initialLengthValue(Reader reader) { final length = reader.readBytes(4); if (length == 0xffffffff) { throw FormatException("64-bit DWARF format detected"); } else if (length > 0xfffffff0) { throw FormatException("Unrecognized reserved initial length value"); } return length; } enum _Tag { compileUnit, inlinedSubroutine, subprogram, } const _tags = { 0x11: _Tag.compileUnit, 0x1d: _Tag.inlinedSubroutine, 0x2e: _Tag.subprogram, }; const _tagStrings = <_Tag, String>{ _Tag.compileUnit: "DW_TAG_compile_unit", _Tag.inlinedSubroutine: "DW_TAG_inlined_subroutine", _Tag.subprogram: "DW_TAG_subroutine", }; enum _AttributeName { abstractOrigin, callColumn, callFile, callLine, compilationDirectory, declarationColumn, declarationFile, declarationLine, highProgramCounter, lowProgramCounter, inline, name, producer, sibling, statementList, } const _attributeNames = { 0x01: _AttributeName.sibling, 0x03: _AttributeName.name, 0x10: _AttributeName.statementList, 0x11: _AttributeName.lowProgramCounter, 0x12: _AttributeName.highProgramCounter, 0x1b: _AttributeName.compilationDirectory, 0x20: _AttributeName.inline, 0x25: _AttributeName.producer, 0x31: _AttributeName.abstractOrigin, 0x39: _AttributeName.declarationColumn, 0x3a: _AttributeName.declarationFile, 0x3b: _AttributeName.declarationLine, 0x57: _AttributeName.callColumn, 0x58: _AttributeName.callFile, 0x59: _AttributeName.callLine, }; const _attributeNameStrings = <_AttributeName, String>{ _AttributeName.sibling: "DW_AT_sibling", _AttributeName.name: "DW_AT_name", _AttributeName.statementList: "DW_AT_stmt_list", _AttributeName.lowProgramCounter: "DW_AT_low_pc", _AttributeName.highProgramCounter: "DW_AT_high_pc", _AttributeName.compilationDirectory: "DW_AT_comp_dir", _AttributeName.inline: "DW_AT_inline", _AttributeName.producer: "DW_AT_producer", _AttributeName.abstractOrigin: "DW_AT_abstract_origin", _AttributeName.declarationColumn: "DW_AT_decl_column", _AttributeName.declarationFile: "DW_AT_decl_file", _AttributeName.declarationLine: "DW_AT_decl_line", _AttributeName.callColumn: "DW_AT_call_column", _AttributeName.callFile: "DW_AT_call_file", _AttributeName.callLine: "DW_AT_call_line", }; enum _AttributeForm { address, constant, reference4, sectionOffset, string, } const _attributeForms = { 0x01: _AttributeForm.address, 0x08: _AttributeForm.string, 0x0f: _AttributeForm.constant, 0x13: _AttributeForm.reference4, 0x17: _AttributeForm.sectionOffset, }; const _attributeFormStrings = <_AttributeForm, String>{ _AttributeForm.address: "DW_FORM_addr", _AttributeForm.string: "DW_FORM_string", _AttributeForm.constant: "DW_FORM_udata", _AttributeForm.reference4: "DW_FORM_ref4", _AttributeForm.sectionOffset: "DW_FORM_sec_offset", }; class _Attribute { final _AttributeName name; final _AttributeForm form; _Attribute._(this.name, this.form); static _Attribute? fromReader(Reader reader) { final nameInt = reader.readLEB128EncodedInteger(); final formInt = reader.readLEB128EncodedInteger(); if (nameInt == 0 && formInt == 0) return null; if (!_attributeNames.containsKey(nameInt)) { throw FormatException("Unexpected DW_AT value 0x${paddedHex(nameInt)}"); } if (!_attributeForms.containsKey(formInt)) { throw FormatException("Unexpected DW_FORM value 0x${paddedHex(formInt)}"); } return _Attribute._(_attributeNames[nameInt]!, _attributeForms[formInt]!); } Object read(Reader reader, CompilationUnitHeader header) { switch (form) { case _AttributeForm.string: return reader.readNullTerminatedString(); case _AttributeForm.address: return reader.readBytes(header.addressSize); case _AttributeForm.sectionOffset: return reader.readBytes(4); case _AttributeForm.constant: return reader.readLEB128EncodedInteger(); case _AttributeForm.reference4: return reader.readBytes(4); } } String valueToString(Object value, [CompilationUnit? unit]) { switch (form) { case _AttributeForm.string: return value as String; case _AttributeForm.address: return '0x' + paddedHex(value as int, unit?.header.addressSize ?? 0); case _AttributeForm.sectionOffset: return paddedHex(value as int, 4); case _AttributeForm.constant: return value.toString(); case _AttributeForm.reference4: final intValue = value as int; final unresolvedValue = paddedHex(intValue, 4); final name = unit?.nameOfOrigin(intValue) ?? ""; return '0x${unresolvedValue} (origin: ${name})'; } } } class _Abbreviation { final int code; final _Tag tag; final bool children; final List<_Attribute> attributes; _Abbreviation._(this.code, this.tag, this.children, this.attributes); // Constants from the DWARF specification. static const _DW_CHILDREN_no = 0x00; static const _DW_CHILDREN_yes = 0x01; static _Abbreviation? fromReader(Reader reader) { final code = reader.readLEB128EncodedInteger(); if (code == 0) return null; final tagInt = reader.readLEB128EncodedInteger(); if (!_tags.containsKey(tagInt)) { throw FormatException("Unexpected DW_TAG value 0x${paddedHex(tagInt)}"); } final tag = _tags[tagInt]!; final childrenByte = reader.readByte(); if (childrenByte != _DW_CHILDREN_no && childrenByte != _DW_CHILDREN_yes) { throw FormatException("Expected DW_CHILDREN_no or DW_CHILDREN_yes: " "${childrenByte}"); } final children = childrenByte == _DW_CHILDREN_yes; final attributes = reader.readRepeated(_Attribute.fromReader).toList(); return _Abbreviation._(code, tag, children, attributes); } void writeToStringBuffer(StringBuffer buffer) { buffer ..write(' Tag: ') ..writeln(_tagStrings[tag]) ..write(' Children: ') ..writeln(children ? 'DW_CHILDREN_yes' : 'DW_CHILDREN_no') ..writeln(' Attributes:'); for (final attribute in attributes) { buffer ..write(' ') ..write(_attributeNameStrings[attribute.name]!) ..write(': ') ..writeln(_attributeFormStrings[attribute.form]!); } } @override String toString() { final buffer = StringBuffer(); writeToStringBuffer(buffer); return buffer.toString(); } } class _AbbreviationsTable { final Map _abbreviations; _AbbreviationsTable._(this._abbreviations); bool containsKey(int code) => _abbreviations.containsKey(code); _Abbreviation? operator [](int code) => _abbreviations[code]; static _AbbreviationsTable? fromReader(Reader reader) { final abbreviations = Map.fromEntries(reader .readRepeated(_Abbreviation.fromReader) .map((abbr) => MapEntry(abbr.code, abbr))); return _AbbreviationsTable._(abbreviations); } void writeToStringBuffer(StringBuffer buffer) { buffer..writeln('Abbreviations table:')..writeln(); _abbreviations.forEach((key, abbreviation) { buffer ..write(' ') ..write(key) ..writeln(':'); abbreviation.writeToStringBuffer(buffer); buffer..writeln(); }); } @override String toString() { final buffer = StringBuffer(); writeToStringBuffer(buffer); return buffer.toString(); } } /// A DWARF Debug Information Entry (DIE). class DebugInformationEntry { // The index of the entry in the abbreviation table for this DIE. final int code; final Map<_Attribute, Object> attributes; final Map children; DebugInformationEntry._(this.code, this.attributes, this.children); static DebugInformationEntry? fromReader( Reader reader, CompilationUnitHeader header) { final code = reader.readLEB128EncodedInteger(); // DIEs with an abbreviation table index of 0 are list end markers. if (code == 0) return null; if (!header.abbreviations.containsKey(code)) { throw FormatException("Unknown abbreviation code 0x${paddedHex(code)}"); } final abbreviation = header.abbreviations[code]!; final attributes = <_Attribute, Object>{}; for (final attribute in abbreviation.attributes) { attributes[attribute] = attribute.read(reader, header); } final children = {}; if (abbreviation.children) { children.addEntries(reader.readRepeatedWithOffsets( (r) => DebugInformationEntry.fromReader(r, header), absolute: true)); } return DebugInformationEntry._(code, attributes, children); } _Attribute? _namedAttribute(_AttributeName name) { for (final attribute in attributes.keys) { if (attribute.name == name) { return attribute; } } return null; } bool containsKey(_AttributeName name) => _namedAttribute(name) != null; Object? operator [](_AttributeName name) => attributes[_namedAttribute(name)]; int? get sectionOffset => this[_AttributeName.statementList] as int?; int? get abstractOrigin => this[_AttributeName.abstractOrigin] as int?; int? get lowPC => this[_AttributeName.lowProgramCounter] as int?; int? get highPC => this[_AttributeName.highProgramCounter] as int?; bool containsPC(int virtualAddress) => (lowPC ?? 0) <= virtualAddress && virtualAddress < (highPC ?? -1); String? get name => this[_AttributeName.name] as String?; int? get callFileIndex => this[_AttributeName.callFile] as int?; int? get callLine => this[_AttributeName.callLine] as int?; int? get callColumn => this[_AttributeName.callColumn] as int?; List? callInfo( CompilationUnit unit, LineNumberProgram lineNumberProgram, int address) { String callFilename(int index) => lineNumberProgram.header.filesInfo[index]?.name ?? ''; if (!containsPC(address)) return null; final tag = unit.header.abbreviations[code]!.tag; final inlined = tag == _Tag.inlinedSubroutine; for (final child in children.values) { final callInfo = child.callInfo(unit, lineNumberProgram, address); if (callInfo == null) continue; if (tag == _Tag.compileUnit) return callInfo; return callInfo ..add(DartCallInfo( function: unit.nameOfOrigin(abstractOrigin ?? -1), inlined: inlined, filename: callFilename(child.callFileIndex ?? -1), line: child.callLine ?? 0, column: child.callColumn ?? 0)); } if (tag == _Tag.compileUnit) return null; final filename = lineNumberProgram.filename(address)!; final line = lineNumberProgram.lineNumber(address)!; final column = lineNumberProgram.column(address)!; return [ DartCallInfo( function: unit.nameOfOrigin(abstractOrigin ?? -1), inlined: inlined, filename: filename, line: line, column: column) ]; } void writeToStringBuffer(StringBuffer buffer, {CompilationUnit? unit, String indent = ''}) { buffer ..write(indent) ..write('Abbreviation code: ') ..write(code) ..writeln('):'); attributes.forEach((attribute, value) { buffer ..write(indent) ..write(' ') ..write(_attributeNameStrings[attribute.name]!) ..write(' => ') ..writeln(attribute.valueToString(value, unit)); }); if (children.isNotEmpty) { buffer ..write(indent) ..write('Children (') ..write(children.length) ..writeln('):'); final sortedChildren = children.entries.toList() ..sort((kv1, kv2) => Comparable.compare(kv1.key, kv2.key)); for (int i = 0; i < sortedChildren.length; i++) { final offset = sortedChildren[i].key; final child = sortedChildren[i].value; buffer ..write(indent) ..write('Child ') ..write(i) ..write(' (at offset 0x') ..write(paddedHex(offset)) ..writeln('):'); child.writeToStringBuffer(buffer, unit: unit, indent: indent + ' '); } } } String toString() { final buffer = StringBuffer(); writeToStringBuffer(buffer); return buffer.toString(); } } class CompilationUnitHeader { final int size; final int version; final int abbreviationsOffset; final int addressSize; final _AbbreviationsTable abbreviations; CompilationUnitHeader._(this.size, this.version, this.abbreviationsOffset, this.addressSize, this.abbreviations); static CompilationUnitHeader? fromReader( Reader reader, Map abbreviationsTables) { final size = _initialLengthValue(reader); // An empty unit is an ending marker. if (size == 0) return null; final version = reader.readBytes(2); if (version != 2) { throw FormatException("Expected DWARF version 2, got $version"); } final abbreviationsOffset = reader.readBytes(4); final abbreviationsTable = abbreviationsTables[abbreviationsOffset]; if (abbreviationsTable == null) { throw FormatException("No abbreviation table found for offset " "0x${paddedHex(abbreviationsOffset, 4)}"); } final addressSize = reader.readByte(); return CompilationUnitHeader._( size, version, abbreviationsOffset, addressSize, abbreviationsTable); } void writeToStringBuffer(StringBuffer buffer) { buffer ..writeln('Compilation unit:') ..write(' Size: ') ..writeln(size) ..write(' Version: ') ..writeln(version) ..write(' Abbreviations offset: 0x') ..writeln(paddedHex(abbreviationsOffset, 4)) ..write(' Address size: ') ..writeln(addressSize) ..writeln(); } @override String toString() { final buffer = StringBuffer(); writeToStringBuffer(buffer); return buffer.toString(); } } /// A class representing a DWARF compilation unit. class CompilationUnit { CompilationUnitHeader header; Map referenceTable; CompilationUnit._(this.header, this.referenceTable); static CompilationUnit? fromReader( Reader reader, Map abbreviationsTables) { final header = CompilationUnitHeader.fromReader(reader, abbreviationsTables); if (header == null) return null; final referenceTable = Map.fromEntries(reader.readRepeatedWithOffsets( (r) => DebugInformationEntry.fromReader(r, header), absolute: true)); _addChildEntries(referenceTable); return CompilationUnit._(header, referenceTable); } static void _addChildEntries(Map table) { final workList = Queue>(); for (final die in table.values) { workList.addAll(die.children.entries); } while (workList.isNotEmpty) { final kv = workList.removeFirst(); final offset = kv.key; final child = kv.value; table[offset] = child; workList.addAll(child.children.entries); } } Iterable? callInfo(LineNumberInfo lineNumberInfo, int address) { for (final die in referenceTable.values) { final lineNumberProgram = lineNumberInfo[die.sectionOffset ?? -1]; if (lineNumberProgram == null) continue; final callInfo = die.callInfo(this, lineNumberProgram, address); if (callInfo != null) return callInfo; } return null; } String nameOfOrigin(int offset) { final origin = referenceTable[offset]; if (origin == null) { throw ArgumentError( "${paddedHex(offset)} is not the offset of an abbreviated unit"); } return origin[_AttributeName.name] as String; } void writeToStringBuffer(StringBuffer buffer) { header.writeToStringBuffer(buffer); referenceTable.forEach((offset, die) { buffer ..write('Debug information entry at offset 0x') ..write(paddedHex(offset)) ..writeln(':'); die.writeToStringBuffer(buffer, unit: this); buffer.writeln(); }); } @override String toString() { final buffer = StringBuffer(); writeToStringBuffer(buffer); return buffer.toString(); } } /// A class representing a DWARF `.debug_info` section. class DebugInfo { final List units; DebugInfo._(this.units); static DebugInfo fromReader( Reader reader, Map abbreviationsTable) { final units = reader .readRepeated( (r) => CompilationUnit.fromReader(reader, abbreviationsTable)) .toList(); return DebugInfo._(units); } Iterable? callInfo(LineNumberInfo lineNumberInfo, int address) { for (final unit in units) { final callInfo = unit.callInfo(lineNumberInfo, address); if (callInfo != null) return callInfo; } return null; } void writeToStringBuffer(StringBuffer buffer) { for (final unit in units) { unit.writeToStringBuffer(buffer); buffer.writeln(); } } String toString() { final buffer = StringBuffer(); writeToStringBuffer(buffer); return buffer.toString(); } } class FileEntry { final String name; final int directoryIndex; final int lastModified; final int size; FileEntry._(this.name, this.directoryIndex, this.lastModified, this.size); static FileEntry? fromReader(Reader reader) { final name = reader.readNullTerminatedString(); // An empty null-terminated string marks the table end. if (name == "") return null; final directoryIndex = reader.readLEB128EncodedInteger(); final lastModified = reader.readLEB128EncodedInteger(); final size = reader.readLEB128EncodedInteger(); return FileEntry._(name, directoryIndex, lastModified, size); } @override String toString() => "File name: $name\n" " Directory index: $directoryIndex\n" " Last modified: $lastModified\n" " Size: $size\n"; } class FileInfo { final Map _files; FileInfo._(this._files); static FileInfo fromReader(Reader reader) { final offsetFiles = reader.readRepeated(FileEntry.fromReader).toList(); final files = {}; for (int i = 0; i < offsetFiles.length; i++) { // File entries are one-based, not zero-based. files[i + 1] = offsetFiles[i]; } return FileInfo._(files); } bool containsKey(int index) => _files.containsKey(index); FileEntry? operator [](int index) => _files[index]; void writeToStringBuffer(StringBuffer buffer) { if (_files.isEmpty) { buffer.writeln("No file information."); return; } final indexHeader = "Entry"; final dirIndexHeader = "Dir"; final modifiedHeader = "Time"; final sizeHeader = "Size"; final nameHeader = "Name"; final indexStrings = _files .map((int i, FileEntry f) => MapEntry(i, i.toString())); final dirIndexStrings = _files.map((int i, FileEntry f) => MapEntry(i, f.directoryIndex.toString())); final modifiedStrings = _files.map((int i, FileEntry f) => MapEntry(i, f.lastModified.toString())); final sizeStrings = _files.map( (int i, FileEntry f) => MapEntry(i, f.size.toString())); final maxIndexLength = indexStrings.values .fold(indexHeader.length, (int acc, String s) => max(acc, s.length)); final maxDirIndexLength = dirIndexStrings.values .fold(dirIndexHeader.length, (int acc, String s) => max(acc, s.length)); final maxModifiedLength = modifiedStrings.values .fold(modifiedHeader.length, (int acc, String s) => max(acc, s.length)); final maxSizeLength = sizeStrings.values .fold(sizeHeader.length, (int acc, String s) => max(acc, s.length)); buffer.writeln("File information:"); buffer..write(" ")..write(indexHeader.padRight(maxIndexLength)); buffer..write(" ")..write(dirIndexHeader.padRight(maxDirIndexLength)); buffer..write(" ")..write(modifiedHeader.padRight(maxModifiedLength)); buffer..write(" ")..write(sizeHeader.padRight(maxSizeLength)); buffer ..write(" ") ..writeln(nameHeader); for (final index in _files.keys) { buffer..write(" ")..write(indexStrings[index]!.padRight(maxIndexLength)); buffer ..write(" ") ..write(dirIndexStrings[index]!.padRight(maxDirIndexLength)); buffer ..write(" ") ..write(modifiedStrings[index]!.padRight(maxModifiedLength)); buffer..write(" ")..write(sizeStrings[index]!.padRight(maxSizeLength)); buffer ..write(" ") ..writeln(_files[index]!.name); } } @override String toString() { var buffer = StringBuffer(); writeToStringBuffer(buffer); return buffer.toString(); } } class LineNumberState { final defaultIsStatement; late int address; late int fileIndex; late int line; late int column; late bool isStatement; late bool basicBlock; late bool endSequence; LineNumberState(this.defaultIsStatement) { reset(); } void reset() { address = 0; fileIndex = 1; line = 1; column = 0; isStatement = defaultIsStatement; basicBlock = false; endSequence = false; } LineNumberState clone() { final clone = LineNumberState(defaultIsStatement); clone.address = address; clone.fileIndex = fileIndex; clone.line = line; clone.column = column; clone.isStatement = isStatement; clone.basicBlock = basicBlock; clone.endSequence = endSequence; return clone; } String toString() => "Current line number state machine registers:\n" " Address: ${paddedHex(address)}\n" " File index: $fileIndex\n" " Line number: $line\n" " Column number: $column\n" " Is ${isStatement ? "" : "not "}a statement.\n" " Is ${basicBlock ? "" : "not "}at the beginning of a basic block.\n" " Is ${endSequence ? "" : "not "}just after the end of a sequence."; } class LineNumberProgramHeader { final int size; final int version; final int headerLength; final int minimumInstructionLength; final bool defaultIsStatement; final int lineBase; final int lineRange; final int opcodeBase; final Map standardOpcodeLengths; final List includeDirectories; final FileInfo filesInfo; LineNumberProgramHeader._( this.size, this.version, this.headerLength, this.minimumInstructionLength, this.defaultIsStatement, this.lineBase, this.lineRange, this.opcodeBase, this.standardOpcodeLengths, this.includeDirectories, this.filesInfo); static LineNumberProgramHeader? fromReader(Reader reader) { final size = _initialLengthValue(reader); if (size == 0) return null; final version = reader.readBytes(2); final headerLength = reader.readBytes(4); // We'll need this later as a double-check that we've read the entire // header. final headerStart = reader.offset; final minimumInstructionLength = reader.readByte(); final isStmtByte = reader.readByte(); if (isStmtByte < 0 || isStmtByte > 1) { throw FormatException( "Unexpected value for default_is_stmt: ${isStmtByte}"); } final defaultIsStatement = isStmtByte == 1; final lineBase = reader.readByte(signed: true); final lineRange = reader.readByte(); final opcodeBase = reader.readByte(); final standardOpcodeLengths = {}; // Standard opcode numbering starts at 1. for (int i = 1; i < opcodeBase; i++) { standardOpcodeLengths[i] = reader.readLEB128EncodedInteger(); } final includeDirectories = []; while (!reader.done) { final directory = reader.readNullTerminatedString(); if (directory == "") break; includeDirectories.add(directory); } if (reader.done) { throw FormatException("Unterminated directory entry"); } final filesInfo = FileInfo.fromReader(reader); // Header length doesn't include the 2-byte version or 4-byte length fields. if (reader.offset != headerStart + headerLength) { throw FormatException("At offset ${reader.offset} after header, " "expected to be at offset ${headerStart + headerLength}"); } return LineNumberProgramHeader._( size, version, headerLength, minimumInstructionLength, defaultIsStatement, lineBase, lineRange, opcodeBase, standardOpcodeLengths, includeDirectories, filesInfo); } void writeToStringBuffer(StringBuffer buffer) { buffer ..write(' Size: ') ..writeln(size) ..write(' Version: ') ..writeln(version) ..write(' Header length: ') ..writeln(headerLength) ..write(' Min instruction length: ') ..writeln(minimumInstructionLength) ..write(' Default value of is_stmt: ') ..writeln(defaultIsStatement) ..write(' Line base: ') ..writeln(lineBase) ..write(' Line range: ') ..writeln(lineRange) ..write(' Opcode base: ') ..writeln(opcodeBase) ..writeln('Standard opcode lengths:'); for (int i = 1; i < opcodeBase; i++) { buffer ..write(' Opcode ') ..write(i) ..write(': ') ..writeln(standardOpcodeLengths[i]); } if (includeDirectories.isEmpty) { buffer.writeln('No include directories.'); } else { buffer.writeln('Include directories:'); for (final dir in includeDirectories) { buffer ..write(' ') ..writeln(dir); } } filesInfo.writeToStringBuffer(buffer); } @override String toString() { final buffer = StringBuffer(); writeToStringBuffer(buffer); return buffer.toString(); } } /// A class representing a DWARF line number program. class LineNumberProgram { final LineNumberProgramHeader header; final List calculatedMatrix; final Map cachedLookups; LineNumberProgram._(this.header, this.calculatedMatrix) : cachedLookups = {}; static LineNumberProgram? fromReader(Reader reader) { final header = LineNumberProgramHeader.fromReader(reader); if (header == null) return null; final calculatedMatrix = _readOpcodes(reader, header).toList(); if (calculatedMatrix.isEmpty) { throw FormatException("No line number information generated by program"); } return LineNumberProgram._(header, calculatedMatrix); } static Iterable _readOpcodes( Reader reader, LineNumberProgramHeader header) sync* { final state = LineNumberState(header.defaultIsStatement); void applySpecialOpcode(int opcode) { final adjustedOpcode = opcode - header.opcodeBase; state.address = adjustedOpcode ~/ header.lineRange; state.line += header.lineBase + (adjustedOpcode % header.lineRange); } while (!reader.done) { final opcode = reader.readByte(); if (opcode >= header.opcodeBase) { applySpecialOpcode(opcode); continue; } switch (opcode) { case 0: // Extended opcodes final extendedLength = reader.readByte(); final subOpcode = reader.readByte(); switch (subOpcode) { case 0: throw FormatException("Attempted to execute extended opcode 0"); case 1: // DW_LNE_end_sequence state.endSequence = true; yield state.clone(); state.reset(); break; case 2: // DW_LNE_set_address // The length includes the subopcode. final valueLength = extendedLength - 1; assert(valueLength == 4 || valueLength == 8); final newAddress = reader.readBytes(valueLength); state.address = newAddress; break; case 3: // DW_LNE_define_file throw FormatException( "DW_LNE_define_file instruction not handled"); default: throw FormatException( "Extended opcode ${subOpcode} not in DWARF 2"); } break; case 1: // DW_LNS_copy yield state.clone(); state.basicBlock = false; break; case 2: // DW_LNS_advance_pc final increment = reader.readLEB128EncodedInteger(); state.address += header.minimumInstructionLength * increment; break; case 3: // DW_LNS_advance_line state.line += reader.readLEB128EncodedInteger(signed: true); break; case 4: // DW_LNS_set_file state.fileIndex = reader.readLEB128EncodedInteger(); break; case 5: // DW_LNS_set_column state.column = reader.readLEB128EncodedInteger(); break; case 6: // DW_LNS_negate_stmt state.isStatement = !state.isStatement; break; case 7: // DW_LNS_set_basic_block state.basicBlock = true; break; case 8: // DW_LNS_const_add_pc applySpecialOpcode(255); break; case 9: // DW_LNS_fixed_advance_pc state.address += reader.readBytes(2); break; default: throw FormatException("Standard opcode ${opcode} not in DWARF 2"); } } } bool containsKey(int address) { assert(calculatedMatrix.last.endSequence); return address >= calculatedMatrix.first.address && address < calculatedMatrix.last.address; } LineNumberState? operator [](int address) { if (cachedLookups.containsKey(address)) return cachedLookups[address]; if (!containsKey(address)) return null; // Since the addresses are generated in increasing order, we can do a // binary search to find the right state. assert(calculatedMatrix.isNotEmpty); var minIndex = 0; var maxIndex = calculatedMatrix.length - 1; while (true) { if (minIndex == maxIndex || minIndex + 1 == maxIndex) { final found = calculatedMatrix[minIndex]; cachedLookups[address] = found; return found; } final index = minIndex + ((maxIndex - minIndex) ~/ 2); final compared = calculatedMatrix[index].address.compareTo(address); if (compared == 0) { return calculatedMatrix[index]; } else if (compared < 0) { minIndex = index; } else if (compared > 0) { maxIndex = index; } } } String? filename(int address) => header.filesInfo[this[address]?.fileIndex ?? -1]?.name; int? lineNumber(int address) => this[address]?.line; int? column(int address) => this[address]?.column; void writeToStringBuffer(StringBuffer buffer) { header.writeToStringBuffer(buffer); buffer.writeln("Results of line number program:"); for (final state in calculatedMatrix) { buffer..writeln(state); } } String toString() { final buffer = StringBuffer(); writeToStringBuffer(buffer); return buffer.toString(); } } /// A class representing a DWARF .debug_line section. class LineNumberInfo { final Map programs; LineNumberInfo._(this.programs); static LineNumberInfo fromReader(Reader reader) { final programs = Map.fromEntries( reader.readRepeatedWithOffsets(LineNumberProgram.fromReader)); return LineNumberInfo._(programs); } bool containsKey(int address) => programs.containsKey(address); LineNumberProgram? operator [](int address) => programs[address]; void writeToStringBuffer(StringBuffer buffer) { programs.forEach((offset, program) { buffer ..write('Line number program @ 0x') ..writeln(paddedHex(offset)); program.writeToStringBuffer(buffer); }); } String toString() { final buffer = StringBuffer(); writeToStringBuffer(buffer); return buffer.toString(); } } // TODO(11617): Replace calls to these functions with a general hashing solution // once available. int _hashCombine(int hash, int value) { hash = 0x1fffffff & (hash + value); hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); return hash ^ (hash >> 6); } int _hashFinish(int hash) { hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); hash = hash ^ (hash >> 11); return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); } /// Represents the information for a call site. abstract class CallInfo { /// Whether this call site is considered internal (i.e. not located in either /// user or library Dart source code). bool get isInternal => true; } /// Represents the information for a call site located in Dart source code. class DartCallInfo extends CallInfo { final bool inlined; final String function; final String filename; final int line; final int column; DartCallInfo( {this.inlined = false, required this.function, required this.filename, required this.line, required this.column}); @override bool get isInternal => false; @override int get hashCode => _hashFinish(_hashCombine( _hashCombine( _hashCombine( _hashCombine( _hashCombine(0, inlined.hashCode), function.hashCode), filename.hashCode), line.hashCode), column.hashCode)); @override bool operator ==(Object other) { if (other is DartCallInfo) { return inlined == other.inlined && function == other.function && filename == other.filename && line == other.line && column == other.column; } return false; } void writeToStringBuffer(StringBuffer buffer) { buffer..write(function)..write(' (')..write(filename); if (line > 0) { buffer..write(':')..write(line); if (column > 0) { buffer..write(':')..write(column); } } buffer.write(')'); } @override String toString() { final buffer = StringBuffer(); writeToStringBuffer(buffer); return buffer.toString(); } } /// Represents the information for a call site located in a Dart stub. class StubCallInfo extends CallInfo { final String name; final int offset; StubCallInfo({required this.name, required this.offset}); @override int get hashCode => _hashFinish( _hashCombine(_hashCombine(0, name.hashCode), offset.hashCode)); @override bool operator ==(Object other) { if (other is StubCallInfo) { return name == other.name && offset == other.offset; } return false; } @override String toString() => "${name}+0x${offset.toRadixString(16)}"; } /// The instructions section in which a program counter address is located. enum InstructionsSection { none, vm, isolate } /// A program counter address viewed as an offset into the appropriate /// instructions section of a Dart snapshot. class PCOffset { final int offset; final InstructionsSection section; PCOffset(this.offset, this.section); /// The virtual address for this [PCOffset] in [dwarf]. int virtualAddressIn(Dwarf dwarf) => dwarf.virtualAddressOf(this); /// The call information found for this [PCOffset] in [dwarf]. /// /// Returns null if the PCOffset is invalid for the given DWARF information. /// /// If [includeInternalFrames] is false, then only information corresponding /// to user or library code is returned. Iterable? callInfoFrom(Dwarf dwarf, {bool includeInternalFrames = false}) => dwarf.callInfoFor(dwarf.virtualAddressOf(this), includeInternalFrames: includeInternalFrames); @override int get hashCode => _hashFinish(_hashCombine(offset.hashCode, section.index)); @override bool operator ==(Object other) { return other is PCOffset && offset == other.offset && section == other.section; } @override String toString() => 'PCOffset($section, $offset)'; } /// The DWARF debugging information for a Dart snapshot. class Dwarf { final Elf _elf; final Map _abbreviationsTables; final DebugInfo _debugInfo; final LineNumberInfo _lineNumberInfo; /// Virtual address of the start of the VM instructions section in the DWARF /// information. final int vmStartAddress; /// Virtual address of the start of the isolate instructions section in the /// DWARF information. final int isolateStartAddress; Dwarf._(this._elf, this._abbreviationsTables, this._debugInfo, this._lineNumberInfo, this.vmStartAddress, this.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); } /// Attempts to load the DWARF debugging information from the given bytes. /// /// Returns a [Dwarf] object if the load succeeds, otherwise returns null. static Dwarf? fromBytes(Uint8List bytes) => Dwarf.fromReader(Reader.fromTypedData(bytes)); /// Attempts to load the DWARF debugging information from the file at [path]. /// /// 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); } /// 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(); } /// The call information for the given virtual address. There may be /// multiple [CallInfo] objects returned for a single virtual address when /// code has been inlined. /// /// Returns null if the given address is invalid for the DWARF information. /// /// If [includeInternalFrames] is false, then only information corresponding /// to user or library code is returned. Iterable? callInfoFor(int address, {bool includeInternalFrames = false}) { var calls = _debugInfo.callInfo(_lineNumberInfo, address); if (calls == null) { final symbol = _elf.staticSymbolAt(address); if (symbol != null) { final offset = address - symbol.value; calls = [StubCallInfo(name: symbol.name, offset: offset)]; } } if (!includeInternalFrames) { return calls?.where((CallInfo c) => !c.isInternal); } return calls; } /// The virtual address in this DWARF information for the given [PCOffset]. int virtualAddressOf(PCOffset pcOffset) { switch (pcOffset.section) { case InstructionsSection.none: // This address is already virtualized, so we don't need to change it. return pcOffset.offset; case InstructionsSection.vm: return pcOffset.offset + vmStartAddress; case InstructionsSection.isolate: return pcOffset.offset + isolateStartAddress; default: throw "Unexpected value for instructions section"; } } void writeToStringBuffer(StringBuffer buffer) { buffer ..writeln('----------------------------------------') ..writeln(' Abbreviation tables') ..writeln('----------------------------------------') ..writeln(); _abbreviationsTables.forEach((offset, table) { buffer..write('(Offset ')..write(paddedHex(offset, 4))..write(') '); table.writeToStringBuffer(buffer); }); buffer ..writeln('----------------------------------------') ..writeln(' Debug information') ..writeln('----------------------------------------') ..writeln(); _debugInfo.writeToStringBuffer(buffer); buffer ..writeln('----------------------------------------') ..writeln(' Line number information') ..writeln('----------------------------------------') ..writeln(); _lineNumberInfo.writeToStringBuffer(buffer); } String dumpFileInfo() { final buffer = StringBuffer(); _elf.writeToStringBuffer(buffer); buffer.writeln(); writeToStringBuffer(buffer); return buffer.toString(); } @override String toString() { final buffer = StringBuffer(); writeToStringBuffer(buffer); return buffer.toString(); } }