Reland "[pkg/native_stack_traces] Support Mach-O dSYM debugging information."
This is a reland of commit 08c13f173c3715fd768d768566eeccf1f9f14c3b
Fixes test failures on non-x64 architectures, both in the test
harness and due to DWARF5 line number program headers having a
non-backwards compatible format. (We generate DWARF2 in the
ELF snapshot writer, but the assembler used for assembly snapshots
may generate DWARF5.)
TEST=vm/dart{,_2}/use_dwarf_stack_traces_flag
Original change's description:
> [pkg/native_stack_traces] Support Mach-O dSYM debugging information.
>
> TEST=vm/dart{,_2}/use_dwarf_stack_traces_flag
>
> Bug: https://github.com/dart-lang/sdk/issues/43612
> Cq-Include-Trybots: luci.dart.try:vm-kernel-precomp-dwarf-linux-product-x64-try,vm-kernel-precomp-linux-product-x64-try,vm-kernel-precomp-mac-product-x64-try,pkg-mac-release-arm64-try,vm-kernel-mac-release-arm64-try,vm-kernel-precomp-nnbd-mac-release-arm64-try
> Change-Id: Icda21bb14dcc0cf4784cea118e6ba7dd4edd35aa
> Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/250381
> Commit-Queue: Tess Strickland <sstrickl@google.com>
> Reviewed-by: Slava Egorov <vegorov@google.com>
Bug: https://github.com/dart-lang/sdk/issues/43612
Change-Id: I8a9cb70e78bc8594bcae004809c5a1be778d691d
Cq-Include-Trybots: luci.dart.try:vm-kernel-precomp-dwarf-linux-product-x64-try,vm-kernel-precomp-linux-product-x64-try,vm-kernel-precomp-mac-product-x64-try,pkg-mac-release-arm64-try,vm-kernel-mac-release-arm64-try,vm-kernel-precomp-nnbd-mac-release-arm64-try,vm-kernel-precomp-linux-debug-x64c-try,vm-kernel-nnbd-linux-release-simarm64-try,vm-kernel-precomp-linux-release-simarm_x64-try,vm-kernel-precomp-nnbd-mac-release-simarm64-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/251464
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Tess Strickland <sstrickl@google.com>
diff --git a/pkg/native_stack_traces/CHANGELOG.md b/pkg/native_stack_traces/CHANGELOG.md
index b96e965..a1b47ce 100644
--- a/pkg/native_stack_traces/CHANGELOG.md
+++ b/pkg/native_stack_traces/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 0.5.0
+
+- Require Dart >= 2.17 (enhanced enum support)
+- Add support for parsing DWARF in Mach-O files and dSYM directories.
+- Add some support for DWARF5.
+- 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 12c61ef..cb44a02 100644
--- a/pkg/native_stack_traces/bin/decode.dart
+++ b/pkg/native_stack_traces/bin/decode.dart
@@ -18,10 +18,11 @@
..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 _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 @@
Options specific to the find command:
${_findParser.usage}''';
+final String _dumpUsage = '''
+Usage: decode dump [options] <snapshot>
+
+The dump command dumps the DWARF information in the given snapshot to either
+standard output or a given output file.
+
+Options specific to the dump command:
+${_dumpParser.usage}''';
+
final _usages = <String?, String>{
null: _mainUsage,
'': _mainUsage,
'help': _helpUsage,
'translate': _translateUsage,
'find': _findUsage,
+ 'dump': _dumpUsage,
};
const int _badUsageExitCode = 1;
@@ -162,15 +174,16 @@
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 @@
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 @@
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 @@
await output.close();
}
+Future<void> dump(ArgResults options) async {
+ void usageError(String message) => errorWithUsage(message, command: 'dump');
+
+ if (options.rest.isEmpty) {
+ return usageError('must provide a path to an ELF file or dSYM directory '
+ 'that contains DWARF information');
+ }
+ final dwarf = _loadFromFile(options.rest.first, usageError);
+ if (dwarf == null) {
+ return 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<void> main(List<String> arguments) async {
ArgResults options;
@@ -310,5 +337,7 @@
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/native_stack_traces.dart b/pkg/native_stack_traces/lib/native_stack_traces.dart
index 13e28ff..e47c840 100644
--- a/pkg/native_stack_traces/lib/native_stack_traces.dart
+++ b/pkg/native_stack_traces/lib/native_stack_traces.dart
@@ -9,4 +9,10 @@
DwarfStackTraceDecoder,
StackTraceHeader;
export 'src/dwarf.dart'
- show CallInfo, DartCallInfo, StubCallInfo, Dwarf, PCOffset;
+ show
+ CallInfo,
+ DartCallInfo,
+ Dwarf,
+ InstructionsSection,
+ PCOffset,
+ StubCallInfo;
diff --git a/pkg/native_stack_traces/lib/src/dwarf.dart b/pkg/native_stack_traces/lib/src/dwarf.dart
index 93a2af4..9944f2d 100644
--- a/pkg/native_stack_traces/lib/src/dwarf.dart
+++ b/pkg/native_stack_traces/lib/src/dwarf.dart
@@ -2,14 +2,21 @@
// 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:async';
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';
+const String _debugStringTableKey = 'debugStringTable';
+const String _debugLineStringTableKey = 'debugLineStringTable';
+
int _initialLengthValue(Reader reader) {
final length = reader.readBytes(4);
if (length == 0xffffffff) {
@@ -21,107 +28,190 @@
}
enum _Tag {
- compileUnit,
- inlinedSubroutine,
- subprogram,
+ // Snake case used to match DWARF specification.
+ compile_unit(0x11),
+ inlined_subroutine(0x1d),
+ subprogram(0x2e);
+
+ final int code;
+
+ const _Tag(this.code);
+
+ static const _prefix = 'DW_TAG';
+
+ static _Tag fromReader(Reader reader) {
+ final code = reader.readLEB128EncodedInteger();
+ for (final name in values) {
+ if (name.code == code) {
+ return name;
+ }
+ }
+ throw FormatException(
+ 'Unexpected $_prefix code 0x${code.toRadixString(16)}');
+ }
+
+ @override
+ String toString() => '${_prefix}_$name';
}
-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,
- artificial,
- callColumn,
- callFile,
- callLine,
- compilationDirectory,
- declarationColumn,
- declarationFile,
- declarationLine,
- highProgramCounter,
- lowProgramCounter,
- inline,
- name,
- producer,
- sibling,
- statementList,
+ // Snake case used to match DWARF specification.
+ sibling(0x01),
+ // Avoiding conflict with enum 'name' property
+ nameValue(0x03),
+ stmt_list(0x10),
+ low_pc(0x11),
+ high_pc(0x12),
+ comp_dir(0x1b),
+ inline(0x20),
+ producer(0x25),
+ abstract_origin(0x31),
+ artificial(0x34),
+ decl_column(0x39),
+ decl_file(0x3a),
+ decl_line(0x3b),
+ call_column(0x57),
+ call_file(0x58),
+ call_line(0x59);
+
+ final int code;
+
+ const _AttributeName(this.code);
+
+ static const _prefix = 'DW_AT';
+
+ static _AttributeName? fromReader(Reader reader) {
+ final code = reader.readLEB128EncodedInteger();
+ if (code == 0x00) return null; // Used as end marker in some cases.
+ for (final name in values) {
+ if (name.code == code) {
+ return name;
+ }
+ }
+ throw FormatException(
+ 'Unexpected $_prefix code 0x${code.toRadixString(16)}');
+ }
+
+ @override
+ String toString() => '${_prefix}_${this == nameValue ? 'name' : name}';
}
-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,
- 0x34: _AttributeName.artificial,
- 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.artificial: 'DW_AT_artificial',
- _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,
- flag,
- reference4,
- sectionOffset,
- string,
+ // Snake case used to match DWARF specification.
+ addr(0x01),
+ data2(0x05),
+ data4(0x06),
+ data8(0x07),
+ string(0x08),
+ data1(0x0b),
+ flag(0x0c),
+ strp(0x0e),
+ udata(0x0f),
+ ref4(0x13),
+ sec_offset(0x17),
+ data16(0x1e),
+ line_strp(0x1f);
+
+ final int code;
+
+ const _AttributeForm(this.code);
+
+ static const _prefix = 'DW_FORM';
+
+ static _AttributeForm? fromReader(Reader reader) {
+ final code = reader.readLEB128EncodedInteger();
+ if (code == 0x00) return null; // Used as end marker in some cases.
+ for (final name in values) {
+ if (name.code == code) {
+ return name;
+ }
+ }
+ throw FormatException('Unexpected $_prefix code '
+ '0x${code.toRadixString(16)}');
+ }
+
+ Object read(Reader reader, {int? addressSize}) {
+ switch (this) {
+ case _AttributeForm.string:
+ return reader.readNullTerminatedString();
+ case _AttributeForm.strp:
+ final offset = reader.readBytes(4); // Assumed 32-bit DWARF
+ final debugStringTable =
+ Zone.current[_debugStringTableKey] as DwarfContainerStringTable?;
+ if (debugStringTable == null) {
+ throw FormatException('No .debug_str available');
+ }
+ return debugStringTable[offset]!;
+ case _AttributeForm.line_strp:
+ final offset = reader.readBytes(4); // Assumed 32-bit DWARF
+ final debugLineStringTable = Zone.current[_debugLineStringTableKey]
+ as DwarfContainerStringTable?;
+ if (debugLineStringTable == null) {
+ throw FormatException('No .debug_line_str available');
+ }
+ return debugLineStringTable[offset]!;
+ case _AttributeForm.flag:
+ return reader.readByte() != 0;
+ case _AttributeForm.addr:
+ if (addressSize == null) {
+ throw FormatException('No address size available');
+ }
+ return reader.readBytes(addressSize);
+ case _AttributeForm.data1:
+ return reader.readByte();
+ case _AttributeForm.data2:
+ return reader.readBytes(2);
+ case _AttributeForm.sec_offset: // Assumed 32-bit DWARF
+ case _AttributeForm.data4:
+ case _AttributeForm.ref4:
+ return reader.readBytes(4);
+ case _AttributeForm.data8:
+ return reader.readBytes(8);
+ case _AttributeForm.udata:
+ return reader.readLEB128EncodedInteger();
+ case _AttributeForm.data16:
+ return reader.readRawBytes(16);
+ }
+ }
+
+ String valueToString(Object value,
+ {CompilationUnit? unit, int? addressSize}) {
+ switch (this) {
+ case _AttributeForm.string:
+ case _AttributeForm.strp:
+ case _AttributeForm.line_strp:
+ return value as String;
+ case _AttributeForm.flag:
+ return value.toString();
+ case _AttributeForm.addr:
+ return '0x${paddedHex(value as int, addressSize ?? 0)}';
+ case _AttributeForm.sec_offset:
+ return paddedHex(value as int, 4); // Assumed 32-bit DWARF
+ case _AttributeForm.data1:
+ case _AttributeForm.data2:
+ case _AttributeForm.data4:
+ case _AttributeForm.data8:
+ case _AttributeForm.udata:
+ return value.toString();
+ case _AttributeForm.ref4:
+ final intValue = value as int;
+ final unresolvedValue = paddedHex(intValue, 4);
+ final name = unit?.nameOfOrigin(intValue) ?? '<unresolved>';
+ return '0x$unresolvedValue (origin: $name)';
+ case _AttributeForm.data16:
+ final bdata = value as ByteData;
+ final buffer = StringBuffer();
+ for (int i = 0; i < 16; i++) {
+ buffer.write(bdata.getUint8(i).toRadixString(16));
+ }
+ return buffer.toString();
+ }
+ }
+
+ @override
+ String toString() => '${_prefix}_$name';
}
-const _attributeForms = <int, _AttributeForm>{
- 0x01: _AttributeForm.address,
- 0x08: _AttributeForm.string,
- 0x0c: _AttributeForm.flag,
- 0x0f: _AttributeForm.constant,
- 0x13: _AttributeForm.reference4,
- 0x17: _AttributeForm.sectionOffset,
-};
-
-const _attributeFormStrings = <_AttributeForm, String>{
- _AttributeForm.address: 'DW_FORM_addr',
- _AttributeForm.string: 'DW_FORM_string',
- _AttributeForm.flag: 'DW_FORM_flag',
- _AttributeForm.constant: 'DW_FORM_udata',
- _AttributeForm.reference4: 'DW_FORM_ref4',
- _AttributeForm.sectionOffset: 'DW_FORM_sec_offset',
-};
-
class _Attribute {
final _AttributeName name;
final _AttributeForm form;
@@ -129,54 +219,22 @@
_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)}');
+ final name = _AttributeName.fromReader(reader);
+ final form = _AttributeForm.fromReader(reader);
+ if (name == null || form == null) {
+ // If one is null, the other should be null.
+ assert(name == null && form == null);
+ return null;
}
- if (!_attributeForms.containsKey(formInt)) {
- throw FormatException('Unexpected DW_FORM value 0x${paddedHex(formInt)}');
- }
- return _Attribute._(_attributeNames[nameInt]!, _attributeForms[formInt]!);
+ return _Attribute._(name, form);
}
- Object read(Reader reader, CompilationUnitHeader header) {
- switch (form) {
- case _AttributeForm.string:
- return reader.readNullTerminatedString();
- case _AttributeForm.flag:
- return reader.readByte() != 0;
- 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);
- }
- }
+ Object read(Reader reader, {int? addressSize}) =>
+ form.read(reader, addressSize: addressSize);
- String valueToString(Object value, [CompilationUnit? unit]) {
- switch (form) {
- case _AttributeForm.string:
- return value as String;
- case _AttributeForm.flag:
- return value.toString();
- 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)';
- }
- }
+ String valueToString(Object value,
+ {CompilationUnit? unit, int? addressSize}) =>
+ form.valueToString(value, unit: unit, addressSize: addressSize);
}
class _Abbreviation {
@@ -194,11 +252,7 @@
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 tag = _Tag.fromReader(reader);
final childrenByte = reader.readByte();
if (childrenByte != _dwChildrenNo && childrenByte != _dwChildrenYes) {
throw FormatException('Expected DW_CHILDREN_no or DW_CHILDREN_yes: '
@@ -212,16 +266,16 @@
void writeToStringBuffer(StringBuffer buffer) {
buffer
..write(' Tag: ')
- ..writeln(_tagStrings[tag])
+ ..writeln(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(attribute.name)
..write(': ')
- ..writeln(_attributeFormStrings[attribute.form]!);
+ ..writeln(attribute.form);
}
}
@@ -291,7 +345,8 @@
final abbreviation = header.abbreviations[code]!;
final attributes = <_Attribute, Object>{};
for (final attribute in abbreviation.attributes) {
- attributes[attribute] = attribute.read(reader, header);
+ attributes[attribute] =
+ attribute.read(reader, addressSize: header.addressSize);
}
final children = <int, DebugInformationEntry>{};
if (abbreviation.children) {
@@ -317,26 +372,26 @@
// ignore: library_private_types_in_public_api
Object? operator [](_AttributeName name) => attributes[_namedAttribute(name)];
- int? get sectionOffset => this[_AttributeName.statementList] as int?;
+ int? get sectionOffset => this[_AttributeName.stmt_list] as int?;
- int? get abstractOrigin => this[_AttributeName.abstractOrigin] as int?;
+ int? get abstractOrigin => this[_AttributeName.abstract_origin] as int?;
- int? get lowPC => this[_AttributeName.lowProgramCounter] as int?;
+ int? get lowPC => this[_AttributeName.low_pc] as int?;
- int? get highPC => this[_AttributeName.highProgramCounter] as int?;
+ int? get highPC => this[_AttributeName.high_pc] as int?;
bool get isArtificial => (this[_AttributeName.artificial] ?? false) as bool;
bool containsPC(int virtualAddress) =>
(lowPC ?? 0) <= virtualAddress && virtualAddress < (highPC ?? -1);
- String? get name => this[_AttributeName.name] as String?;
+ String? get name => this[_AttributeName.nameValue] as String?;
- int? get callFileIndex => this[_AttributeName.callFile] as int?;
+ int? get callFileIndex => this[_AttributeName.call_file] as int?;
- int? get callLine => this[_AttributeName.callLine] as int?;
+ int? get callLine => this[_AttributeName.call_line] as int?;
- int? get callColumn => this[_AttributeName.callColumn] as int?;
+ int? get callColumn => this[_AttributeName.call_column] as int?;
List<CallInfo>? callInfo(
CompilationUnit unit, LineNumberProgram lineNumberProgram, int address) {
@@ -345,12 +400,12 @@
if (!containsPC(address)) return null;
final tag = unit.header.abbreviations[code]!.tag;
- final inlined = tag == _Tag.inlinedSubroutine;
+ final inlined = tag == _Tag.inlined_subroutine;
for (final child in children.values) {
final callInfo = child.callInfo(unit, lineNumberProgram, address);
if (callInfo == null) continue;
- if (tag == _Tag.compileUnit) return callInfo;
+ if (tag == _Tag.compile_unit) return callInfo;
return callInfo
..add(DartCallInfo(
@@ -362,7 +417,7 @@
column: child.callColumn ?? 0));
}
- if (tag == _Tag.compileUnit) return null;
+ if (tag == _Tag.compile_unit) return null;
final filename = lineNumberProgram.filename(address)!;
final line = lineNumberProgram.lineNumber(address)!;
@@ -389,9 +444,9 @@
buffer
..write(indent)
..write(' ')
- ..write(_attributeNameStrings[attribute.name]!)
+ ..write(attribute.name)
..write(' => ')
- ..writeln(attribute.valueToString(value, unit));
+ ..writeln(attribute.valueToString(value, unit: unit));
});
if (children.isNotEmpty) {
buffer
@@ -531,7 +586,7 @@
throw ArgumentError(
'${paddedHex(offset)} is not the offset of an abbreviated unit');
}
- return origin[_AttributeName.name] as String;
+ return origin[_AttributeName.nameValue] as String;
}
void writeToStringBuffer(StringBuffer buffer) {
@@ -594,6 +649,73 @@
}
}
+enum _LineNumberContentType {
+ path(0x01),
+ directory_index(0x02),
+ timestamp(0x03),
+ size(0x04),
+ md5(0x05);
+
+ final int code;
+
+ const _LineNumberContentType(this.code);
+
+ static const String _prefix = 'DW_LNCT';
+
+ static _LineNumberContentType fromReader(Reader reader) {
+ final code = reader.readLEB128EncodedInteger();
+ for (final type in values) {
+ if (type.code == code) {
+ return type;
+ }
+ }
+ throw FormatException('Unexpected $_prefix code '
+ '0x${code.toRadixString(16)}');
+ }
+
+ void validate(_AttributeForm form) {
+ switch (this) {
+ case _LineNumberContentType.path:
+ if (form == _AttributeForm.string || form == _AttributeForm.line_strp) {
+ return;
+ }
+ break;
+ case _LineNumberContentType.directory_index:
+ if (form == _AttributeForm.data1 ||
+ form == _AttributeForm.data2 ||
+ form == _AttributeForm.udata) {
+ return;
+ }
+ break;
+ case _LineNumberContentType.timestamp:
+ if (form == _AttributeForm.data4 ||
+ form == _AttributeForm.data8 ||
+ form == _AttributeForm.udata) {
+ return;
+ }
+ break;
+ case _LineNumberContentType.size:
+ if (form == _AttributeForm.data1 ||
+ form == _AttributeForm.data2 ||
+ form == _AttributeForm.data4 ||
+ form == _AttributeForm.data8 ||
+ form == _AttributeForm.udata) {
+ return;
+ }
+ break;
+ case _LineNumberContentType.md5:
+ if (form == _AttributeForm.data16) {
+ return;
+ }
+ break;
+ }
+ throw FormatException('Unexpected form $form for $this');
+ }
+
+ @override
+ String toString() => '${_prefix}_${this == md5 ? 'MD5' : name}';
+}
+
class FileEntry {
final String name;
final int directoryIndex;
@@ -634,6 +756,81 @@
return FileInfo._(files);
}
+ static FileInfo fromReaderDwarf5(Reader reader, {int? addressSize}) {
+ final entryFormatCount = reader.readByte();
+ final entryFormatTypes = <_LineNumberContentType>[];
+ final entryFormatForms = <_AttributeForm>[];
+ int? sizeIndex;
+ int? directoryIndexIndex;
+ int? timestampIndex;
+ int? nameIndex;
+
+ for (int i = 0; i < entryFormatCount; i++) {
+ final type = _LineNumberContentType.fromReader(reader);
+ final form = _AttributeForm.fromReader(reader)!;
+ type.validate(form);
+ entryFormatTypes.add(type);
+ entryFormatForms.add(form);
+ switch (type) {
+ case _LineNumberContentType.path:
+ if (nameIndex != null) {
+ throw FormatException('Multiple $type entries in format');
+ }
+ nameIndex = i;
+ break;
+ case _LineNumberContentType.directory_index:
+ if (directoryIndexIndex != null) {
+ throw FormatException('Multiple $type entries in format');
+ }
+ directoryIndexIndex = i;
+ break;
+ case _LineNumberContentType.timestamp:
+ if (timestampIndex != null) {
+ throw FormatException('Multiple $type entries in format');
+ }
+ timestampIndex = i;
+ break;
+ case _LineNumberContentType.size:
+ if (sizeIndex != null) {
+ throw FormatException('Multiple $type entries in format');
+ }
+ sizeIndex = i;
+ break;
+ case _LineNumberContentType.md5:
+ break;
+ }
+ }
+ if (nameIndex == null) {
+ throw FormatException(
+ 'Missing ${_LineNumberContentType.path} entry in format');
+ }
+
+ final fileNamesCount = reader.readLEB128EncodedInteger();
+ if (entryFormatCount == 0 && fileNamesCount != 0) {
+ throw FormatException('Missing entry format(s)');
+ }
+ final files = <int, FileEntry>{};
+ for (int i = 0; i < fileNamesCount; i++) {
+ final values = <Object>[];
+ for (int j = 0; j < entryFormatCount; j++) {
+ final form = entryFormatForms[j];
+ final value = form.read(reader, addressSize: addressSize);
+ values.add(value);
+ }
+ final name = values[nameIndex] as String;
+ // For any missing values, just use 0.
+ final size = sizeIndex == null ? 0 : values[sizeIndex] as int;
+ final directoryIndex =
+ directoryIndexIndex == null ? 0 : values[directoryIndexIndex] as int;
+ final timestamp =
+ timestampIndex == null ? 0 : values[timestampIndex] as int;
+ // In DWARF5, file entries are zero-based, as the current compilation file
+ // name is provided first instead of implicit.
+ files[i] = FileEntry._(name, directoryIndex, timestamp, size);
+ }
+ return FileInfo._(files);
+ }
+
bool containsKey(int index) => _files.containsKey(index);
FileEntry? operator [](int index) => _files[index];
@@ -716,12 +913,17 @@
final bool defaultIsStatement;
late int address;
+ late int opIndex;
late int fileIndex;
late int line;
late int column;
late bool isStatement;
late bool basicBlock;
late bool endSequence;
+ late bool prologueEnd;
+ late bool epilogueBegin;
+ late int isa;
+ late int discriminator;
LineNumberState(this.defaultIsStatement) {
reset();
@@ -729,35 +931,50 @@
void reset() {
address = 0;
+ opIndex = 0;
fileIndex = 1;
line = 1;
column = 0;
isStatement = defaultIsStatement;
basicBlock = false;
endSequence = false;
+ prologueEnd = false;
+ epilogueBegin = false;
+ isa = 0;
+ discriminator = 0;
}
LineNumberState clone() {
final clone = LineNumberState(defaultIsStatement);
clone.address = address;
+ clone.opIndex = opIndex;
clone.fileIndex = fileIndex;
clone.line = line;
clone.column = column;
clone.isStatement = isStatement;
clone.basicBlock = basicBlock;
clone.endSequence = endSequence;
+ clone.prologueEnd = prologueEnd;
+ clone.epilogueBegin = epilogueBegin;
+ clone.isa = isa;
+ clone.discriminator = discriminator;
return clone;
}
@override
String toString() => 'Current line number state machine registers:\n'
' Address: ${paddedHex(address)}\n'
+ ' Op index: $opIndex\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.";
+ " Is ${endSequence ? "" : "not "}just after the end of a sequence.\n"
+ " Is ${prologueEnd ? "" : "not "}at a function entry breakpoint.\n"
+ " Is ${epilogueBegin ? "" : "not "}at a function exit breakpoint.\n"
+ ' Applicable instruction set architecture: $isa\n'
+ ' Block discrimator: $discriminator\n';
}
class LineNumberProgramHeader {
@@ -772,6 +989,7 @@
final Map<int, int> standardOpcodeLengths;
final List<String> includeDirectories;
final FileInfo filesInfo;
+ final int _fullHeaderSize;
LineNumberProgramHeader._(
this.size,
@@ -784,17 +1002,29 @@
this.opcodeBase,
this.standardOpcodeLengths,
this.includeDirectories,
- this.filesInfo);
+ this.filesInfo,
+ this._fullHeaderSize);
static LineNumberProgramHeader? fromReader(Reader reader) {
final size = _initialLengthValue(reader);
if (size == 0) return null;
+ final headerStart = reader.offset;
final version = reader.readBytes(2);
+ // Only used for DWARF5.
+ int? addressSize;
+ if (version == 5) {
+ // These fields are DWARF5 specific.
+ addressSize = reader.readByte();
+ final segmentSelectorSize = reader.readByte();
+ // We don't support segmented memory addresses here;
+ assert(segmentSelectorSize == 0);
+ }
+
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 offsetAfterHeaderLength = reader.offset;
final minimumInstructionLength = reader.readByte();
final isStmtByte = reader.readByte();
if (isStmtByte < 0 || isStmtByte > 1) {
@@ -811,22 +1041,46 @@
standardOpcodeLengths[i] = reader.readLEB128EncodedInteger();
}
final includeDirectories = <String>[];
- while (!reader.done) {
- final directory = reader.readNullTerminatedString();
- if (directory == '') break;
- includeDirectories.add(directory);
+ if (version == 5) {
+ final directoryEntryFormatCount = reader.readByte();
+ if (directoryEntryFormatCount > 1) {
+ throw FormatException(
+ 'Multiple directory formats not currently handled');
+ }
+ final contentType = _LineNumberContentType.fromReader(reader);
+ if (contentType != _LineNumberContentType.path) {
+ throw FormatException('Unexpected content type $contentType');
+ }
+ final form = _AttributeForm.fromReader(reader)!;
+ contentType.validate(form);
+ final directoryCount = reader.readLEB128EncodedInteger();
+ for (int i = 0; i < directoryCount; i++) {
+ final value = form.read(reader, addressSize: addressSize);
+ includeDirectories.add(value as String);
+ }
+ } else {
+ while (!reader.done) {
+ final directory = reader.readNullTerminatedString();
+ if (directory == '') break;
+ includeDirectories.add(directory);
+ }
+ if (reader.done) {
+ throw FormatException('Unterminated directory entry');
+ }
}
- if (reader.done) {
- throw FormatException('Unterminated directory entry');
- }
- final filesInfo = FileInfo.fromReader(reader);
+ final filesInfo = version == 5
+ ? FileInfo.fromReaderDwarf5(reader, addressSize: addressSize)
+ : FileInfo.fromReader(reader);
- // Header length doesn't include the 2-byte version or 4-byte length fields.
- if (reader.offset != headerStart + headerLength) {
+ // Header length doesn't include anything up to the header length field.
+ if (reader.offset != offsetAfterHeaderLength + headerLength) {
throw FormatException('At offset ${reader.offset} after header, '
'expected to be at offset ${headerStart + headerLength}');
}
+ // We also keep note of the full header size internally so we can adjust
+ // readers as necessary later.
+ final fullHeaderSize = reader.offset - headerStart;
return LineNumberProgramHeader._(
size,
version,
@@ -838,7 +1092,8 @@
opcodeBase,
standardOpcodeLengths,
includeDirectories,
- filesInfo);
+ filesInfo,
+ fullHeaderSize);
}
void writeToStringBuffer(StringBuffer buffer) {
@@ -902,26 +1157,36 @@
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');
- }
+ // Sometimes the assembler will generate an empty DWARF LNP, so don't check
+ // for non-empty LNPs.
return LineNumberProgram._(header, calculatedMatrix);
}
static Iterable<LineNumberState> _readOpcodes(
- Reader reader, LineNumberProgramHeader header) sync* {
+ Reader originalReader, LineNumberProgramHeader header) sync* {
final state = LineNumberState(header.defaultIsStatement);
+ final programSize = header.size - header._fullHeaderSize;
+ final reader = originalReader.refocusedCopy(
+ originalReader.start + originalReader.offset, programSize);
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();
+ state.basicBlock = false;
+ state.prologueEnd = false;
+ state.epilogueBegin = false;
+ state.discriminator = 0;
continue;
}
switch (opcode) {
@@ -935,7 +1200,7 @@
state.endSequence = true;
yield state.clone();
state.reset();
- break;
+ continue;
case 2: // DW_LNE_set_address
// The length includes the subopcode.
final valueLength = extendedLength - 1;
@@ -954,6 +1219,9 @@
case 1: // DW_LNS_copy
yield state.clone();
state.basicBlock = false;
+ state.prologueEnd = false;
+ state.epilogueBegin = false;
+ state.discriminator = 0;
break;
case 2: // DW_LNS_advance_pc
final increment = reader.readLEB128EncodedInteger();
@@ -975,15 +1243,27 @@
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);
break;
+ case 10: // DW_LNS_set_prologue_end (DWARF5)
+ state.prologueEnd = true;
+ break;
+ case 11: // DW_LNS_set_epilogue_begin (DWARF5)
+ state.epilogueBegin = true;
+ break;
+ case 12: // DW_LNS_set_isa (DWARF5)
+ state.isa = reader.readLEB128EncodedInteger();
+ break;
default:
throw FormatException('Standard opcode $opcode not in DWARF 2');
}
}
+ // Adjust the original reader to be at the same offset.
+ originalReader.seek(programSize);
}
bool containsKey(int address) {
@@ -1200,12 +1480,12 @@
other is PCOffset && offset == other.offset && section == other.section;
@override
- String toString() => 'PCOffset($section, $offset)';
+ String toString() => 'PCOffset($section, 0x${offset.toRadixString(16)})';
}
/// The DWARF debugging information for a Dart snapshot.
class Dwarf {
- final Elf _elf;
+ final DwarfContainer _container;
final Map<int, _AbbreviationsTable> _abbreviationsTables;
final DebugInfo _debugInfo;
final LineNumberInfo _lineNumberInfo;
@@ -1218,17 +1498,48 @@
/// 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) =>
+ // We use Zone values to pass around the string tables that may be used
+ // when parsing different sections.
+ runZoned(() {
+ final abbrevReader = container.abbreviationsTableReader(reader);
+ final abbreviationsTables = Map.fromEntries(abbrevReader
+ .readRepeatedWithOffsets(_AbbreviationsTable.fromReader));
+
+ final debugInfo = DebugInfo.fromReader(
+ container.debugInfoReader(reader), abbreviationsTables);
+
+ final lineNumberInfo =
+ LineNumberInfo.fromReader(container.lineNumberInfoReader(reader));
+
+ return Dwarf._(
+ container,
+ abbreviationsTables,
+ debugInfo,
+ lineNumberInfo,
+ container.vmStartAddress,
+ container.isolateStartAddress);
+ }, zoneValues: {
+ _debugStringTableKey: container.debugStringTable,
+ _debugLineStringTableKey: container.debugLineStringTable,
+ });
+
/// 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 +1552,12 @@
///
/// 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 +1571,7 @@
{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 = <CallInfo>[StubCallInfo(name: symbol.name, offset: offset)];
@@ -1358,7 +1627,7 @@
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 0000000..bb03a72
--- /dev/null
+++ b/pkg/native_stack_traces/lib/src/dwarf_container.dart
@@ -0,0 +1,31 @@
+// 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 debugStringTable;
+ DwarfContainerStringTable? get debugLineStringTable;
+
+ 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 c29b1e4..d8328eb 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 @@
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 @@
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 @@
_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) =>
@@ -636,17 +648,10 @@
..write(' Description: ')
..writeln(description);
}
-
- @override
- String toString() {
- final buffer = StringBuffer();
- writeToStringBuffer(buffer);
- return buffer.toString();
- }
}
/// 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<int, String> _entries;
StringTable._(entry, this._entries) : super._(entry);
@@ -658,8 +663,22 @@
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 +699,7 @@
enum SymbolBinding {
STB_LOCAL,
STB_GLOBAL,
+ STB_WEAK,
}
enum SymbolType {
@@ -696,15 +716,17 @@
}
/// 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 +753,6 @@
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 +769,9 @@
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 +821,13 @@
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,15 +954,17 @@
}
/// 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;
final Map<SectionHeaderEntry, Section> _sections;
final Map<String, Set<Section>> _sectionsByName;
+ final StringTable? _debugStringTable;
+ final StringTable? _debugLineStringTable;
Elf._(this._header, this._programHeader, this._sectionHeader, this._sections,
- this._sectionsByName);
+ this._sectionsByName, this._debugStringTable, this._debugLineStringTable);
/// Creates an [Elf] from [bytes].
///
@@ -984,16 +1008,24 @@
/// 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].
@@ -1070,13 +1102,79 @@
cacheSymbolNames('.strtab', '.symtab');
cacheSymbolNames('.dynstr', '.dynsym');
+
+ StringTable? debugStringTable;
+ if (sectionsByName.containsKey('.debug_str')) {
+ // Stored as PROGBITS, so need to explicitly parse as a string table.
+ debugStringTable = StringTable.fromReader(
+ reader, sectionsByName['.debug_str']!.single.headerEntry);
+ }
+
+ StringTable? debugLineStringTable;
+ if (sectionsByName.containsKey('.debug_line_str')) {
+ // Stored as PROGBITS, so need to explicitly parse as a string table.
+ debugLineStringTable = StringTable.fromReader(
+ reader, sectionsByName['.debug_line_str']!.single.headerEntry);
+ }
+
// Set the wordSize and endian of the original reader before returning.
elfReader.wordSize = reader.wordSize;
elfReader.endian = reader.endian;
- return Elf._(
- header, programHeader, sectionHeader, sections, sectionsByName);
+ return Elf._(header, programHeader, sectionHeader, sections, sectionsByName,
+ debugStringTable, debugLineStringTable);
}
+ @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();
+ }
+
+ @override
+ DwarfContainerStringTable? get debugStringTable => _debugStringTable;
+
+ @override
+ DwarfContainerStringTable? get debugLineStringTable => _debugLineStringTable;
+
+ @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 0000000..8fac18c
--- /dev/null
+++ b/pkg/native_stack_traces/lib/src/macho.dart
@@ -0,0 +1,560 @@
+// 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<int, String> _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<String, Symbol> _symbols;
+
+ SymbolTable._(this._symbols);
+
+ static SymbolTable fromReader(
+ Reader reader, int nsyms, StringTable stringTable) {
+ final symbols = <String, Symbol>{};
+ 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<String> get keys => _symbols.keys;
+ Iterable<Symbol> 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<String, Section> 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 = <String, Section>{};
+ 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<LoadCommand> _commands;
+ final SymbolTable _symbolTable;
+ final SegmentCommand _dwarfSegment;
+ final StringTable? _debugStringTable;
+ final StringTable? _debugLineStringTable;
+
+ MachO._(this._header, this._commands, this._symbolTable, this._dwarfSegment,
+ this._debugStringTable, this._debugLineStringTable);
+
+ 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<SymbolTableCommand>().single.load(reader);
+
+ final dwarfSegment = commands
+ .whereType<SegmentCommand?>()
+ .firstWhere((sc) => sc!.segname == '__DWARF', orElse: () => null);
+ if (dwarfSegment == null) {
+ print("No DWARF information in Mach-O file");
+ return null;
+ }
+
+ final debugStringTableSection = dwarfSegment.sections['__debug_str'];
+ StringTable? debugStringTable;
+ if (debugStringTableSection != null) {
+ debugStringTable =
+ StringTable.fromReader(debugStringTableSection.refocus(reader));
+ }
+
+ final debugLineStringTableSection =
+ dwarfSegment.sections['__debug_line_str'];
+ StringTable? debugLineStringTable;
+ if (debugLineStringTableSection != null) {
+ debugLineStringTable =
+ StringTable.fromReader(debugLineStringTableSection.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,
+ debugStringTable, debugLineStringTable);
+ }
+
+ 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 debugStringTable => _debugStringTable;
+
+ @override
+ DwarfContainerStringTable? get debugLineStringTable => _debugLineStringTable;
+
+ @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 8158878..71a8f83 100644
--- a/pkg/native_stack_traces/lib/src/reader.dart
+++ b/pkg/native_stack_traces/lib/src/reader.dart
@@ -28,8 +28,7 @@
Reader.fromTypedData(TypedData data, {int? wordSize, Endian? endian})
: _wordSize = wordSize,
_endian = endian,
- bdata =
- ByteData.view(data.buffer, data.offsetInBytes, data.lengthInBytes);
+ bdata = ByteData.sublistView(data);
Reader.fromFile(String path, {int? wordSize, Endian? endian})
: _wordSize = wordSize,
@@ -37,9 +36,15 @@
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);
}
@@ -49,9 +54,11 @@
int get length => bdata.lengthInBytes;
bool get done => _offset >= length;
+ Uint8List get bytes => Uint8List.sublistView(bdata);
+
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;
}
@@ -83,18 +90,38 @@
}
}
+ ByteData readRawBytes(int size) {
+ if (offset + size > length) {
+ throw ArgumentError('attempt to read $size bytes with only '
+ '${length - _offset} bytes remaining in the reader');
+ }
+ final start = _offset;
+ _offset += size;
+ return ByteData.sublistView(bdata, start, start + size);
+ }
+
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/pkg/native_stack_traces/pubspec.yaml b/pkg/native_stack_traces/pubspec.yaml
index 78b29eb..09ed948 100644
--- a/pkg/native_stack_traces/pubspec.yaml
+++ b/pkg/native_stack_traces/pubspec.yaml
@@ -1,10 +1,10 @@
name: native_stack_traces
-version: 0.4.6
+version: 0.5.0
description: Utilities for working with non-symbolic stack traces.
repository: https://github.com/dart-lang/sdk/tree/main/pkg/native_stack_traces
environment:
- sdk: '>=2.14.0 <3.0.0'
+ sdk: '>=2.17.0 <3.0.0'
executables:
decode:
diff --git a/runtime/lib/object.cc b/runtime/lib/object.cc
index 31d6f25..3f50303 100644
--- a/runtime/lib/object.cc
+++ b/runtime/lib/object.cc
@@ -318,6 +318,22 @@
return Object::null();
}
+DEFINE_NATIVE_ENTRY(Internal_randomInstructionsOffsetInsideAllocateObjectStub,
+ 0,
+ 0) {
+ auto& stub = Code::Handle(
+ zone, isolate->group()->object_store()->allocate_object_stub());
+ const uword entry = stub.EntryPoint();
+ const uword random_offset = isolate->random()->NextUInt32() % stub.Size();
+ // We return the offset into the isolate instructions instead of the full
+ // address because that fits into small Smis on 32-bit architectures or
+ // compressed pointer builds.
+ const uword instructions_start =
+ reinterpret_cast<uword>(isolate->source()->snapshot_instructions);
+ ASSERT(entry >= instructions_start);
+ return Smi::New((entry - instructions_start) + 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 c422f18..4db4fe0 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,7 @@
foo(c) => bar(c);
main() {
+ print(
+ VMInternalsForTesting.randomInstructionsOffsetInsideAllocateObjectStub());
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 d8cb33f..08936ff 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 @@
}
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,249 @@
]);
// Run the resulting Dwarf-AOT compiled script.
- final dwarfTrace1 = await runError(aotRuntime, <String>[
- '--dwarf-stack-traces-mode',
- scriptDwarfSnapshot,
- scriptDill,
- ]);
- final dwarfTrace2 = await runError(aotRuntime, <String>[
+
+ final output1 = await runTestProgram(aotRuntime,
+ <String>['--dwarf-stack-traces-mode', scriptDwarfSnapshot, scriptDill]);
+ final output2 = await runTestProgram(aotRuntime, <String>[
'--no-dwarf-stack-traces-mode',
scriptDwarfSnapshot,
- scriptDill,
+ scriptDill
]);
// Run the resulting non-Dwarf-AOT compiled script.
- final nonDwarfTrace1 = await runError(aotRuntime, <String>[
+ final nonDwarfTrace1 = (await runTestProgram(aotRuntime, <String>[
'--dwarf-stack-traces-mode',
scriptNonDwarfSnapshot,
scriptDill,
- ]);
- final nonDwarfTrace2 = await runError(aotRuntime, <String>[
+ ]))
+ .trace;
+ final nonDwarfTrace2 = (await runTestProgram(aotRuntime, <String>[
'--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)!;
+ // Currently there are no appropriate buildtools on the SIMARM and SIMARM64
+ // trybots as normally they compile to ELF and don't need them for compiling
+ // assembly snapshots.
+ if ((Platform.isLinux || Platform.isMacOS) &&
+ !buildDir.endsWith('SIMARM') &&
+ !buildDir.endsWith('SIMARM64')) {
+ 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, <String>[
+ // 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, <String>[
+ '--dwarf-stack-traces-mode',
+ scriptDwarfAssemblySnapshot,
+ scriptDill,
+ ]);
+ final assemblyOutput2 = await runTestProgram(aotRuntime, <String>[
+ '--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<String> trace;
+ final int allocateObjectInstructionsOffset;
+
+ DwarfTestOutput(this.trace, this.allocateObjectInstructionsOffset);
+}
+
+Future<void> compareTraces(List<String> 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!);
+ final translatedDwarfTrace1 =
+ await Stream.fromIterable(output1.trace).transform(decoder).toList();
- final translatedStackFrames = onlySymbolicFrameLines(translatedDwarfTrace1);
- final originalStackFrames = onlySymbolicFrameLines(nonDwarfTrace1);
+ final allocateObjectPCOffset1 = PCOffset(
+ output1.allocateObjectInstructionsOffset, InstructionsSection.isolate);
+ final allocateObjectPCOffset2 = PCOffset(
+ output2.allocateObjectInstructionsOffset, InstructionsSection.isolate);
- print('Stack frames from translated non-symbolic stack trace:');
- translatedStackFrames.forEach(print);
- print('');
+ print('Offset of first stub address is $allocateObjectPCOffset1');
+ print('Offset of second stub address is $allocateObjectPCOffset2');
- print('Stack frames from original symbolic stack trace:');
- originalStackFrames.forEach(print);
- print('');
+ final allocateObjectRelocatedAddress1 =
+ dwarf.virtualAddressOf(allocateObjectPCOffset1);
+ final allocateObjectRelocatedAddress2 =
+ dwarf.virtualAddressOf(allocateObjectPCOffset2);
- Expect.isTrue(translatedStackFrames.length > 0);
- Expect.isTrue(originalStackFrames.length > 0);
+ final allocateObjectCallInfo1 = dwarf.callInfoFor(
+ allocateObjectRelocatedAddress1,
+ includeInternalFrames: true);
+ final allocateObjectCallInfo2 = dwarf.callInfoFor(
+ allocateObjectRelocatedAddress2,
+ includeInternalFrames: true);
- // 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);
+ 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);
- print('Stack frames from translated non-symbolic stack trace, no columns:');
- columnStrippedTranslated.forEach(print);
- print('');
+ print("Successfully matched AllocateObject stub addresses");
+ print("");
- Expect.deepEquals(columnStrippedTranslated, originalStackFrames);
+ final translatedStackFrames = onlySymbolicFrameLines(translatedDwarfTrace1);
+ final originalStackFrames = onlySymbolicFrameLines(nonDwarfTrace);
- // 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 translated non-symbolic stack trace:');
+ translatedStackFrames.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;
+ print('Stack frames from original symbolic stack trace:');
+ originalStackFrames.forEach(print);
+ print('');
- final absTrace1 = absoluteAddresses(dwarfTrace1);
- final absTrace2 = absoluteAddresses(dwarfTrace2);
+ Expect.isTrue(translatedStackFrames.length > 0);
+ Expect.isTrue(originalStackFrames.length > 0);
- final relocatedFromDso1 = absTrace1.map((a) => a - dsoBase1);
- final relocatedFromDso2 = absTrace2.map((a) => a - dsoBase2);
+ // 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);
- Expect.deepEquals(relocatedFromDso1, relocatedFromDso2);
+ print('Stack frames from translated non-symbolic stack trace, no columns:');
+ columnStrippedTranslated.forEach(print);
+ print('');
- // 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);
+ Expect.deepEquals(columnStrippedTranslated, originalStackFrames);
- Expect.deepEquals(virtTrace1, virtTrace2);
+ // 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, tracePCOffsets1.map((o) => o.virtualAddressIn(dwarf)));
- Expect.deepEquals(
- virtTrace2, tracePCOffsets2.map((o) => o.virtualAddressIn(dwarf)));
+ // 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, relocatedFromDso1);
- Expect.deepEquals(virtTrace2, relocatedFromDso2);
- });
+ final absTrace1 = absoluteAddresses(output1.trace);
+ final absTrace2 = absoluteAddresses(output2.trace);
+
+ 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<DwarfTestOutput> runTestProgram(
+ String executable, List<String> 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 431fce5..159010b0 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 @@
return Directory(clangDir).existsSync() ? clangDir : null;
}
-Future<void> assembleSnapshot(String assemblyPath, String snapshotPath) async {
+Future<void> assembleSnapshot(String assemblyPath, String snapshotPath,
+ {bool debug = false}) async {
if (!Platform.isLinux && !Platform.isMacOS) {
throw "Unsupported platform ${Platform.operatingSystem} for assembling";
}
@@ -48,9 +49,7 @@
String cc = 'gcc';
String shared = '-shared';
- if (Platform.isMacOS) {
- cc = 'clang';
- } else if (buildDir.endsWith('SIMARM') || buildDir.endsWith('SIMARM64')) {
+ if (buildDir.endsWith('SIMARM') || buildDir.endsWith('SIMARM64')) {
final clangBuildTools = clangBuildToolsDir;
if (clangBuildTools != null) {
cc = path.join(clangBuildTools, 'clang');
@@ -58,21 +57,31 @@
throw 'Cannot assemble for ${path.basename(buildDir)} '
'without //buildtools on ${Platform.operatingSystem}';
}
+ } else if (Platform.isMacOS) {
+ cc = 'clang';
}
- if (Platform.isMacOS) {
- shared = '-dynamiclib';
- // Tell Mac linker to give up generating eh_frame from dwarf.
- ldFlags.add('-Wl,-no_compact_unwind');
- } else if (buildDir.endsWith('SIMARM')) {
+ if (buildDir.endsWith('SIMARM')) {
ccFlags.add('--target=armv7-linux-gnueabihf');
} else if (buildDir.endsWith('SIMARM64')) {
ccFlags.add('--target=aarch64-linux-gnu');
+ } else 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');
}
if (buildDir.endsWith('X64') || buildDir.endsWith('SIMARM64')) {
ccFlags.add('-m64');
}
+ if (debug) {
+ ccFlags.add('-g');
+ }
await run(cc, <String>[
...ccFlags,
@@ -93,8 +102,8 @@
var strip = 'strip';
- if ((Platform.isLinux &&
- (buildDir.endsWith('SIMARM') || buildDir.endsWith('SIMARM64'))) ||
+ if (buildDir.endsWith('SIMARM') ||
+ buildDir.endsWith('SIMARM64') ||
(Platform.isMacOS && forceElf)) {
final clangBuildTools = clangBuildToolsDir;
if (clangBuildTools != null) {
@@ -158,9 +167,7 @@
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<List<String>> runError(String executable, List<String> args) async {
@@ -172,9 +179,7 @@
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 30d90d5..9962f97 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 @@
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 0000000..51d3c4b
--- /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 2bd55a2..f79ada1 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 @@
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 5025c84..6dbd53b 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 @@
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 7ea2d60..8d9123f 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 @@
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 a4e399e..71b1d72 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,7 @@
foo(c) => bar(c);
main() {
+ print(
+ VMInternalsForTesting.randomInstructionsOffsetInsideAllocateObjectStub());
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 5850250..84cc657 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 @@
}
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,117 @@
]);
// Run the resulting Dwarf-AOT compiled script.
- final dwarfTrace1 = await runError(aotRuntime, <String>[
- '--dwarf-stack-traces-mode',
- scriptDwarfSnapshot,
- scriptDill,
- ]);
- final dwarfTrace2 = await runError(aotRuntime, <String>[
+
+ final output1 = await runTestProgram(aotRuntime,
+ <String>['--dwarf-stack-traces-mode', scriptDwarfSnapshot, scriptDill]);
+ final output2 = await runTestProgram(aotRuntime, <String>[
'--no-dwarf-stack-traces-mode',
scriptDwarfSnapshot,
- scriptDill,
+ scriptDill
]);
// Run the resulting non-Dwarf-AOT compiled script.
- final nonDwarfTrace1 = await runError(aotRuntime, <String>[
+ final nonDwarfTrace1 = (await runTestProgram(aotRuntime, <String>[
'--dwarf-stack-traces-mode',
scriptNonDwarfSnapshot,
scriptDill,
- ]);
- final nonDwarfTrace2 = await runError(aotRuntime, <String>[
+ ]))
+ .trace;
+ final nonDwarfTrace2 = (await runTestProgram(aotRuntime, <String>[
'--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);
+ // Currently there are no appropriate buildtools on the SIMARM and SIMARM64
+ // trybots as normally they compile to ELF and don't need them for compiling
+ // assembly snapshots.
+ if ((Platform.isLinux || Platform.isMacOS) &&
+ !buildDir.endsWith('SIMARM') &&
+ !buildDir.endsWith('SIMARM64')) {
+ 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, <String>[
+ // 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, <String>[
+ '--dwarf-stack-traces-mode',
+ scriptDwarfAssemblySnapshot,
+ scriptDill,
+ ]);
+ final assemblyOutput2 = await runTestProgram(aotRuntime, <String>[
+ '--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<String> trace;
+ final int allocateObjectInstructionsOffset;
+
+ DwarfTestOutput(this.trace, this.allocateObjectInstructionsOffset);
+}
+
+Future<void> compareTraces(List<String> 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 +198,131 @@
// 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);
+ final translatedDwarfTrace1 =
+ await Stream.fromIterable(output1.trace).transform(decoder).toList();
- final translatedStackFrames = onlySymbolicFrameLines(translatedDwarfTrace1);
- final originalStackFrames = onlySymbolicFrameLines(nonDwarfTrace1);
+ final allocateObjectPCOffset1 = PCOffset(
+ output1.allocateObjectInstructionsOffset, InstructionsSection.isolate);
+ final allocateObjectPCOffset2 = PCOffset(
+ output2.allocateObjectInstructionsOffset, InstructionsSection.isolate);
- print('Stack frames from translated non-symbolic stack trace:');
- translatedStackFrames.forEach(print);
- print('');
+ print('Offset of first stub address is $allocateObjectPCOffset1');
+ print('Offset of second stub address is $allocateObjectPCOffset2');
- print('Stack frames from original symbolic stack trace:');
- originalStackFrames.forEach(print);
- print('');
+ final allocateObjectRelocatedAddress1 =
+ dwarf.virtualAddressOf(allocateObjectPCOffset1);
+ final allocateObjectRelocatedAddress2 =
+ dwarf.virtualAddressOf(allocateObjectPCOffset2);
- Expect.isTrue(translatedStackFrames.length > 0);
- Expect.isTrue(originalStackFrames.length > 0);
+ final allocateObjectCallInfo1 = dwarf.callInfoFor(
+ allocateObjectRelocatedAddress1,
+ includeInternalFrames: true);
+ final allocateObjectCallInfo2 = dwarf.callInfoFor(
+ allocateObjectRelocatedAddress2,
+ includeInternalFrames: true);
- // 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);
+ 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);
- print('Stack frames from translated non-symbolic stack trace, no columns:');
- columnStrippedTranslated.forEach(print);
- print('');
+ print("Successfully matched AllocateObject stub addresses");
+ print("");
- Expect.deepEquals(columnStrippedTranslated, originalStackFrames);
+ final translatedStackFrames = onlySymbolicFrameLines(translatedDwarfTrace1);
+ final originalStackFrames = onlySymbolicFrameLines(nonDwarfTrace);
- // 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 translated non-symbolic stack trace:');
+ translatedStackFrames.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;
+ print('Stack frames from original symbolic stack trace:');
+ originalStackFrames.forEach(print);
+ print('');
- final absTrace1 = absoluteAddresses(dwarfTrace1);
- final absTrace2 = absoluteAddresses(dwarfTrace2);
+ Expect.isTrue(translatedStackFrames.length > 0);
+ Expect.isTrue(originalStackFrames.length > 0);
- final relocatedFromDso1 = absTrace1.map((a) => a - dsoBase1);
- final relocatedFromDso2 = absTrace2.map((a) => a - dsoBase2);
+ // 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);
- Expect.deepEquals(relocatedFromDso1, relocatedFromDso2);
+ print('Stack frames from translated non-symbolic stack trace, no columns:');
+ columnStrippedTranslated.forEach(print);
+ print('');
- // 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);
+ Expect.deepEquals(columnStrippedTranslated, originalStackFrames);
- Expect.deepEquals(virtTrace1, virtTrace2);
+ // 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, tracePCOffsets1.map((o) => o.virtualAddressIn(dwarf)));
- Expect.deepEquals(
- virtTrace2, tracePCOffsets2.map((o) => o.virtualAddressIn(dwarf)));
+ // 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, relocatedFromDso1);
- Expect.deepEquals(virtTrace2, relocatedFromDso2);
- });
+ final absTrace1 = absoluteAddresses(output1.trace);
+ final absTrace2 = absoluteAddresses(output2.trace);
+
+ 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<DwarfTestOutput> runTestProgram(
+ String executable, List<String> 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 b408f42..ecdc9a5 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 @@
return Directory(clangDir).existsSync() ? clangDir : null;
}
-Future<void> assembleSnapshot(String assemblyPath, String snapshotPath) async {
+Future<void> assembleSnapshot(String assemblyPath, String snapshotPath,
+ {bool debug = false}) async {
if (!Platform.isLinux && !Platform.isMacOS) {
throw "Unsupported platform ${Platform.operatingSystem} for assembling";
}
@@ -50,30 +51,39 @@
String cc = 'gcc';
String shared = '-shared';
- if (Platform.isMacOS) {
- cc = 'clang';
- } else if (buildDir.endsWith('SIMARM') || buildDir.endsWith('SIMARM64')) {
- if (clangBuildToolsDir != null) {
- cc = path.join(clangBuildToolsDir, 'clang');
+ if (buildDir.endsWith('SIMARM') || buildDir.endsWith('SIMARM64')) {
+ final clangBuildTools = clangBuildToolsDir;
+ if (clangBuildTools != null) {
+ cc = path.join(clangBuildTools, 'clang');
} else {
throw 'Cannot assemble for ${path.basename(buildDir)} '
'without //buildtools on ${Platform.operatingSystem}';
}
+ } else if (Platform.isMacOS) {
+ cc = 'clang';
}
- if (Platform.isMacOS) {
- shared = '-dynamiclib';
- // Tell Mac linker to give up generating eh_frame from dwarf.
- ldFlags.add('-Wl,-no_compact_unwind');
- } else if (buildDir.endsWith('SIMARM')) {
+ if (buildDir.endsWith('SIMARM')) {
ccFlags.add('--target=armv7-linux-gnueabihf');
} else if (buildDir.endsWith('SIMARM64')) {
ccFlags.add('--target=aarch64-linux-gnu');
+ } else 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');
}
if (buildDir.endsWith('X64') || buildDir.endsWith('SIMARM64')) {
ccFlags.add('-m64');
}
+ if (debug) {
+ ccFlags.add('-g');
+ }
await run(cc, <String>[
...ccFlags,
@@ -94,8 +104,8 @@
var strip = 'strip';
- if ((Platform.isLinux &&
- (buildDir.endsWith('SIMARM') || buildDir.endsWith('SIMARM64'))) ||
+ if (buildDir.endsWith('SIMARM') ||
+ buildDir.endsWith('SIMARM64') ||
(Platform.isMacOS && forceElf)) {
if (clangBuildToolsDir != null) {
strip = path.join(clangBuildToolsDir, 'llvm-strip');
@@ -158,9 +168,7 @@
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<List<String>> runError(String executable, List<String> args) async {
@@ -172,9 +180,7 @@
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 cd0ed39..1f9df8d 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 @@
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 0000000..c01efdb
--- /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 db548f4..ddee6b2 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 @@
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 a552ea0..e145e86 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 @@
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 4a054e8..0cb879a 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 @@
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 a380012..f7254b9 100644
--- a/runtime/vm/bootstrap_natives.h
+++ b/runtime/vm/bootstrap_natives.h
@@ -329,6 +329,7 @@
V(Internal_writeIntoOneByteString, 3) \
V(Internal_writeIntoTwoByteString, 3) \
V(Internal_deoptimizeFunctionsOnStack, 0) \
+ V(Internal_randomInstructionsOffsetInsideAllocateObjectStub, 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 c02b2f8..30d0176 100644
--- a/sdk/lib/_internal/vm/lib/internal_patch.dart
+++ b/sdk/lib/_internal/vm/lib/internal_patch.dart
@@ -179,6 +179,13 @@
@pragma("vm:external-name", "Internal_deoptimizeFunctionsOnStack")
external static void deoptimizeFunctionsOnStack();
+
+ // Used to verify that PC addresses in stubs can be named using DWARF info
+ // by returning an offset into the isolate instructions that should correspond
+ // to a known stub.
+ @pragma("vm:external-name",
+ "Internal_randomInstructionsOffsetInsideAllocateObjectStub")
+ external static int randomInstructionsOffsetInsideAllocateObjectStub();
}
@patch