Files
sdk/pkg/native_stack_traces/lib/src/dwarf.dart
T
Tess Strickland d5649fc9f6 [vm] Add build ID to non-symbolic stack traces.
Since we've run out of room for more fields in the Image object header
on 64-bit architectures, the serializer instead creates an ImageHeader
object for precompiled snapshots that is placed at the start of text
segments. The new ImageHeader object contains the following information:

* The offset of the BSS segment from the text segment, previously
  stored in the Image object header.

* The relocated address of the text segment in the dynamic shared
  object. Due to restrictions when generating assembly snapshots, this
  field is only set for ELF snapshots, and so it can also be used to
  detect whether a snapshot was compiled to assembly or ELF.

* The offset of the build ID description field from the text segment.

* The length of the build ID description field.

We replace the BSS offset in the Image object header with the offset of
the ImageHeader object within the text segment, so that we can detect
when a given Image has an ImageHeader object available.

There are no methods available on ImageHeader objects, but instead the
Image itself controls access to the information. In particular, the
relocated address method either returns the relocated address
information from the ImageHeader object or from the initialized BSS
depending on the type of snapshot, so the caller need not do this work.
Also, instead of returning the raw offset to the BSS section and having
the caller turn that into an appropriate pointer, the method for
accessing the BSS segment now returns a pointer to the segment.

Bug: https://github.com/dart-lang/sdk/issues/43274
Cq-Include-Trybots: luci.dart.try:vm-precomp-ffi-qemu-linux-release-arm-try,vm-kernel-precomp-android-release-arm64-try,vm-kernel-precomp-android-release-arm_x64-try
Change-Id: I15eae4ad0a088260b127f3d07da79374215b7f56
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/163207
Commit-Queue: Tess Strickland <sstrickl@google.com>
Reviewed-by: Daco Harkes <dacoharkes@google.com>
2020-09-22 17:14:44 +00:00

1343 lines
42 KiB
Dart

// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart: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 = <int, _Tag>{
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 = <int, _AttributeName>{
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 = <int, _AttributeForm>{
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) ?? "<unresolved>";
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<int, _Abbreviation> _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<int, DebugInformationEntry> 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 = <int, DebugInformationEntry>{};
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>? callInfo(
CompilationUnit unit, LineNumberProgram lineNumberProgram, int address) {
String callFilename(int index) =>
lineNumberProgram.header.filesInfo[index]?.name ?? '<unknown file>';
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<int, _AbbreviationsTable> 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<int, DebugInformationEntry> referenceTable;
CompilationUnit._(this.header, this.referenceTable);
static CompilationUnit? fromReader(
Reader reader, Map<int, _AbbreviationsTable> 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<int, DebugInformationEntry> table) {
final workList = Queue<MapEntry<int, DebugInformationEntry>>();
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>? 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<CompilationUnit> units;
DebugInfo._(this.units);
static DebugInfo fromReader(
Reader reader, Map<int, _AbbreviationsTable> abbreviationsTable) {
final units = reader
.readRepeated(
(r) => CompilationUnit.fromReader(reader, abbreviationsTable))
.toList();
return DebugInfo._(units);
}
Iterable<CallInfo>? 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<int, FileEntry> _files;
FileInfo._(this._files);
static FileInfo fromReader(Reader reader) {
final offsetFiles = reader.readRepeated(FileEntry.fromReader).toList();
final files = <int, FileEntry>{};
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<int, String>(i, i.toString()));
final dirIndexStrings = _files.map((int i, FileEntry f) =>
MapEntry<int, String>(i, f.directoryIndex.toString()));
final modifiedStrings = _files.map((int i, FileEntry f) =>
MapEntry<int, String>(i, f.lastModified.toString()));
final sizeStrings = _files.map(
(int i, FileEntry f) => MapEntry<int, String>(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<int, int> standardOpcodeLengths;
final List<String> 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 = <int, int>{};
// Standard opcode numbering starts at 1.
for (int i = 1; i < opcodeBase; i++) {
standardOpcodeLengths[i] = reader.readLEB128EncodedInteger();
}
final includeDirectories = <String>[];
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<LineNumberState> calculatedMatrix;
final Map<int, LineNumberState> 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<LineNumberState> _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<int, LineNumberProgram> 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<CallInfo>? 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<int, _AbbreviationsTable> _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<CallInfo>? 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 = <CallInfo>[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();
}
}