Version 2.19.0-26.0.dev

Merge commit '27f6c6d660ee5115e9ec0975bd3fbacc3d7cf254' into 'dev'
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
diff --git a/tools/VERSION b/tools/VERSION
index ecc211e..65a2458 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 19
 PATCH 0
-PRERELEASE 25
+PRERELEASE 26
 PRERELEASE_PATCH 0
\ No newline at end of file