[vm] Add non-symbolic stack trace support for deferred loading units.

Dart VM changes:

Note that the following changes are backwards compatible in the
case that a Dart program has no deferred loading units (i.e., the
Dart program is contained in a single shared object snapshot).

When there are non-root loading units, the non-symbol stack trace
header now includes information about loading units as follows:

loading_unit: N, build_id: S, dso_base: A, instructions: A

where N is an integer, S is a string of hex digits (0-9a-f), and A
is a word-sized address printed as a hex string (without prefix).

In addition, all non-symbolic stack frames for isolate instructions
include a unit field, including those for the root loading unit, e.g.,

   #NN abs <address> unit <id> virt <address> <symbol>+<offset>

If there are no non-root loading units, then the non-symbolic stack
trace is unchanged from its previous format.

Adds a build ID to split deferred loading unit snapshots.
Fixes: https://github.com/dart-lang/sdk/issues/43516

If separate debugging information is requested, the loading unit
manifest includes a 'debugPath' field for each loading unit,
which contains the path to its separate debugging information.

Removes the attempt to store the relocated address of the instructions
section when running from an assembled snapshot in the initialized BSS.

Adds OS::GetAppDSOBase, which takes a pointer to the instructions
section and returns a pointer to its loaded shared object in memory.
For compiled-to-ELF snapshots, it does this using the relocated address
of the instructions in the Image, and for assembled snapshots, it
delegates to NativeSymbolResolver::LookupSharedObject.

-----

Changes to package:native_stack_traces:

PCOffset now has two new fields:

* int? unitId: the unit ID of the loading unit, when available.
* String? buildId: the build ID of the loading unit, when available.

For PCOffsets in the VM section, the unitId and buildId are those of
the root loading unit.

The constructor for the DwarfStackTraceDecoder now takes two
additional optional named arguments:

* Map<int, Dwarf>? dwarfByUnitId: A map associating loading unit IDs
  with the appropriate Dwarf object. May or may not contain an entry
  for the root loading unit.
* Iterable<Dwarf>? unitDwarfs: An iterable container holding Dwarf
  objects. May or may not contain an entry for the root loading unit.

The Dwarf object that is passed to the DwarfStackTraceDecoder as a
positional argument is used for all lookups within the root loading
unit. If the dwarfByUnitId or unitDwarfs arguments contain an entry
for the root loading unit, it should be the same as the positional
argument.

When decoding a non-symbolic stack frame with a non-root loading unit
id, the decoder first looks in the map for the appropriate Dwarf object.
If one is not found, the decoder uses the build ID for the loading unit
to find the appropriate Dwarf object in the iterable container. If an
appropriate Dwarf object cannot be found in either manner, the
non-symbolic stack frame is emitted without change.

The native_stack_traces:decode executable now takes two additional
multi-options for the translate command:

* -u, --unit_debug: Takes a path to the associated DWARF information.
* --unit_id_debug: Takes N=FILE, where N is the loading unit ID and
  FILE is a path to the associated DWARF information.

The arguments to -u are collected into an iterable container to be
passed as the unitDwarfs argument to the DwarfStackTraceDecoder, and
the arguments to --unit-id-debug are collected into a map to be passed
as the dwarfByUnitId argument.

TEST=vm/dart/use_dwarf_stack_traces_flag_deferred

Issue: https://github.com/dart-lang/sdk/issues/53902
Change-Id: I210d4f69e4ae9fd37275a96beb1aac55c5e9d080
Cq-Include-Trybots: luci.dart.try:vm-aot-dwarf-linux-product-x64-try,vm-aot-linux-release-x64-try,vm-aot-linux-debug-x64-try,vm-aot-mac-release-arm64-try,vm-aot-mac-product-arm64-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/362380
Reviewed-by: Ryan Macnak <rmacnak@google.com>
Commit-Queue: Tess Strickland <sstrickl@google.com>
diff --git a/pkg/native_stack_traces/CHANGELOG.md b/pkg/native_stack_traces/CHANGELOG.md
index 26985d0..afdcbab 100644
--- a/pkg/native_stack_traces/CHANGELOG.md
+++ b/pkg/native_stack_traces/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.5.7
+
+- Translates non-symbolic stack traces that include information for
+  deferred loading units.
+
 ## 0.5.6
 
 - Added retrieval of the static symbol table contents for use in Dart tests.
diff --git a/pkg/native_stack_traces/bin/decode.dart b/pkg/native_stack_traces/bin/decode.dart
index ca1f9c6..23d0f4c 100644
--- a/pkg/native_stack_traces/bin/decode.dart
+++ b/pkg/native_stack_traces/bin/decode.dart
@@ -27,6 +27,15 @@
 
 final ArgParser _translateParser =
     _createBaseDebugParser(ArgParser(allowTrailingOptions: true))
+      ..addMultiOption('unit_debug',
+          abbr: 'u',
+          help: 'filename containing debugging information'
+              ' for a deferred loading unit',
+          valueHelp: 'FILE')
+      ..addMultiOption('unit_id_debug',
+          help: 'ID and filename containing debugging information'
+              ' for a deferred loading unit',
+          valueHelp: 'N=FILE')
       ..addOption('input',
           abbr: 'i', help: 'Filename for processed input', valueHelp: 'FILE')
       ..addOption('output',
@@ -90,6 +99,23 @@
 non-symbolic stack traces converted to symbolic stack traces that contain
 function names, file names, and line numbers.
 
+If there are deferred loading units and their loading unit ids are known, then
+the debugging information for the deferred loading units can be specified
+using the --unit_id_debug command line option. E.g., if the debugging
+information for loading unit 5 is in debug_5.so and the information for
+loading unit 6 in debug_6.so, then the following command line arguments can
+be used:
+  --unit_id_debug 5=debug_5.so --unit_id_debug 6=debug_6.so
+or
+  --unit_id_debug 5=debug_5.so,6=debug_6.so
+
+If the loading unit ids are not known, but the build IDs in the debugging
+information match those of the deferred units, then the --unit_debug command
+line option (which can be abbreviated as -u) can be used:
+  -u debug_5.so -u debug_6.so
+or
+  -u debug_5.so,debug_6.so
+
 Options shared by all commands:
 ${_argParser.usage}
 
@@ -285,6 +311,8 @@
   }
 }
 
+final RegExp _unitDebugRE = RegExp(r'(\d+)=(.*)');
+
 Future<void> translate(ArgResults options) async {
   void usageError(String message) =>
       errorWithUsage(message, command: 'translate');
@@ -302,11 +330,41 @@
   final input = options['input'] != null
       ? io.File(path.canonicalize(path.normalize(options['input']))).openRead()
       : io.stdin;
+  Map<int, Dwarf>? dwarfByUnitId;
+  if (options['unit_id_debug'] != null) {
+    dwarfByUnitId = <int, Dwarf>{};
+    for (final unitArg in options['unit_id_debug']!) {
+      final match = _unitDebugRE.firstMatch(unitArg);
+      if (match == null) {
+        usageError('Expected N=FILE where N is an integer, got $unitArg');
+        return;
+      }
+      final unitId = int.parse(match[1]!);
+      final filename = match[2]!;
+      final dwarf = _loadFromFile(filename, usageError);
+      if (dwarf == null) return;
+      dwarfByUnitId[unitId] = dwarf;
+    }
+  }
+  List<Dwarf>? unitDwarfs;
+  if (options['unit_debug'] != null) {
+    unitDwarfs = <Dwarf>[];
+    for (final filename in options['unit_debug']!) {
+      final dwarf = _loadFromFile(filename, usageError);
+      if (dwarf == null) return;
+      unitDwarfs.add(dwarf);
+    }
+  }
 
   final convertedStream = input
       .transform(utf8.decoder)
       .transform(const LineSplitter())
-      .transform(DwarfStackTraceDecoder(dwarf, includeInternalFrames: verbose))
+      .transform(DwarfStackTraceDecoder(
+        dwarf,
+        includeInternalFrames: verbose,
+        dwarfByUnitId: dwarfByUnitId,
+        unitDwarfs: unitDwarfs,
+      ))
       .map((s) => '$s\n')
       .transform(utf8.encoder);
 
diff --git a/pkg/native_stack_traces/lib/src/constants.dart b/pkg/native_stack_traces/lib/src/constants.dart
index 4edfb41..979babb 100644
--- a/pkg/native_stack_traces/lib/src/constants.dart
+++ b/pkg/native_stack_traces/lib/src/constants.dart
@@ -20,3 +20,6 @@
 
 // The dynamic symbol name for the isolate data section.
 const String isolateDataSymbolName = '_kDartIsolateSnapshotData';
+
+// The ID for the root loading unit.
+const int rootLoadingUnitId = 1;
diff --git a/pkg/native_stack_traces/lib/src/convert.dart b/pkg/native_stack_traces/lib/src/convert.dart
index c2a5300..7dd554b 100644
--- a/pkg/native_stack_traces/lib/src/convert.dart
+++ b/pkg/native_stack_traces/lib/src/convert.dart
@@ -23,6 +23,31 @@
 final _osArchLineRE = RegExp(r'os(?:=|: )(\S+?),? '
     r'arch(?:=|: )(\S+?),? comp(?:=|: )(yes|no),? sim(?:=|: )(yes|no)');
 
+// A pattern matching a build ID in the non-symbolic stack trace header.
+//
+// This RegExp has been adjusted to parse the header line found in
+// non-symbolic stack traces and the modified version in signal handler stack
+// traces.
+const _buildIdREString = r"build_id(?:=|: )'([\da-f]+)'";
+final _buildIdRE = RegExp(_buildIdREString);
+
+// A pattern matching a loading unit in the non-symbolic stack trace header.
+//
+// This RegExp has been adjusted to parse the header line found in
+// non-symbolic stack traces and the modified version in signal handler stack
+// traces.
+final _loadingUnitLineRE = RegExp(r'loading_unit(?:=|: )([\d]+),? (?:' +
+    _buildIdREString +
+    r',? )?dso_base(?:=|: )([\da-f]+),? instructions(?:=|: )([\da-f]+)');
+
+// A pattern matching the isolate DSO base in the non-symbolic stack trace
+// header.
+//
+// This RegExp has been adjusted to parse the header line found in
+// non-symbolic stack traces and the modified version in signal handler stack
+// traces.
+final _isolateDsoBaseLineRE = RegExp(r'isolate_dso_base(?:=|: )([\da-f]+)');
+
 // A pattern matching the last line of the non-symbolic stack trace header.
 //
 // This RegExp has been adjusted to parse the header line found in
@@ -31,19 +56,68 @@
 final _instructionsLineRE = RegExp(r'isolate_instructions(?:=|: )([\da-f]+),? '
     r'vm_instructions(?:=|: )([\da-f]+)');
 
+/// Information for a loading unit.
+class LoadingUnit {
+  /// The id of the loading unit.
+  final int id;
+
+  /// The base address at which the loading unit was loaded.
+  final int dsoBase;
+
+  /// The address at which the loading unit instructions were loaded.
+  final int start;
+
+  /// The build ID of the loading unit, when available.
+  final String? buildId;
+
+  LoadingUnit(this.id, this.dsoBase, this.start, {this.buildId});
+
+  void writeToStringBuffer(StringBuffer buffer) {
+    buffer
+      ..write('LoadingUnit(')
+      ..write(id)
+      ..write(', start: ')
+      ..write(start.toRadixString(16))
+      ..write(', dso_base: ')
+      ..write(dsoBase.toRadixString(16));
+    if (buildId != null) {
+      buffer
+        ..write(", buildId: '")
+        ..write(buildId)
+        ..write("'");
+    }
+    buffer.write(")");
+  }
+
+  @override
+  String toString() {
+    final b = StringBuffer();
+    writeToStringBuffer(b);
+    return b.toString();
+  }
+}
+
 /// Header information for a non-symbolic Dart stack trace.
 class StackTraceHeader {
   String? _os;
   String? _arch;
   bool? _compressed;
   bool? _simulated;
+  String? _buildId;
+  Map<int, LoadingUnit>? _units;
   int? _isolateStart;
   int? _vmStart;
+  int? _isolateDsoBase;
 
   String? get os => _os;
   String? get architecture => _arch;
+  String? get buildId => _buildId;
+  Map<int, LoadingUnit>? get units => _units;
   bool? get compressedPointers => _compressed;
   bool? get usingSimulator => _simulated;
+  int? get vmStart => _vmStart;
+  int? get isolateStart => _isolateStart;
+  int? get isolateDsoBase => _isolateDsoBase;
 
   static StackTraceHeader fromStarts(int isolateStart, int vmStart,
           {String? architecture}) =>
@@ -52,11 +126,23 @@
         .._vmStart = vmStart
         .._arch = architecture;
 
+  static StackTraceHeader fromLines(List<String> lines, {bool lossy = false}) {
+    final result = StackTraceHeader();
+    for (final line in lines) {
+      result.tryParseHeaderLine(line, lossy: lossy);
+    }
+    return result;
+  }
+
   /// Try and parse the given line as one of the recognized lines in the
   /// header of a non-symbolic stack trace.
   ///
+  /// If [lossy] is true, then the parser assumes that some header lines may
+  /// have been lost (e.g., due to log truncation) and recreates missing parts
+  /// of the header from other parsed parts if possible.
+  ///
   /// Returns whether the line was recognized and parsed successfully.
-  bool tryParseHeaderLine(String line) {
+  bool tryParseHeaderLine(String line, {bool lossy = false}) {
     if (line.contains(_headerStartLine)) {
       // This is the start of a new non-symbolic stack trace, so reset all the
       // stored information to be parsed anew.
@@ -64,81 +150,215 @@
       _arch = null;
       _compressed = null;
       _simulated = null;
+      _buildId = null;
       _isolateStart = null;
+      _isolateDsoBase = null;
       _vmStart = null;
+      _units = null;
       return true;
     }
-    RegExpMatch? match = _osArchLineRE.firstMatch(line);
+    RegExpMatch? match;
+    match = _osArchLineRE.firstMatch(line);
     if (match != null) {
       _os = match[1]!;
       _arch = match[2]!;
       _compressed = match[3]! == "yes";
       _simulated = match[4]! == "yes";
-      // The architecture line always proceeds the instructions section line,
-      // so reset these to null just in case we missed the header line.
-      _isolateStart = null;
-      _vmStart = null;
+      if (lossy) {
+        // Reset all stored information that is parsed after this point,
+        // just in case we've missed earlier lines in this header.
+        _buildId = null;
+        _isolateStart = null;
+        _isolateDsoBase = null;
+        _vmStart = null;
+        _units = null;
+      }
+      return true;
+    }
+    // Have to check for loading units first because they can include a
+    // build ID, so the build ID RegExp matches them as well.
+    match = _loadingUnitLineRE.firstMatch(line);
+    if (match != null) {
+      _units ??= <int, LoadingUnit>{};
+      final id = int.parse(match[1]!);
+      final buildId = match[2];
+      final dsoBase = int.parse(match[3]!, radix: 16);
+      final start = int.parse(match[4]!, radix: 16);
+      _units![id] = LoadingUnit(id, dsoBase, start, buildId: buildId);
+      if (lossy) {
+        // Reset all stored information that is parsed after this point,
+        // just in case we've missed earlier lines in this header.
+        _isolateStart = null;
+        _isolateDsoBase = null;
+        _vmStart = null;
+      }
+      return true;
+    }
+    match = _buildIdRE.firstMatch(line);
+    if (match != null) {
+      _buildId = match[1]!;
+      if (lossy) {
+        // Reset all stored information that is parsed after this point,
+        // just in case we've missed earlier lines in this header.
+        _isolateStart = null;
+        _isolateDsoBase = null;
+        _vmStart = null;
+        _units = null;
+      }
+      return true;
+    }
+    match = _isolateDsoBaseLineRE.firstMatch(line);
+    if (match != null) {
+      _isolateDsoBase = int.parse(match[1]!, radix: 16);
+      if (lossy) {
+        // Reset all stored information that is parsed after this point,
+        // just in case we've missed earlier lines in this header.
+        _isolateStart = null;
+        _vmStart = null;
+      }
       return true;
     }
     match = _instructionsLineRE.firstMatch(line);
     if (match != null) {
       _isolateStart = int.parse(match[1]!, radix: 16);
       _vmStart = int.parse(match[2]!, radix: 16);
+      if (_units != null) {
+        final rootUnit = _units![constants.rootLoadingUnitId];
+        if (lossy && rootUnit == null) {
+          // We missed the header entry for the root loading unit, but it can
+          // be reconstructed from other header lines.
+          _units![constants.rootLoadingUnitId] = LoadingUnit(
+            constants.rootLoadingUnitId,
+            _isolateDsoBase!,
+            _isolateStart!,
+            buildId: _buildId,
+          );
+        } else {
+          assert(rootUnit != null);
+          assert(_isolateStart == rootUnit!.start);
+          assert(_isolateDsoBase == rootUnit!.dsoBase);
+          assert(_buildId == null || _buildId == rootUnit!.buildId);
+        }
+      }
       return true;
     }
     return false;
   }
 
+  // Returns the closest positive offset, unless both offsets are negative in
+  // which case it returns the negative offset closest to zero.
+  int _closestOffset(int offset1, int offset2) {
+    if (offset1 < 0) {
+      if (offset2 < 0) return max(offset1, offset2);
+      return offset2;
+    }
+    if (offset2 < 0) return offset1;
+    return min(offset1, offset2);
+  }
+
   /// The [PCOffset] for the given absolute program counter address.
   PCOffset? offsetOf(int address) {
     if (_isolateStart == null || _vmStart == null) return null;
-    final isolateOffset = address - _isolateStart!;
     var vmOffset = address - _vmStart!;
-    if (vmOffset > 0 && vmOffset == min(vmOffset, isolateOffset)) {
-      return PCOffset(vmOffset, InstructionsSection.vm,
-          os: _os,
-          architecture: _arch,
-          compressedPointers: _compressed,
-          usingSimulator: _simulated);
-    } else {
-      return PCOffset(isolateOffset, InstructionsSection.isolate,
-          os: _os,
-          architecture: _arch,
-          compressedPointers: _compressed,
-          usingSimulator: _simulated);
+    var unitOffset = address - _isolateStart!;
+    var unitBuildId = _buildId;
+    var unitId = constants.rootLoadingUnitId;
+    if (units != null) {
+      for (final unit in units!.values) {
+        final newOffset = address - unit.start;
+        if (newOffset == _closestOffset(unitOffset, newOffset)) {
+          unitOffset = newOffset;
+          unitBuildId = unit.buildId;
+          unitId = unit.id;
+        }
+      }
     }
+    if (unitOffset == _closestOffset(vmOffset, unitOffset)) {
+      return PCOffset(unitOffset, InstructionsSection.isolate,
+          os: _os,
+          architecture: _arch,
+          compressedPointers: _compressed,
+          usingSimulator: _simulated,
+          buildId: unitBuildId,
+          unitId: unitId);
+    }
+    // The VM section is always stored in the root loading unit.
+    return PCOffset(vmOffset, InstructionsSection.vm,
+        os: _os,
+        architecture: _arch,
+        compressedPointers: _compressed,
+        usingSimulator: _simulated,
+        buildId: _buildId,
+        unitId: constants.rootLoadingUnitId);
+  }
+
+  void writeToStringBuffer(StringBuffer buffer) {
+    var printedField = false;
+    void printField(String name, dynamic value) {
+      buffer
+        ..writeln(printedField ? ',' : '')
+        ..write('  ')
+        ..write(name)
+        ..write(': ')
+        ..write(value);
+      printedField = true;
+    }
+
+    buffer.write('StackTraceHeader(');
+    if (_vmStart != null) {
+      printField('vmStart', _vmStart!.toRadixString(16));
+    }
+    if (_isolateStart != null) {
+      printField('isolateStart', _isolateStart!.toRadixString(16));
+    }
+    if (_isolateDsoBase != null) {
+      printField('isolateDsoBase', _isolateDsoBase!.toRadixString(16));
+    }
+    if (_arch != null) {
+      final b = StringBuffer();
+      if (_simulated == true) {
+        b.write('SIM');
+      }
+      b.write(_arch!.toUpperCase());
+      if (_compressed == true) {
+        b.write('C');
+      }
+      printField('arch', b.toString());
+    } else {
+      if (_simulated != null) {
+        printField('simulated', _simulated);
+      }
+      if (_compressed != null) {
+        printField('compressed', _compressed);
+      }
+    }
+    if (_buildId != null) {
+      printField('buildId', "'$_buildId'");
+    }
+    if (_units != null) {
+      final b = StringBuffer();
+      b.writeln('{');
+      for (final unitId in _units!.keys) {
+        b.write('    $unitId => ');
+        _units![unitId]!.writeToStringBuffer(b);
+        b.writeln(',');
+      }
+      b.write('}');
+      printField('units', b.toString());
+    }
+    buffer.write(')');
+  }
+
+  @override
+  String toString() {
+    final buffer = StringBuffer();
+    writeToStringBuffer(buffer);
+    return buffer.toString();
   }
 }
 
-/// A Dart DWARF stack trace contains up to four pieces of information:
-///   - The zero-based frame index from the top of the stack.
-///   - The absolute address of the program counter.
-///   - The virtual address of the program counter, if the snapshot was
-///     loaded as a dynamic library, otherwise not present.
-///   - The location of the virtual address, which is one of the following:
-///     - A dynamic symbol name, a plus sign, and an integer offset.
-///     - The path to the snapshot, if it was loaded as a dynamic library,
-///       otherwise the string "<unknown>".
-const _symbolOffsetREString = r'(?<symbol>' +
-    constants.vmSymbolName +
-    r'|' +
-    constants.isolateSymbolName +
-    r')\+(?<offset>(?:0x)?[\da-f]+)';
-final _symbolOffsetRE = RegExp(_symbolOffsetREString);
-final _traceLineRE =
-    RegExp(r'\s*#(\d+) abs (?<absolute>[\da-f]+)(?: virt (?<virtual>[\da-f]+))?'
-        r' (?<rest>.*)$');
-
-/// Parses strings of the format <static symbol>+<integer offset>, where
-/// <static symbol> is one of the static symbols used for Dart instruction
-/// sections.
-///
-/// Unless forceHexadecimal is true, an integer offset without a "0x" prefix or
-/// any hexadecimal digits will be parsed as decimal.
-///
-/// Returns null if the string is not of the expected format.
-PCOffset? tryParseSymbolOffset(String s,
-    {bool forceHexadecimal = false, StackTraceHeader? header}) {
+(InstructionsSection, int)? _tryParseSymbolOffset(String s,
+    {bool forceHexadecimal = false}) {
   final match = _symbolOffsetRE.firstMatch(s);
   if (match == null) return null;
   final symbolString = match.namedGroup('symbol')!;
@@ -156,31 +376,90 @@
   if (offset == null) return null;
   switch (symbolString) {
     case constants.vmSymbolName:
-      return PCOffset(offset, InstructionsSection.vm,
-          os: header?.os,
-          architecture: header?.architecture,
-          compressedPointers: header?.compressedPointers,
-          usingSimulator: header?.usingSimulator);
+      return (InstructionsSection.vm, offset);
     case constants.isolateSymbolName:
-      return PCOffset(offset, InstructionsSection.isolate,
-          os: header?.os,
-          architecture: header?.architecture,
-          compressedPointers: header?.compressedPointers,
-          usingSimulator: header?.usingSimulator);
+      return (InstructionsSection.isolate, offset);
     default:
       break;
   }
   return null;
 }
 
+/// Parses strings of the format <static symbol>+<integer offset>, where
+/// <static symbol> is one of the static symbols used for Dart instruction
+/// sections.
+///
+/// Unless forceHexadecimal is true, an integer offset without a "0x" prefix or
+/// any hexadecimal digits will be parsed as decimal.
+///
+/// Assumes that the symbol should be resolved in the root loading unit.
+///
+/// Returns null if the string is not of the expected format.
+PCOffset? tryParseSymbolOffset(String s,
+    {bool forceHexadecimal = false,
+    String? buildId,
+    StackTraceHeader? header}) {
+  final result = _tryParseSymbolOffset(s, forceHexadecimal: forceHexadecimal);
+  if (result == null) return null;
+  return PCOffset(result.$2, result.$1,
+      os: header?.os,
+      architecture: header?.architecture,
+      compressedPointers: header?.compressedPointers,
+      usingSimulator: header?.usingSimulator,
+      buildId: header?.buildId,
+      unitId: constants.rootLoadingUnitId);
+}
+
+/// A Dart DWARF stack trace contains up to four pieces of information:
+///   - The zero-based frame index from the top of the stack.
+///   - The absolute address of the program counter.
+///   - The virtual address of the program counter, if the snapshot was
+///     loaded as a dynamic library, otherwise not present.
+///   - The location of the virtual address, which is one of the following:
+///     - A dynamic symbol name, a plus sign, and an integer offset.
+///     - The path to the snapshot, if it was loaded as a dynamic library,
+///       otherwise the string "<unknown>".
+const _symbolOffsetREString = r'(?<symbol>' +
+    constants.vmSymbolName +
+    r'|' +
+    constants.isolateSymbolName +
+    r')\+(?<offset>(?:0x)?[\da-f]+)';
+final _symbolOffsetRE = RegExp(_symbolOffsetREString);
+final _traceLineRE = RegExp(r'\s*#(\d+) abs (?<absolute>[\da-f]+)'
+    r'(?: unit (?<unitId>\d+))?'
+    r'(?: virt (?<virtual>[\da-f]+))?'
+    r' (?<rest>.*)$');
+
 PCOffset? _retrievePCOffset(StackTraceHeader header, RegExpMatch? match) {
   if (match == null) return null;
-  final restString = match.namedGroup('rest')!;
-  // Try checking for symbol information first, since we don't need the header
-  // information to translate it.
-  if (restString.isNotEmpty) {
-    final offset = tryParseSymbolOffset(restString, header: header);
-    if (offset != null) return offset;
+  // Retrieve the unit ID for this stack frame, if one was provided.
+  var unitId = constants.rootLoadingUnitId;
+  var buildId = header.buildId;
+  if (match.namedGroup('unitId') != null) {
+    unitId = int.parse(match.namedGroup('unitId')!);
+    final unit = header.units?[unitId];
+    if (unit == null) {
+      // The given non-root loading unit wasn't found in the header.
+      return null;
+    }
+    buildId = unit.buildId;
+  }
+  if (unitId == constants.rootLoadingUnitId) {
+    // Try checking for symbol information first, since we don't need the header
+    // information to translate it for the root loading unit.
+    final restString = match.namedGroup('rest')!;
+    if (restString.isNotEmpty) {
+      final result = _tryParseSymbolOffset(restString);
+      if (result != null) {
+        return PCOffset(result.$2, result.$1,
+            os: header.os,
+            architecture: header.architecture,
+            compressedPointers: header.compressedPointers,
+            usingSimulator: header.usingSimulator,
+            buildId: buildId,
+            unitId: unitId);
+      }
+    }
   }
   // If we're parsing the absolute address, we can only convert it into
   // a PCOffset if we saw the instructions line of the stack trace header.
@@ -192,23 +471,25 @@
   // depends on a version of Dart which only prints virtual addresses when the
   // virtual addresses in the snapshot are the same as in separately saved
   // debugging information, the other methods should be tried first.
-  final virtualString = match.namedGroup('virtual');
-  if (virtualString != null) {
-    final address = int.parse(virtualString, radix: 16);
+  if (match.namedGroup('virtual') != null) {
+    final address = int.parse(match.namedGroup('virtual')!, radix: 16);
     return PCOffset(address, InstructionsSection.none,
         os: header.os,
         architecture: header.architecture,
         compressedPointers: header.compressedPointers,
-        usingSimulator: header.usingSimulator);
+        usingSimulator: header.usingSimulator,
+        buildId: buildId,
+        unitId: unitId);
   }
   return null;
 }
 
 /// The [PCOffset]s for frames of the non-symbolic stack traces in [lines].
-Iterable<PCOffset> collectPCOffsets(Iterable<String> lines) sync* {
+Iterable<PCOffset> collectPCOffsets(Iterable<String> lines,
+    {bool lossy = false}) sync* {
   final header = StackTraceHeader();
   for (var line in lines) {
-    if (header.tryParseHeaderLine(line)) {
+    if (header.tryParseHeaderLine(line, lossy: lossy)) {
       continue;
     }
     final match = _traceLineRE.firstMatch(line);
@@ -243,10 +524,18 @@
 /// frame portion of the original line.
 class DwarfStackTraceDecoder extends StreamTransformerBase<String, String> {
   final Dwarf _dwarf;
+  final Map<int, Dwarf>? _dwarfByUnitId;
+  final Iterable<Dwarf>? _unitDwarfs;
   final bool _includeInternalFrames;
 
-  DwarfStackTraceDecoder(this._dwarf, {bool includeInternalFrames = false})
-      : _includeInternalFrames = includeInternalFrames;
+  DwarfStackTraceDecoder(
+    this._dwarf, {
+    Map<int, Dwarf>? dwarfByUnitId,
+    Iterable<Dwarf>? unitDwarfs,
+    bool includeInternalFrames = false,
+  })  : _dwarfByUnitId = dwarfByUnitId,
+        _unitDwarfs = unitDwarfs,
+        _includeInternalFrames = includeInternalFrames;
 
   @override
   Stream<String> bind(Stream<String> stream) async* {
@@ -254,7 +543,7 @@
     final header = StackTraceHeader();
     await for (final line in stream) {
       // If we successfully parse a header line, then we reset the depth to 0.
-      if (header.tryParseHeaderLine(line)) {
+      if (header.tryParseHeaderLine(line, lossy: true)) {
         depth = 0;
         yield line;
         continue;
@@ -263,7 +552,36 @@
       // line as a stack trace line, then just pass the line through unchanged.
       final lineMatch = _traceLineRE.firstMatch(line);
       final offset = _retrievePCOffset(header, lineMatch);
-      final callInfo = offset?.callInfoFrom(_dwarf,
+      if (offset == null) {
+        yield line;
+        continue;
+      }
+      Dwarf dwarf = _dwarf;
+      final unitId = offset.unitId;
+      if (unitId != null && unitId != constants.rootLoadingUnitId) {
+        Dwarf? unitDwarf;
+        // Prefer the map that specifies loading unit IDs over the iterable.
+        if (_dwarfByUnitId != null) {
+          unitDwarf = _dwarfByUnitId![unitId];
+        }
+        if (unitDwarf == null &&
+            _unitDwarfs != null &&
+            offset.buildId != null) {
+          for (final d in _unitDwarfs!) {
+            if (d.buildId(offset.architecture) == offset.buildId) {
+              unitDwarf = d;
+            }
+          }
+        }
+        // Don't attempt to translate if we couldn't find the correct debugging
+        // information for this loading unit.
+        if (unitDwarf == null) {
+          yield line;
+          continue;
+        }
+        dwarf = unitDwarf;
+      }
+      final callInfo = offset.callInfoFrom(dwarf,
           includeInternalFrames: _includeInternalFrames);
       if (callInfo == null) {
         yield line;
diff --git a/pkg/native_stack_traces/lib/src/dwarf.dart b/pkg/native_stack_traces/lib/src/dwarf.dart
index d0320a8..fc34557 100644
--- a/pkg/native_stack_traces/lib/src/dwarf.dart
+++ b/pkg/native_stack_traces/lib/src/dwarf.dart
@@ -1867,11 +1867,20 @@
   /// Whether the architecture was being simulated, when available.
   final bool? usingSimulator;
 
+  /// The build ID of the corresponding instructions section, when available.
+  final String? buildId;
+
+  /// The loading unit ID of the corresponding instructions section, when
+  /// available.
+  final int? unitId;
+
   PCOffset(this.offset, this.section,
       {this.os,
       this.architecture,
       this.compressedPointers,
-      this.usingSimulator});
+      this.usingSimulator,
+      this.buildId,
+      this.unitId});
 
   /// The virtual address for this [PCOffset] in [dwarf].
   int virtualAddressIn(Dwarf dwarf) => dwarf.virtualAddressOf(this);
@@ -1898,23 +1907,25 @@
       os == other.os &&
       architecture == other.architecture &&
       compressedPointers == other.compressedPointers &&
-      usingSimulator == other.usingSimulator;
+      usingSimulator == other.usingSimulator &&
+      buildId == other.buildId &&
+      unitId == other.unitId;
 
   @override
   String toString() {
     final buffer = StringBuffer();
     buffer
       ..write('PCOffset(')
-      ..write(section)
+      ..write(section.name)
       ..write(', 0x')
       ..write(offset.toRadixString(16));
     if (os != null) {
       buffer
-        ..write(', ')
+        ..write(', os: ')
         ..write(os!);
     }
     if (architecture != null) {
-      buffer.write(', ');
+      buffer.write(', architecture: ');
       if (usingSimulator ?? false) {
         buffer.write('SIM');
       }
@@ -1923,6 +1934,17 @@
         buffer.write('C');
       }
     }
+    if (buildId != null) {
+      buffer
+        ..write(", buildId: '")
+        ..write(buildId)
+        ..write("'");
+    }
+    if (unitId != null) {
+      buffer
+        ..write(', unitId: ')
+        ..write(unitId!);
+    }
     buffer.write(')');
     return buffer.toString();
   }
diff --git a/pkg/native_stack_traces/pubspec.yaml b/pkg/native_stack_traces/pubspec.yaml
index 34c4823..51d1118 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.5.6
+version: 0.5.7
 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.17.0 <3.0.0'
+  sdk: '>=3.0.0 <4.0.0'
 
 executables:
   decode:
diff --git a/runtime/bin/gen_snapshot.cc b/runtime/bin/gen_snapshot.cc
index ad7ea2e..e01c2c7 100644
--- a/runtime/bin/gen_snapshot.cc
+++ b/runtime/bin/gen_snapshot.cc
@@ -497,7 +497,7 @@
 
 static File* OpenLoadingUnitManifest() {
   File* manifest_file = OpenFile(loading_unit_manifest_filename);
-  if (!manifest_file->Print("{ \"loadingUnits\": [\n")) {
+  if (!manifest_file->Print("{ \"loadingUnits\": [\n ")) {
     PrintErrAndExit("Error: Unable to write file: %s\n\n",
                     loading_unit_manifest_filename);
   }
@@ -506,14 +506,19 @@
 
 static void WriteLoadingUnitManifest(File* manifest_file,
                                      intptr_t id,
-                                     const char* path) {
+                                     const char* path,
+                                     const char* debug_path = nullptr) {
   TextBuffer line(128);
   if (id != 1) {
-    line.AddString(",\n");
+    line.AddString(",\n ");
   }
-  line.Printf("{ \"id\": %" Pd ", \"path\": \"", id);
+  line.Printf("{\n  \"id\": %" Pd ",\n  \"path\": \"", id);
   line.AddEscapedString(path);
-  line.AddString("\", \"libraries\": [\n");
+  if (debug_path != nullptr) {
+    line.Printf("\",\n  \"debugPath\": \"");
+    line.AddEscapedString(debug_path);
+  }
+  line.AddString("\",\n  \"libraries\": [\n   ");
   Dart_Handle uris = Dart_LoadingUnitLibraryUris(id);
   CHECK_RESULT(uris);
   intptr_t length;
@@ -522,21 +527,21 @@
     const char* uri;
     CHECK_RESULT(Dart_StringToCString(Dart_ListGetAt(uris, i), &uri));
     if (i != 0) {
-      line.AddString(",\n");
+      line.AddString(",\n   ");
     }
     line.AddString("\"");
     line.AddEscapedString(uri);
     line.AddString("\"");
   }
-  line.AddString("]}");
-  if (!manifest_file->Print("%s\n", line.buffer())) {
+  line.AddString("\n  ]}");
+  if (!manifest_file->Print("%s", line.buffer())) {
     PrintErrAndExit("Error: Unable to write file: %s\n\n",
                     loading_unit_manifest_filename);
   }
 }
 
 static void CloseLoadingUnitManifest(File* manifest_file) {
-  if (!manifest_file->Print("] }\n")) {
+  if (!manifest_file->Print("]}\n")) {
     PrintErrAndExit("Error: Unable to write file: %s\n\n",
                     loading_unit_manifest_filename);
   }
@@ -556,19 +561,20 @@
   File* file = OpenFile(filename);
   *write_callback_data = file;
 
+  char* debug_filename = nullptr;
   if (debugging_info_filename != nullptr) {
-    char* debug_filename =
+    debug_filename =
         loading_unit_id == 1
             ? Utils::StrDup(debugging_info_filename)
             : Utils::SCreate("%s-%" Pd ".part.so", debugging_info_filename,
                              loading_unit_id);
     File* debug_file = OpenFile(debug_filename);
     *write_debug_callback_data = debug_file;
-    free(debug_filename);
   }
 
   WriteLoadingUnitManifest(reinterpret_cast<File*>(callback_data),
-                           loading_unit_id, filename);
+                           loading_unit_id, filename, debug_filename);
+  free(debug_filename);
 
   free(filename);
 }
diff --git a/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_deferred_program.dart b/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_deferred_program.dart
new file mode 100644
index 0000000..b50e12b
--- /dev/null
+++ b/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_deferred_program.dart
@@ -0,0 +1,23 @@
+// Copyright (c) 2024, 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.
+
+// Put the unused deferred library before the used one, to ensure that the
+// loaded units (the root loading unit and the loading unit for lib) are not
+// given consecutive unit IDs.
+import 'use_save_debugging_info_flag_program.dart' deferred as unused;
+import 'use_dwarf_stack_traces_flag_program.dart' deferred as lib;
+
+void main(List<String> args) async {
+  // The test never calls this program with arguments, so unused is not
+  // loaded during actual runs. This is just here to avoid the frontend
+  // being too clever by half.
+  if (!args.isEmpty) {
+    await unused.loadLibrary();
+  }
+  await lib.loadLibrary();
+
+  lib.main();
+}
diff --git a/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_deferred_test.dart b/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_deferred_test.dart
new file mode 100644
index 0000000..dd9c903
--- /dev/null
+++ b/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_deferred_test.dart
@@ -0,0 +1,584 @@
+// Copyright (c) 2024, 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.
+
+// This test checks that we can properly resolve non-symbolic stack frames
+// involving non-root deferred loading units. It also checks that
+// native_stack_traces/src/convert.dart is able to resolve such frames in two
+// different cases when applicable:
+// * When given a map of unit ids to DWARF objects (all cases)
+// * When given an iterable of DWARF objects, in which case it falls back
+//   on build ID lookup (direct ELF output only).
+
+import "dart:async";
+import "dart:convert";
+import "dart:io";
+
+import 'package:expect/expect.dart';
+import 'package:native_stack_traces/native_stack_traces.dart';
+import 'package:native_stack_traces/src/constants.dart' show rootLoadingUnitId;
+import 'package:native_stack_traces/src/convert.dart' show LoadingUnit;
+import 'package:native_stack_traces/src/macho.dart';
+import 'package:path/path.dart' as path;
+
+import 'use_flag_test_helper.dart';
+import 'use_dwarf_stack_traces_flag_test.dart' as original;
+
+Future<void> main() async {
+  await original.runTests(
+      'dwarf-flag-deferred-test',
+      path.join(sdkDir, 'runtime', 'tests', 'vm', 'dart',
+          'use_dwarf_stack_traces_flag_deferred_program.dart'),
+      testNonDwarf,
+      testElf,
+      testAssembly);
+}
+
+Manifest useSnapshotForDwarfPath(Manifest original,
+    {String? outputDir, String suffix = ''}) {
+  final entries = <int, ManifestEntry>{};
+  for (final id in original.ids) {
+    final oldEntry = original[id]!;
+    final snapshotBasename = oldEntry.snapshotBasename;
+    if (snapshotBasename == null) {
+      entries[id] = oldEntry.replaceDwarf(oldEntry.path);
+    } else {
+      entries[id] = oldEntry
+          .replaceDwarf(path.join(outputDir!, snapshotBasename + suffix));
+    }
+  }
+  return Manifest._(entries);
+}
+
+const _asmExt = '.S';
+const _soExt = '.so';
+
+Future<List<String>> testNonDwarf(String tempDir, String scriptDill) async {
+  final scriptNonDwarfUnitManifestPath =
+      path.join(tempDir, 'manifest_non_dwarf.json');
+  final scriptNonDwarfSnapshot = path.join(tempDir, 'non_dwarf' + _soExt);
+  await run(genSnapshot, <String>[
+    '--no-dwarf-stack-traces-mode',
+    '--loading-unit-manifest=$scriptNonDwarfUnitManifestPath',
+    '--snapshot-kind=app-aot-elf',
+    '--elf=$scriptNonDwarfSnapshot',
+    scriptDill,
+  ]);
+
+  final scriptNonDwarfUnitManifest =
+      Manifest.fromPath(scriptNonDwarfUnitManifestPath);
+  if (scriptNonDwarfUnitManifest == null) {
+    throw "Failure parsing manifest $scriptNonDwarfUnitManifestPath";
+  }
+  if (!scriptNonDwarfUnitManifest.contains(rootLoadingUnitId)) {
+    throw "Manifest '$scriptNonDwarfUnitManifestPath' "
+        "does not contain root unit info";
+  }
+  Expect.stringEquals(scriptNonDwarfSnapshot,
+      scriptNonDwarfUnitManifest[rootLoadingUnitId]!.path);
+
+  // Run the resulting non-Dwarf-AOT compiled script.
+  final nonDwarfTrace1 =
+      (await original.runTestProgram(dartPrecompiledRuntime, <String>[
+    '--dwarf-stack-traces-mode',
+    scriptNonDwarfSnapshot,
+  ]))
+          .trace;
+  final nonDwarfTrace2 =
+      (await original.runTestProgram(dartPrecompiledRuntime, <String>[
+    '--no-dwarf-stack-traces-mode',
+    scriptNonDwarfSnapshot,
+  ]))
+          .trace;
+
+  // Ensure the result is based off the flag passed to gen_snapshot, not
+  // the one passed to the runtime.
+  Expect.deepEquals(nonDwarfTrace1, nonDwarfTrace2);
+
+  return nonDwarfTrace1;
+}
+
+Future<void> testElf(
+    String tempDir, String scriptDill, List<String> nonDwarfTrace) async {
+  final scriptDwarfUnitManifestPath = path.join(tempDir, 'manifest_elf.json');
+  final scriptDwarfSnapshot = path.join(tempDir, 'dwarf' + _soExt);
+  final scriptDwarfDebugInfo = path.join(tempDir, 'debug_info' + _soExt);
+  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=$scriptDwarfDebugInfo',
+    '--loading-unit-manifest=$scriptDwarfUnitManifestPath',
+    '--snapshot-kind=app-aot-elf',
+    '--elf=$scriptDwarfSnapshot',
+    scriptDill,
+  ]);
+
+  final scriptDwarfUnitManifest =
+      Manifest.fromPath(scriptDwarfUnitManifestPath);
+  if (scriptDwarfUnitManifest == null) {
+    throw "Failure parsing manifest $scriptDwarfUnitManifestPath";
+  }
+  if (!scriptDwarfUnitManifest.contains(rootLoadingUnitId)) {
+    throw "Manifest '$scriptDwarfUnitManifest' "
+        "does not contain root unit info";
+  }
+  Expect.stringEquals(
+      scriptDwarfSnapshot, scriptDwarfUnitManifest[rootLoadingUnitId]!.path);
+
+  // Run the resulting Dwarf-AOT compiled script.
+
+  final output1 =
+      await original.runTestProgram(dartPrecompiledRuntime, <String>[
+    '--dwarf-stack-traces-mode',
+    scriptDwarfSnapshot,
+  ]);
+  final output2 =
+      await original.runTestProgram(dartPrecompiledRuntime, <String>[
+    '--no-dwarf-stack-traces-mode',
+    scriptDwarfSnapshot,
+  ]);
+
+  // Check with DWARF from separate debugging information.
+  await compareTraces(nonDwarfTrace, output1, output2, scriptDwarfUnitManifest);
+  // Check with DWARF in generated snapshot (e.g., replacing the Dwarf paths
+  // in the dwarf-stack-traces manifest, which point at the separate
+  // debugging information, with the output snapshot paths.)
+  final manifest = useSnapshotForDwarfPath(scriptDwarfUnitManifest);
+  await compareTraces(nonDwarfTrace, output1, output2, manifest);
+}
+
+Future<void> testAssembly(
+    String tempDir, String scriptDill, List<String> nonDwarfTrace) async {
+  // Currently there are no appropriate buildtools on the simulator trybots as
+  // normally they compile to ELF and don't need them for compiling assembly
+  // snapshots.
+  if (isSimulator || (!Platform.isLinux && !Platform.isMacOS)) return;
+
+  final scriptAssembly = path.join(tempDir, 'dwarf_assembly' + _asmExt);
+  final scriptDwarfAssemblyDebugInfo =
+      path.join(tempDir, 'dwarf_assembly_info' + _soExt);
+  final scriptDwarfAssemblyUnitManifestPath =
+      path.join(tempDir, 'manifest_assembly.json');
+
+  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',
+    '--loading-unit-manifest=$scriptDwarfAssemblyUnitManifestPath',
+    '--snapshot-kind=app-aot-assembly',
+    '--assembly=$scriptAssembly',
+    scriptDill,
+  ]);
+
+  final scriptDwarfAssemblyUnitManifest =
+      Manifest.fromPath(scriptDwarfAssemblyUnitManifestPath);
+  if (scriptDwarfAssemblyUnitManifest == null) {
+    throw "Failure parsing manifest $scriptDwarfAssemblyUnitManifest";
+  }
+  if (!scriptDwarfAssemblyUnitManifest.contains(rootLoadingUnitId)) {
+    throw "Manifest '$scriptDwarfAssemblyUnitManifest' "
+        "does not contain root unit info";
+  }
+  Expect.stringEquals(
+      scriptAssembly, scriptDwarfAssemblyUnitManifest[rootLoadingUnitId]!.path);
+  Expect.stringEquals(scriptDwarfAssemblyDebugInfo,
+      scriptDwarfAssemblyUnitManifest[rootLoadingUnitId]!.dwarfPath!);
+
+  for (final entry in scriptDwarfAssemblyUnitManifest.entries) {
+    Expect.isNotNull(entry.snapshotBasename);
+    final outputPath = path.join(tempDir, entry.snapshotBasename!);
+    await assembleSnapshot(entry.path, outputPath, debug: true);
+  }
+  final scriptDwarfAssemblySnapshot = path.join(tempDir,
+      scriptDwarfAssemblyUnitManifest[rootLoadingUnitId]!.snapshotBasename!);
+
+  // Run the resulting Dwarf-AOT compiled script.
+  final assemblyOutput1 =
+      await original.runTestProgram(dartPrecompiledRuntime, <String>[
+    '--dwarf-stack-traces-mode',
+    scriptDwarfAssemblySnapshot,
+    scriptDill,
+  ]);
+  final assemblyOutput2 =
+      await original.runTestProgram(dartPrecompiledRuntime, <String>[
+    '--no-dwarf-stack-traces-mode',
+    scriptDwarfAssemblySnapshot,
+    scriptDill,
+  ]);
+
+  // Check with DWARF from separate debugging information.
+  await compareTraces(nonDwarfTrace, assemblyOutput1, assemblyOutput2,
+      scriptDwarfAssemblyUnitManifest,
+      fromAssembly: true);
+  // Check with DWARF in assembled snapshot. Note that we get a separate .dSYM
+  // bundle on MacOS, so we need to add a '.dSYM' suffix there.
+  final manifest = useSnapshotForDwarfPath(scriptDwarfAssemblyUnitManifest,
+      outputDir: tempDir, suffix: Platform.isMacOS ? '.dSYM' : '');
+  await compareTraces(nonDwarfTrace, assemblyOutput1, assemblyOutput2, manifest,
+      fromAssembly: true);
+
+  // Next comes tests for MacOS universal binaries.
+  if (!Platform.isMacOS) return;
+
+  // Create empty MachO files (just a header) for each of the possible
+  // architectures.
+  final emptyFiles = <String, String>{};
+  for (final arch in original.machOArchNames.values) {
+    // Don't create an empty file for the current architecture.
+    if (arch == original.dartNameForCurrentArchitecture) continue;
+    final contents = emptyMachOForArchitecture(arch);
+    Expect.isNotNull(contents);
+    final emptyPath = path.join(tempDir, "empty_$arch.so");
+    await File(emptyPath).writeAsBytes(contents!, flush: true);
+    emptyFiles[arch] = emptyPath;
+  }
+
+  Future<void> testUniversalBinary(
+      String binaryPath, List<String> machoFiles) async {
+    await run(lipo, <String>[...machoFiles, '-create', '-output', binaryPath]);
+    final entries = <int, ManifestEntry>{};
+    for (final id in scriptDwarfAssemblyUnitManifest.ids) {
+      entries[id] = scriptDwarfAssemblyUnitManifest[id]!;
+      if (id == rootLoadingUnitId) {
+        entries[id] = entries[id]!.replaceDwarf(binaryPath);
+      }
+    }
+    final manifest = Manifest._(entries);
+    await compareTraces(
+        nonDwarfTrace, assemblyOutput1, assemblyOutput2, manifest,
+        fromAssembly: true);
+  }
+
+  final scriptDwarfAssemblyDebugSnapshotFile =
+      MachO.handleDSYM(manifest[rootLoadingUnitId]!.dwarfPath!);
+  await testUniversalBinary(path.join(tempDir, "ub-single"),
+      <String>[scriptDwarfAssemblyDebugSnapshotFile]);
+  await testUniversalBinary(path.join(tempDir, "ub-multiple"),
+      <String>[...emptyFiles.values, scriptDwarfAssemblyDebugSnapshotFile]);
+}
+
+Future<void> compareTraces(
+    List<String> nonDwarfTrace,
+    original.DwarfTestOutput output1,
+    original.DwarfTestOutput output2,
+    Manifest manifest,
+    {bool fromAssembly = false}) async {
+  Expect.isNotNull(manifest[rootLoadingUnitId]);
+
+  final header1 = StackTraceHeader.fromLines(output1.trace);
+  print('Header1 = $header1');
+  checkHeaderWithUnits(header1, fromAssembly: fromAssembly);
+  final header2 = StackTraceHeader.fromLines(output2.trace);
+  print('Header2 = $header2');
+  checkHeaderWithUnits(header2, fromAssembly: fromAssembly);
+
+  // 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 = collectPCOffsetsByUnit(output1.trace);
+  print("PCOffsets from trace 1:");
+  printByUnit(tracePCOffsets1);
+  final tracePCOffsets2 = collectPCOffsetsByUnit(output2.trace);
+  print("PCOffsets from trace 2:");
+  printByUnit(tracePCOffsets2);
+
+  Expect.deepEquals(tracePCOffsets1, tracePCOffsets2);
+
+  Expect.isNotNull(tracePCOffsets1[rootLoadingUnitId]);
+  Expect.isNotEmpty(tracePCOffsets1[rootLoadingUnitId]!);
+  final sampleOffset = tracePCOffsets1[rootLoadingUnitId]!.first;
+
+  // Only retrieve the DWARF objects that we need to decode the stack traces.
+  final dwarfByUnitId = <int, Dwarf>{};
+  for (final id in tracePCOffsets1.keys.toSet()) {
+    Expect.isTrue(header2.units!.containsKey(id));
+    final dwarfPath = manifest[id]!.dwarfPath;
+    Expect.isNotNull(dwarfPath);
+    print("Reading dwarf for unit $id from $dwarfPath}");
+    final dwarf = Dwarf.fromFile(dwarfPath!);
+    Expect.isNotNull(dwarf);
+    dwarfByUnitId[id] = dwarf!;
+  }
+  // The first non-root loading unit is not loaded and so shouldn't appear in
+  // the stack trace at all, but the root and second non-root loading units do.
+  Expect.isTrue(dwarfByUnitId.containsKey(rootLoadingUnitId));
+  Expect.isFalse(dwarfByUnitId.containsKey(rootLoadingUnitId + 1));
+  Expect.isTrue(dwarfByUnitId.containsKey(rootLoadingUnitId + 2));
+  final rootDwarf = dwarfByUnitId[rootLoadingUnitId]!;
+
+  original.checkRootUnitAssumptions(output1, output2, rootDwarf,
+      sampleOffset: sampleOffset, matchingBuildIds: !fromAssembly);
+
+  // The offsets of absolute addresses from their respective DSO base
+  // should be the same for both traces.
+  final absTrace1 = absoluteAddresses(output1.trace);
+  print("Absolute addresses from trace 1:");
+  printByUnit(absTrace1, toString: addressString);
+
+  final absTrace2 = absoluteAddresses(output2.trace);
+  print("Absolute addresses from trace 2:");
+  printByUnit(absTrace2, toString: addressString);
+
+  final dsoBase1 = <int, int>{};
+  for (final unit in header1.units!.values) {
+    dsoBase1[unit.id] = unit.dsoBase;
+  }
+  print("DSO bases for trace 1:");
+  for (final unitId in dsoBase1.keys) {
+    print("  $unitId => 0x${dsoBase1[unitId]!.toRadixString(16)}");
+  }
+
+  final dsoBase2 = <int, int>{};
+  for (final unit in header2.units!.values) {
+    dsoBase2[unit.id] = unit.dsoBase;
+  }
+  print("DSO bases for trace 2:");
+  for (final unitId in dsoBase2.keys) {
+    print("  $unitId => 0x${dsoBase2[unitId]!.toRadixString(16)}");
+  }
+
+  final relocatedFromDso1 = Map.fromEntries(absTrace1.keys.map((unitId) =>
+      MapEntry(unitId, absTrace1[unitId]!.map((a) => a - dsoBase1[unitId]!))));
+  print("Relocated addresses from trace 1:");
+  printByUnit(relocatedFromDso1, toString: addressString);
+
+  final relocatedFromDso2 = Map.fromEntries(absTrace2.keys.map((unitId) =>
+      MapEntry(unitId, absTrace2[unitId]!.map((a) => a - dsoBase2[unitId]!))));
+  print("Relocated addresses from trace 2:");
+  printByUnit(relocatedFromDso2, toString: addressString);
+
+  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.
+  //
+  // Note that since only addresses in the root loading unit are marked with
+  // 'virt', we don't need to handle other loading units here.
+  final virtTrace1 = explicitVirtualAddresses(output1.trace);
+  print("Virtual addresses in frames from trace 1:");
+  printByUnit(virtTrace1, toString: addressString);
+  final virtTrace2 = explicitVirtualAddresses(output2.trace);
+  print("Virtual addresses in frames from trace 2:");
+  printByUnit(virtTrace2, toString: addressString);
+
+  final fromTracePCOffsets1 = <int, Iterable<int>>{};
+  for (final unitId in tracePCOffsets1.keys) {
+    final dwarf = dwarfByUnitId[unitId];
+    Expect.isNotNull(dwarf);
+    fromTracePCOffsets1[unitId] =
+        tracePCOffsets1[unitId]!.map((o) => o.virtualAddressIn(dwarf!));
+  }
+  print("Virtual addresses calculated from PCOffsets in trace 1:");
+  printByUnit(fromTracePCOffsets1, toString: addressString);
+  final fromTracePCOffsets2 = <int, Iterable<int>>{};
+  for (final unitId in tracePCOffsets2.keys) {
+    final dwarf = dwarfByUnitId[unitId];
+    Expect.isNotNull(dwarf);
+    fromTracePCOffsets2[unitId] =
+        tracePCOffsets2[unitId]!.map((o) => o.virtualAddressIn(dwarf!));
+  }
+  print("Virtual addresses calculated from PCOffsets in trace 2:");
+  printByUnit(fromTracePCOffsets2, toString: addressString);
+
+  Expect.deepEquals(virtTrace1, virtTrace2);
+  Expect.deepEquals(virtTrace1, fromTracePCOffsets1);
+  Expect.deepEquals(virtTrace2, fromTracePCOffsets2);
+  Expect.deepEquals(virtTrace1, relocatedFromDso1);
+  Expect.deepEquals(virtTrace2, relocatedFromDso2);
+
+  // Check that translating the DWARF stack trace (without internal frames)
+  // matches the symbolic stack trace, and that for ELF outputs, we can also
+  // decode using the build IDs instead of a unit ID to unit map.
+  final decoder =
+      DwarfStackTraceDecoder(rootDwarf, dwarfByUnitId: dwarfByUnitId);
+  final translatedDwarfTrace1 =
+      await Stream.fromIterable(output1.trace).transform(decoder).toList();
+  if (!fromAssembly) {
+    final unitDwarfs = dwarfByUnitId.values;
+    final decoder2 = DwarfStackTraceDecoder(rootDwarf, unitDwarfs: unitDwarfs);
+    final translatedDwarfTrace2 =
+        await Stream.fromIterable(output1.trace).transform(decoder2).toList();
+    Expect.deepEquals(translatedDwarfTrace1, translatedDwarfTrace2);
+  }
+
+  original.checkTranslatedTrace(nonDwarfTrace, translatedDwarfTrace1);
+}
+
+void checkHeaderWithUnits(StackTraceHeader header,
+    {bool fromAssembly = false}) {
+  original.checkHeader(header);
+  // Additional requirements for the deferred test.
+  Expect.isNotNull(header.units);
+  // There should be an entry included for the root loading unit.
+  Expect.isNotNull(header.units![rootLoadingUnitId]);
+  // The first non-root loading unit is never loaded by the test program.
+  // Verify that it is not listed for direct-to-ELF snapshots. (It may be
+  // eagerly loaded in assembly snapshots.)
+  if (!fromAssembly) {
+    Expect.isNull(header.units![rootLoadingUnitId + 1]);
+  }
+  // There should be an entry included for the second non-root loading unit.
+  Expect.isNotNull(header.units![rootLoadingUnitId + 2]);
+  for (final unitId in header.units!.keys) {
+    final unit = header.units![unitId]!;
+    Expect.equals(unitId, unit.id);
+    Expect.isNotNull(unit.buildId);
+  }
+  // The information for the root loading unit should match the non-loading
+  // unit information in the header.
+  Expect.equals(header.isolateStart!, header.units![rootLoadingUnitId]!.start!);
+  Expect.equals(
+      header.isolateDsoBase!, header.units![rootLoadingUnitId]!.dsoBase!);
+  Expect.stringEquals(
+      header.buildId!, header.units![rootLoadingUnitId]!.buildId!);
+}
+
+Map<int, Iterable<PCOffset>> collectPCOffsetsByUnit(Iterable<String> lines) {
+  final result = <int, List<PCOffset>>{};
+  for (final o in collectPCOffsets(lines)) {
+    final unitId = o.unitId!;
+    result[unitId] ??= <PCOffset>[];
+    result[unitId]!.add(o);
+  }
+  return result;
+}
+
+final _unitRE = RegExp(r' unit (\d+)');
+
+// Unlike in the original, we want to also collect addressed based on the
+// loading unit.
+Map<int, Iterable<int>> parseUsingAddressRegExp(
+    RegExp re, Iterable<String> lines) {
+  final result = <int, List<int>>{};
+  for (final line in lines) {
+    var match = re.firstMatch(line);
+    if (match != null) {
+      final address = int.parse(match.group(1)!, radix: 16);
+      var unitId = rootLoadingUnitId;
+      match = _unitRE.firstMatch(line);
+      if (match != null) {
+        unitId = int.parse(match.group(1)!);
+      }
+      result[unitId] ??= <int>[];
+      result[unitId]!.add(address);
+    }
+  }
+  return result;
+}
+
+final _absRE = RegExp(r'abs ([a-f\d]+)');
+
+Map<int, Iterable<int>> absoluteAddresses(Iterable<String> lines) =>
+    parseUsingAddressRegExp(_absRE, lines);
+
+final _virtRE = RegExp(r'virt ([a-f\d]+)');
+
+Map<int, Iterable<int>> explicitVirtualAddresses(Iterable<String> lines) =>
+    parseUsingAddressRegExp(_virtRE, lines);
+
+void printByUnit<X>(Map<int, Iterable<X>> valuesByUnit,
+    {String Function(X) toString = objectString}) {
+  for (final unitId in valuesByUnit.keys) {
+    print("  For unit $unitId:");
+    for (final value in valuesByUnit[unitId]!) {
+      print("    * ${toString(value)}");
+    }
+  }
+}
+
+String objectString(dynamic object) => object.toString();
+String addressString(int address) => address.toRadixString(16);
+
+class ManifestEntry {
+  final int id;
+  final String path;
+  final String? dwarfPath;
+  final String? snapshotBasename;
+
+  const ManifestEntry._(this.id, this.path,
+      {this.dwarfPath, this.snapshotBasename});
+
+  static const _idKey = "id";
+  static const _pathKey = "path";
+  static const _dwarfPathKey = "debugPath";
+
+  static ManifestEntry? fromJson(Map<String, dynamic> entry) {
+    final int? id = entry[_idKey];
+    if (id == null) return null;
+    final String? path = entry[_pathKey];
+    if (path == null) return null;
+    if (!File(path).existsSync()) return null;
+    final String? dwarfPath = entry[_dwarfPathKey];
+    if (dwarfPath != null) {
+      if (!File(dwarfPath).existsSync()) return null;
+    }
+    return ManifestEntry._(id, path, dwarfPath: dwarfPath);
+  }
+
+  ManifestEntry replaceSnapshotBasename(String basename) =>
+      ManifestEntry._(id, path,
+          dwarfPath: dwarfPath, snapshotBasename: basename);
+
+  ManifestEntry replaceDwarf(String newPath) => ManifestEntry._(id, path,
+      dwarfPath: newPath, snapshotBasename: snapshotBasename);
+}
+
+class Manifest {
+  final Map<int, ManifestEntry> _map;
+
+  const Manifest._(this._map);
+
+  static Manifest? fromJson(Map<String, dynamic> manifestJson) {
+    final entriesJson = manifestJson["loadingUnits"];
+    if (entriesJson == null) return null;
+    final entryMap = <int, ManifestEntry>{};
+    for (final entryJson in entriesJson) {
+      final entry = ManifestEntry.fromJson(entryJson);
+      if (entry == null) return null;
+      entryMap[entry.id] = entry;
+    }
+    final rootEntry = entryMap[rootLoadingUnitId];
+    if (rootEntry == null) return null;
+    if (rootEntry.path.endsWith(_asmExt)) {
+      // Add the expected basenames for the assembled snapshots.
+      var basename = path.basename(rootEntry.path);
+      basename =
+          basename.replaceRange(basename.length - _asmExt.length, null, _soExt);
+      entryMap[rootLoadingUnitId] = rootEntry.replaceSnapshotBasename(basename);
+      for (final id in entryMap.keys) {
+        if (id == rootLoadingUnitId) continue;
+        final entry = entryMap[id]!;
+        // Note that this must match the suffix added to the snapshot URI
+        // in Loader::DeferredLoadHandler.
+        entryMap[id] = entryMap[id]!
+            .replaceSnapshotBasename(basename + '-$id.part' + _soExt);
+      }
+    }
+    return Manifest._(entryMap);
+  }
+
+  static Manifest? fromPath(String path) {
+    final file = File(path);
+    if (!file.existsSync()) return null;
+    return fromJson(json.decode(file.readAsStringSync()));
+  }
+
+  int get length => _map.length;
+
+  bool contains(int i) => _map.containsKey(i);
+  ManifestEntry? operator [](int i) => _map[i];
+
+  Iterable<int> get ids => _map.keys;
+  Iterable<ManifestEntry> get entries => _map.values;
+}
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 b949287..54a6356 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
@@ -17,7 +17,22 @@
 
 import 'use_flag_test_helper.dart';
 
-main(List<String> args) async {
+Future<void> main() async {
+  await runTests(
+      'dwarf-flag-test',
+      path.join(sdkDir, 'runtime', 'tests', 'vm', 'dart',
+          'use_dwarf_stack_traces_flag_program.dart'),
+      testNonDwarf,
+      testElf,
+      testAssembly);
+}
+
+Future<void> runTests(
+    String tempPrefix,
+    String scriptPath,
+    Future<List<String>> Function(String, String) testNonDwarf,
+    Future<void> Function(String, String, List<String>) testElf,
+    Future<void> Function(String, String, List<String>) testAssembly) async {
   if (!isAOTRuntime) {
     return; // Running in JIT: AOT binaries not available.
   }
@@ -37,12 +52,10 @@
     throw "Cannot run test as $platformDill does not exist";
   }
 
-  await withTempDir('dwarf-flag-test', (String tempDir) async {
+  await withTempDir(tempPrefix, (String tempDir) async {
     // 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.
@@ -51,73 +64,79 @@
       '--platform=$platformDill',
       '-o',
       scriptDill,
-      script,
+      scriptPath,
     ]);
 
-    // Run the AOT compiler with/without Dwarf stack traces.
-    final scriptDwarfSnapshot = path.join(tempDir, 'dwarf.so');
-    final scriptNonDwarfSnapshot = path.join(tempDir, 'non_dwarf.so');
-    final scriptDwarfDebugInfo = path.join(tempDir, 'debug_info.so');
-    await Future.wait(<Future>[
-      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=$scriptDwarfDebugInfo',
-        '--snapshot-kind=app-aot-elf',
-        '--elf=$scriptDwarfSnapshot',
-        scriptDill,
-      ]),
-      run(genSnapshot, <String>[
-        '--no-dwarf-stack-traces-mode',
-        '--snapshot-kind=app-aot-elf',
-        '--elf=$scriptNonDwarfSnapshot',
-        scriptDill,
-      ]),
-    ]);
+    final nonDwarfTrace = await testNonDwarf(tempDir, scriptDill);
 
-    // Run the resulting Dwarf-AOT compiled script.
+    await testElf(tempDir, scriptDill, nonDwarfTrace);
 
-    final output1 = await runTestProgram(dartPrecompiledRuntime,
-        <String>['--dwarf-stack-traces-mode', scriptDwarfSnapshot, scriptDill]);
-    final output2 = await runTestProgram(dartPrecompiledRuntime, <String>[
-      '--no-dwarf-stack-traces-mode',
-      scriptDwarfSnapshot,
-      scriptDill
-    ]);
-
-    // Run the resulting non-Dwarf-AOT compiled script.
-    final nonDwarfTrace1 =
-        (await runTestProgram(dartPrecompiledRuntime, <String>[
-      '--dwarf-stack-traces-mode',
-      scriptNonDwarfSnapshot,
-      scriptDill,
-    ]))
-            .trace;
-    final nonDwarfTrace2 =
-        (await runTestProgram(dartPrecompiledRuntime, <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);
-
-    // 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);
-
-    await testAssembly(tempDir, scriptDill, nonDwarfTrace1);
+    await testAssembly(tempDir, scriptDill, nonDwarfTrace);
   });
 }
 
-const _lipoBinary = "/usr/bin/lipo";
+Future<List<String>> testNonDwarf(String tempDir, String scriptDill) async {
+  final scriptNonDwarfSnapshot = path.join(tempDir, 'non_dwarf.so');
+
+  await run(genSnapshot, <String>[
+    '--no-dwarf-stack-traces-mode',
+    '--snapshot-kind=app-aot-elf',
+    '--elf=$scriptNonDwarfSnapshot',
+    scriptDill,
+  ]);
+
+  // Run the resulting non-Dwarf-AOT compiled script.
+  final nonDwarfTrace1 = (await runTestProgram(dartPrecompiledRuntime, <String>[
+    '--dwarf-stack-traces-mode',
+    scriptNonDwarfSnapshot,
+    scriptDill,
+  ]))
+      .trace;
+  final nonDwarfTrace2 = (await runTestProgram(dartPrecompiledRuntime, <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);
+
+  return nonDwarfTrace1;
+}
+
+Future<void> testElf(
+    String tempDir, String scriptDill, List<String> nonDwarfTrace) async {
+  final scriptDwarfSnapshot = path.join(tempDir, 'dwarf.so');
+  final scriptDwarfDebugInfo = path.join(tempDir, 'debug_info.so');
+  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=$scriptDwarfDebugInfo',
+    '--snapshot-kind=app-aot-elf',
+    '--elf=$scriptDwarfSnapshot',
+    scriptDill,
+  ]);
+
+  // Run the resulting Dwarf-AOT compiled script.
+
+  final output1 = await runTestProgram(dartPrecompiledRuntime,
+      <String>['--dwarf-stack-traces-mode', scriptDwarfSnapshot, scriptDill]);
+  final output2 = await runTestProgram(dartPrecompiledRuntime, <String>[
+    '--no-dwarf-stack-traces-mode',
+    scriptDwarfSnapshot,
+    scriptDill
+  ]);
+
+  // Check with DWARF from separate debugging information.
+  await compareTraces(nonDwarfTrace, output1, output2, scriptDwarfDebugInfo);
+  // Check with DWARF in generated snapshot.
+  await compareTraces(nonDwarfTrace, output1, output2, scriptDwarfSnapshot);
+}
 
 Future<void> testAssembly(
     String tempDir, String scriptDill, List<String> nonDwarfTrace) async {
@@ -173,15 +192,10 @@
   // Next comes tests for MacOS universal binaries.
   if (!Platform.isMacOS) return;
 
-  // Test this before continuing.
-  if (!await File(_lipoBinary).exists()) {
-    Expect.fail("missing lipo binary");
-  }
-
   // Create empty MachO files (just a header) for each of the possible
   // architectures.
   final emptyFiles = <String, String>{};
-  for (final arch in _machOArchNames.values) {
+  for (final arch in machOArchNames.values) {
     // Don't create an empty file for the current architecture.
     if (arch == dartNameForCurrentArchitecture) continue;
     final contents = emptyMachOForArchitecture(arch);
@@ -193,8 +207,7 @@
 
   Future<void> testUniversalBinary(
       String binaryPath, List<String> machoFiles) async {
-    await run(
-        _lipoBinary, <String>[...machoFiles, '-create', '-output', binaryPath]);
+    await run(lipo, <String>[...machoFiles, '-create', '-output', binaryPath]);
     await compareTraces(
         nonDwarfTrace, assemblyOutput1, assemblyOutput2, binaryPath,
         fromAssembly: true);
@@ -218,6 +231,21 @@
 Future<void> compareTraces(List<String> nonDwarfTrace, DwarfTestOutput output1,
     DwarfTestOutput output2, String dwarfPath,
     {bool fromAssembly = false}) async {
+  final header1 = StackTraceHeader.fromLines(output1.trace);
+  print('Header1 = $header1');
+  checkHeader(header1);
+  final header2 = StackTraceHeader.fromLines(output2.trace);
+  print('Header2 = $header1');
+  checkHeader(header2);
+
+  // 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);
+  if (dwarf == null) {
+    throw 'No DWARF information at $dwarfPath';
+  }
+
   // 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.
@@ -225,120 +253,15 @@
   final tracePCOffsets2 = collectPCOffsets(output2.trace);
   Expect.deepEquals(tracePCOffsets1, tracePCOffsets2);
 
-  if (tracePCOffsets1.isNotEmpty) {
-    final exampleOffset = tracePCOffsets1.first;
+  Expect.isNotEmpty(tracePCOffsets1);
+  checkRootUnitAssumptions(output1, output2, dwarf,
+      sampleOffset: tracePCOffsets1.first, matchingBuildIds: !fromAssembly);
 
-    // We run the test program on the same host OS as the test, so any of the
-    // PCOffsets above should have this information.
-    Expect.isNotNull(exampleOffset.os);
-    Expect.isNotNull(exampleOffset.architecture);
-    Expect.isNotNull(exampleOffset.usingSimulator);
-    Expect.isNotNull(exampleOffset.compressedPointers);
-
-    Expect.equals(exampleOffset.os, Platform.operatingSystem);
-    final archString = '${exampleOffset.usingSimulator! ? 'SIM' : ''}'
-        '${exampleOffset.architecture!.toUpperCase()}'
-        '${exampleOffset.compressedPointers! ? 'C' : ''}';
-    final baseBuildDir = path.basename(buildDir);
-    Expect.isTrue(baseBuildDir.endsWith(archString),
-        'Expected $baseBuildDir to end with $archString');
-  }
-
-  // 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) {
-    final dwarfBuildId = dwarf!.buildId();
-    Expect.isNotNull(dwarfBuildId);
-    print('Dwarf build ID: "${dwarfBuildId!}"');
-    // We should never generate an all-zero build ID.
-    Expect.notEquals(dwarfBuildId, "00000000000000000000000000000000");
-    // This is a common failure case as well, when HashBitsContainer ends up
-    // hashing over seemingly empty sections.
-    Expect.notEquals(dwarfBuildId, "01000000010000000100000001000000");
-    final buildId1 = buildId(output1.trace);
-    Expect.isFalse(buildId1.isEmpty);
-    print('Trace 1 build ID: "${buildId1}"');
-    Expect.equals(dwarfBuildId, buildId1);
-    final buildId2 = buildId(output2.trace);
-    Expect.isFalse(buildId2.isEmpty);
-    print('Trace 2 build ID: "${buildId2}"');
-    Expect.equals(dwarfBuildId, buildId2);
-  } else {
-    // Just check that the build IDs exist in the traces and are the same.
-    final buildId1 = buildId(output1.trace);
-    Expect.isFalse(buildId1.isEmpty, 'Could not find build ID in first trace');
-    print('Trace 1 build ID: "${buildId1}"');
-    final buildId2 = buildId(output2.trace);
-    Expect.isFalse(buildId2.isEmpty, 'Could not find build ID in second trace');
-    print('Trace 2 build ID: "${buildId2}"');
-    Expect.equals(buildId1, buildId2);
-  }
-
-  final decoder = DwarfStackTraceDecoder(dwarf!);
+  final decoder = DwarfStackTraceDecoder(dwarf);
   final translatedDwarfTrace1 =
       await Stream.fromIterable(output1.trace).transform(decoder).toList();
 
-  final allocateObjectPCOffset1 = PCOffset(
-      output1.allocateObjectInstructionsOffset, InstructionsSection.isolate);
-  final allocateObjectPCOffset2 = PCOffset(
-      output2.allocateObjectInstructionsOffset, InstructionsSection.isolate);
-
-  print('Offset of first stub address is $allocateObjectPCOffset1');
-  print('Offset of second stub address is $allocateObjectPCOffset2');
-
-  final allocateObjectCallInfo1 = dwarf.callInfoForPCOffset(
-      allocateObjectPCOffset1,
-      includeInternalFrames: true);
-  final allocateObjectCallInfo2 = dwarf.callInfoForPCOffset(
-      allocateObjectPCOffset2,
-      includeInternalFrames: true);
-
-  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("Successfully matched AllocateObject stub addresses");
-  print("");
-
-  final translatedStackFrames = onlySymbolicFrameLines(translatedDwarfTrace1);
-  final originalStackFrames = onlySymbolicFrameLines(nonDwarfTrace);
-
-  print('Stack frames from translated non-symbolic stack trace:');
-  translatedStackFrames.forEach(print);
-  print('');
-
-  print('Stack frames from original symbolic stack trace:');
-  originalStackFrames.forEach(print);
-  print('');
-
-  Expect.isTrue(translatedStackFrames.length > 0);
-  Expect.isTrue(originalStackFrames.length > 0);
-
-  // In symbolic mode, we don't store column information to avoid an increase
-  // in size of CodeStackMaps. Thus, we need to strip any columns from the
-  // translated non-symbolic stack to compare them via equality.
-  final columnStrippedTranslated = removeColumns(translatedStackFrames);
-
-  print('Stack frames from translated non-symbolic stack trace, no columns:');
-  columnStrippedTranslated.forEach(print);
-  print('');
-
-  Expect.deepEquals(columnStrippedTranslated, originalStackFrames);
+  checkTranslatedTrace(nonDwarfTrace, translatedDwarfTrace1);
 
   // 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.
@@ -377,6 +300,121 @@
   Expect.deepEquals(virtTrace2, relocatedFromDso2);
 }
 
+void checkHeader(StackTraceHeader header) {
+  // These should be all available.
+  Expect.isNotNull(header.vmStart);
+  Expect.isNotNull(header.isolateStart);
+  Expect.isNotNull(header.isolateDsoBase);
+  Expect.isNotNull(header.buildId);
+  Expect.isNotNull(header.os);
+  Expect.isNotNull(header.architecture);
+  Expect.isNotNull(header.usingSimulator);
+  Expect.isNotNull(header.compressedPointers);
+}
+
+Future<void> checkRootUnitAssumptions(
+    DwarfTestOutput output1, DwarfTestOutput output2, Dwarf rootDwarf,
+    {required PCOffset sampleOffset, bool matchingBuildIds = true}) async {
+  // We run the test program on the same host OS as the test, so any
+  // PCOffset from the trace should have this information.
+  Expect.isNotNull(sampleOffset.os);
+  Expect.isNotNull(sampleOffset.architecture);
+  Expect.isNotNull(sampleOffset.usingSimulator);
+  Expect.isNotNull(sampleOffset.compressedPointers);
+
+  Expect.equals(sampleOffset.os, Platform.operatingSystem);
+  final archString = '${sampleOffset.usingSimulator! ? 'SIM' : ''}'
+      '${sampleOffset.architecture!.toUpperCase()}'
+      '${sampleOffset.compressedPointers! ? 'C' : ''}';
+  final baseBuildDir = path.basename(buildDir);
+  Expect.isTrue(baseBuildDir.endsWith(archString),
+      'Expected $baseBuildDir to end with $archString');
+
+  // Check that the build IDs exist in the traces and are the same.
+  final buildId1 = buildId(output1.trace);
+  Expect.isFalse(buildId1.isEmpty, 'Could not find build ID in first trace');
+  print('Trace 1 build ID: "${buildId1}"');
+  final buildId2 = buildId(output2.trace);
+  Expect.isFalse(buildId2.isEmpty, 'Could not find build ID in second trace');
+  print('Trace 2 build ID: "${buildId2}"');
+  Expect.equals(buildId1, buildId2);
+
+  if (matchingBuildIds) {
+    // The build ID in the traces should be the same as the DWARF build ID
+    // when the ELF was generated by gen_snapshot.
+    final dwarfBuildId = rootDwarf.buildId();
+    Expect.isNotNull(dwarfBuildId);
+    print('Dwarf build ID: "${dwarfBuildId!}"');
+    // We should never generate an all-zero build ID.
+    Expect.notEquals(dwarfBuildId, "00000000000000000000000000000000");
+    // This is a common failure case as well, when HashBitsContainer ends up
+    // hashing over seemingly empty sections.
+    Expect.notEquals(dwarfBuildId, "01000000010000000100000001000000");
+    Expect.stringEquals(dwarfBuildId, buildId1);
+    Expect.stringEquals(dwarfBuildId, buildId2);
+  }
+
+  final allocateObjectPCOffset1 = PCOffset(
+      output1.allocateObjectInstructionsOffset, InstructionsSection.isolate);
+  print('Offset of first stub address is $allocateObjectPCOffset1');
+  final allocateObjectPCOffset2 = PCOffset(
+      output2.allocateObjectInstructionsOffset, InstructionsSection.isolate);
+  print('Offset of second stub address is $allocateObjectPCOffset2');
+
+  final allocateObjectCallInfo1 = rootDwarf.callInfoForPCOffset(
+      allocateObjectPCOffset1,
+      includeInternalFrames: true);
+  print('Call info for first stub address is $allocateObjectCallInfo1');
+  final allocateObjectCallInfo2 = rootDwarf.callInfoForPCOffset(
+      allocateObjectPCOffset2,
+      includeInternalFrames: true);
+  print('Call info for second stub address is $allocateObjectCallInfo2');
+
+  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("Successfully matched AllocateObject stub addresses");
+  print("");
+}
+
+void checkTranslatedTrace(List<String> nonDwarfTrace, List<String> dwarfTrace) {
+  final translatedStackFrames = onlySymbolicFrameLines(dwarfTrace);
+  final originalStackFrames = onlySymbolicFrameLines(nonDwarfTrace);
+
+  print('Stack frames from translated non-symbolic stack trace:');
+  translatedStackFrames.forEach(print);
+  print('');
+
+  print('Stack frames from original symbolic stack trace:');
+  originalStackFrames.forEach(print);
+  print('');
+
+  Expect.isTrue(translatedStackFrames.length > 0);
+  Expect.isTrue(originalStackFrames.length > 0);
+
+  // In symbolic mode, we don't store column information to avoid an increase
+  // in size of CodeStackMaps. Thus, we need to strip any columns from the
+  // translated non-symbolic stack to compare them via equality.
+  final columnStrippedTranslated = removeColumns(translatedStackFrames);
+
+  print('Stack frames from translated non-symbolic stack trace, no columns:');
+  columnStrippedTranslated.forEach(print);
+  print('');
+
+  Expect.deepEquals(columnStrippedTranslated, originalStackFrames);
+}
+
 Future<DwarfTestOutput> runTestProgram(
     String executable, List<String> args) async {
   final result = await runHelper(executable, args);
@@ -447,7 +485,7 @@
 
 // We only list architectures supported by the current CpuType enum in
 // pkg:native_stack_traces/src/macho.dart.
-const _machOArchNames = <String, String>{
+const machOArchNames = <String, String>{
   "ARM": "arm",
   "ARM64": "arm64",
   "IA32": "ia32",
@@ -455,7 +493,7 @@
 };
 
 String? get dartNameForCurrentArchitecture {
-  for (final entry in _machOArchNames.entries) {
+  for (final entry in machOArchNames.entries) {
     if (buildDir.endsWith(entry.key)) {
       return entry.value;
     }
diff --git a/runtime/tests/vm/dart/use_flag_test_helper.dart b/runtime/tests/vm/dart/use_flag_test_helper.dart
index d5064be..7789dc3 100644
--- a/runtime/tests/vm/dart/use_flag_test_helper.dart
+++ b/runtime/tests/vm/dart/use_flag_test_helper.dart
@@ -53,6 +53,15 @@
     buildDir, 'dart_precompiled_runtime' + (Platform.isWindows ? '.exe' : ''));
 final checkedInDartVM = path.join('tools', 'sdks', 'dart-sdk', 'bin',
     'dart' + (Platform.isWindows ? '.exe' : ''));
+// Lazily initialize 'lipo' so that tests that don't use it on platforms
+// that don't have it don't fail.
+late final lipo = () {
+  final path = "/usr/bin/lipo";
+  if (File(path).existsSync()) {
+    return path;
+  }
+  throw 'Could not find lipo binary at $path';
+}();
 
 final isSimulator = path.basename(buildDir).contains('SIM');
 
diff --git a/runtime/vm/app_snapshot.cc b/runtime/vm/app_snapshot.cc
index be5d10d..29b2ae7 100644
--- a/runtime/vm/app_snapshot.cc
+++ b/runtime/vm/app_snapshot.cc
@@ -4280,7 +4280,8 @@
       LoadingUnitPtr unit = objects_[i];
       AutoTraceObject(unit);
       WriteCompressedField(unit, parent);
-      s->Write<int32_t>(unit->untag()->id_);
+      s->Write<intptr_t>(
+          unit->untag()->packed_fields_.Read<UntaggedLoadingUnit::IdBits>());
     }
   }
 
@@ -4308,9 +4309,11 @@
                                      LoadingUnit::InstanceSize());
       unit->untag()->parent_ = static_cast<LoadingUnitPtr>(d.ReadRef());
       unit->untag()->base_objects_ = Array::null();
-      unit->untag()->id_ = d.Read<int32_t>();
-      unit->untag()->loaded_ = false;
-      unit->untag()->load_outstanding_ = false;
+      unit->untag()->instructions_image_ = nullptr;
+      unit->untag()->packed_fields_ =
+          UntaggedLoadingUnit::LoadStateBits::encode(
+              UntaggedLoadingUnit::kNotLoaded) |
+          UntaggedLoadingUnit::IdBits::encode(d.Read<intptr_t>());
     }
   }
 };
@@ -9961,6 +9964,20 @@
   ProgramDeserializationRoots roots(thread_->isolate_group()->object_store());
   deserializer.Deserialize(&roots);
 
+  if (Snapshot::IncludesCode(kind_)) {
+    const auto& units = Array::Handle(
+        thread_->isolate_group()->object_store()->loading_units());
+    if (!units.IsNull()) {
+      const auto& unit = LoadingUnit::Handle(
+          LoadingUnit::RawCast(units.At(LoadingUnit::kRootId)));
+      // Unlike other units, we don't explicitly load the root loading unit,
+      // so we mark it as loaded here, setting the instructions image as well.
+      unit.set_load_outstanding();
+      unit.set_instructions_image(instructions_image_);
+      unit.set_loaded(true);
+    }
+  }
+
   InitializeBSS();
 
   return ApiError::null();
@@ -10001,6 +10018,7 @@
     ASSERT(instructions_image_ != nullptr);
     thread_->isolate_group()->SetupImagePage(instructions_image_,
                                              /* is_executable */ true);
+    unit.set_instructions_image(instructions_image_);
   }
 
   UnitDeserializationRoots roots(unit);
diff --git a/runtime/vm/bss_relocs.cc b/runtime/vm/bss_relocs.cc
index db34c2c..b76eb86 100644
--- a/runtime/vm/bss_relocs.cc
+++ b/runtime/vm/bss_relocs.cc
@@ -28,16 +28,6 @@
 }
 
 void BSS::Initialize(Thread* current, uword* bss_start, bool vm) {
-  auto const instructions = reinterpret_cast<uword>(
-      current->isolate_group()->source()->snapshot_instructions);
-  uword dso_base;
-  // Needed for assembly snapshots. For ELF snapshots, we set up the relocated
-  // address information directly in the text segment InstructionsSection.
-  if (NativeSymbolResolver::LookupSharedObject(instructions, &dso_base)) {
-    InitializeBSSEntry(Relocation::InstructionsRelocatedAddress,
-                       instructions - dso_base, bss_start);
-  }
-
   // TODO(https://dartbug.com/52579): Remove.
   InitializeBSSEntry(Relocation::DRT_GetFfiCallbackMetadata,
                      reinterpret_cast<uword>(DLRT_GetFfiCallbackMetadata),
diff --git a/runtime/vm/bss_relocs.h b/runtime/vm/bss_relocs.h
index a4b7ffb..37e2173 100644
--- a/runtime/vm/bss_relocs.h
+++ b/runtime/vm/bss_relocs.h
@@ -16,7 +16,6 @@
   // portion of the BSS segment, so just the indices are shared, not the values
   // stored at the index.
   enum class Relocation : intptr_t {
-    InstructionsRelocatedAddress,
     DRT_GetFfiCallbackMetadata,  // TODO(https://dartbug.com/52579): Remove.
     DRT_ExitTemporaryIsolate,    // TODO(https://dartbug.com/52579): Remove.
     EndOfVmEntries,
diff --git a/runtime/vm/compiler/frontend/kernel_translation_helper.cc b/runtime/vm/compiler/frontend/kernel_translation_helper.cc
index e3ee0db..c841c20 100644
--- a/runtime/vm/compiler/frontend/kernel_translation_helper.cc
+++ b/runtime/vm/compiler/frontend/kernel_translation_helper.cc
@@ -2007,14 +2007,13 @@
 
   for (int i = 0; i < unit_count; i++) {
     intptr_t id = helper_->ReadUInt();
-    unit = LoadingUnit::New();
-    unit.set_id(id);
 
     intptr_t parent_id = helper_->ReadUInt();
     RELEASE_ASSERT(parent_id < id);
     parent ^= loading_units.At(parent_id);
     RELEASE_ASSERT(parent.IsNull() == (parent_id == 0));
-    unit.set_parent(parent);
+
+    unit = LoadingUnit::New(id, parent);
 
     intptr_t library_count = helper_->ReadUInt();
     uris = Array::New(library_count);
diff --git a/runtime/vm/compiler/runtime_offsets_extracted.h b/runtime/vm/compiler/runtime_offsets_extracted.h
index 6b16a64..6d46e40 100644
--- a/runtime/vm/compiler/runtime_offsets_extracted.h
+++ b/runtime/vm/compiler/runtime_offsets_extracted.h
@@ -1410,7 +1410,7 @@
 static constexpr dart::compiler::target::word String_InstanceSize = 0x10;
 static constexpr dart::compiler::target::word SubtypeTestCache_InstanceSize =
     0x18;
-static constexpr dart::compiler::target::word LoadingUnit_InstanceSize = 0x20;
+static constexpr dart::compiler::target::word LoadingUnit_InstanceSize = 0x28;
 static constexpr dart::compiler::target::word
     TransferableTypedData_InstanceSize = 0x8;
 static constexpr dart::compiler::target::word Type_InstanceSize = 0x30;
@@ -2829,7 +2829,7 @@
 static constexpr dart::compiler::target::word String_InstanceSize = 0x10;
 static constexpr dart::compiler::target::word SubtypeTestCache_InstanceSize =
     0x18;
-static constexpr dart::compiler::target::word LoadingUnit_InstanceSize = 0x20;
+static constexpr dart::compiler::target::word LoadingUnit_InstanceSize = 0x28;
 static constexpr dart::compiler::target::word
     TransferableTypedData_InstanceSize = 0x8;
 static constexpr dart::compiler::target::word Type_InstanceSize = 0x30;
@@ -3539,7 +3539,7 @@
 static constexpr dart::compiler::target::word String_InstanceSize = 0x10;
 static constexpr dart::compiler::target::word SubtypeTestCache_InstanceSize =
     0x18;
-static constexpr dart::compiler::target::word LoadingUnit_InstanceSize = 0x18;
+static constexpr dart::compiler::target::word LoadingUnit_InstanceSize = 0x20;
 static constexpr dart::compiler::target::word
     TransferableTypedData_InstanceSize = 0x8;
 static constexpr dart::compiler::target::word Type_InstanceSize = 0x28;
@@ -4251,7 +4251,7 @@
 static constexpr dart::compiler::target::word String_InstanceSize = 0x10;
 static constexpr dart::compiler::target::word SubtypeTestCache_InstanceSize =
     0x18;
-static constexpr dart::compiler::target::word LoadingUnit_InstanceSize = 0x18;
+static constexpr dart::compiler::target::word LoadingUnit_InstanceSize = 0x20;
 static constexpr dart::compiler::target::word
     TransferableTypedData_InstanceSize = 0x8;
 static constexpr dart::compiler::target::word Type_InstanceSize = 0x28;
@@ -5671,7 +5671,7 @@
 static constexpr dart::compiler::target::word String_InstanceSize = 0x10;
 static constexpr dart::compiler::target::word SubtypeTestCache_InstanceSize =
     0x18;
-static constexpr dart::compiler::target::word LoadingUnit_InstanceSize = 0x20;
+static constexpr dart::compiler::target::word LoadingUnit_InstanceSize = 0x28;
 static constexpr dart::compiler::target::word
     TransferableTypedData_InstanceSize = 0x8;
 static constexpr dart::compiler::target::word Type_InstanceSize = 0x30;
@@ -7073,7 +7073,7 @@
 static constexpr dart::compiler::target::word String_InstanceSize = 0x10;
 static constexpr dart::compiler::target::word SubtypeTestCache_InstanceSize =
     0x18;
-static constexpr dart::compiler::target::word LoadingUnit_InstanceSize = 0x20;
+static constexpr dart::compiler::target::word LoadingUnit_InstanceSize = 0x28;
 static constexpr dart::compiler::target::word
     TransferableTypedData_InstanceSize = 0x8;
 static constexpr dart::compiler::target::word Type_InstanceSize = 0x30;
@@ -8476,7 +8476,7 @@
 static constexpr dart::compiler::target::word String_InstanceSize = 0x10;
 static constexpr dart::compiler::target::word SubtypeTestCache_InstanceSize =
     0x18;
-static constexpr dart::compiler::target::word LoadingUnit_InstanceSize = 0x20;
+static constexpr dart::compiler::target::word LoadingUnit_InstanceSize = 0x28;
 static constexpr dart::compiler::target::word
     TransferableTypedData_InstanceSize = 0x8;
 static constexpr dart::compiler::target::word Type_InstanceSize = 0x30;
@@ -9178,7 +9178,7 @@
 static constexpr dart::compiler::target::word String_InstanceSize = 0x10;
 static constexpr dart::compiler::target::word SubtypeTestCache_InstanceSize =
     0x18;
-static constexpr dart::compiler::target::word LoadingUnit_InstanceSize = 0x18;
+static constexpr dart::compiler::target::word LoadingUnit_InstanceSize = 0x20;
 static constexpr dart::compiler::target::word
     TransferableTypedData_InstanceSize = 0x8;
 static constexpr dart::compiler::target::word Type_InstanceSize = 0x28;
@@ -9882,7 +9882,7 @@
 static constexpr dart::compiler::target::word String_InstanceSize = 0x10;
 static constexpr dart::compiler::target::word SubtypeTestCache_InstanceSize =
     0x18;
-static constexpr dart::compiler::target::word LoadingUnit_InstanceSize = 0x18;
+static constexpr dart::compiler::target::word LoadingUnit_InstanceSize = 0x20;
 static constexpr dart::compiler::target::word
     TransferableTypedData_InstanceSize = 0x8;
 static constexpr dart::compiler::target::word Type_InstanceSize = 0x28;
@@ -11286,7 +11286,7 @@
 static constexpr dart::compiler::target::word String_InstanceSize = 0x10;
 static constexpr dart::compiler::target::word SubtypeTestCache_InstanceSize =
     0x18;
-static constexpr dart::compiler::target::word LoadingUnit_InstanceSize = 0x20;
+static constexpr dart::compiler::target::word LoadingUnit_InstanceSize = 0x28;
 static constexpr dart::compiler::target::word
     TransferableTypedData_InstanceSize = 0x8;
 static constexpr dart::compiler::target::word Type_InstanceSize = 0x30;
@@ -12854,7 +12854,7 @@
 static constexpr dart::compiler::target::word
     AOT_SubtypeTestCache_InstanceSize = 0x18;
 static constexpr dart::compiler::target::word AOT_LoadingUnit_InstanceSize =
-    0x20;
+    0x28;
 static constexpr dart::compiler::target::word
     AOT_TransferableTypedData_InstanceSize = 0x8;
 static constexpr dart::compiler::target::word AOT_Type_InstanceSize = 0x30;
@@ -13650,7 +13650,7 @@
 static constexpr dart::compiler::target::word
     AOT_SubtypeTestCache_InstanceSize = 0x18;
 static constexpr dart::compiler::target::word AOT_LoadingUnit_InstanceSize =
-    0x20;
+    0x28;
 static constexpr dart::compiler::target::word
     AOT_TransferableTypedData_InstanceSize = 0x8;
 static constexpr dart::compiler::target::word AOT_Type_InstanceSize = 0x30;
@@ -14440,7 +14440,7 @@
 static constexpr dart::compiler::target::word
     AOT_SubtypeTestCache_InstanceSize = 0x18;
 static constexpr dart::compiler::target::word AOT_LoadingUnit_InstanceSize =
-    0x18;
+    0x20;
 static constexpr dart::compiler::target::word
     AOT_TransferableTypedData_InstanceSize = 0x8;
 static constexpr dart::compiler::target::word AOT_Type_InstanceSize = 0x28;
@@ -15232,7 +15232,7 @@
 static constexpr dart::compiler::target::word
     AOT_SubtypeTestCache_InstanceSize = 0x18;
 static constexpr dart::compiler::target::word AOT_LoadingUnit_InstanceSize =
-    0x18;
+    0x20;
 static constexpr dart::compiler::target::word
     AOT_TransferableTypedData_InstanceSize = 0x8;
 static constexpr dart::compiler::target::word AOT_Type_InstanceSize = 0x28;
@@ -16809,7 +16809,7 @@
 static constexpr dart::compiler::target::word
     AOT_SubtypeTestCache_InstanceSize = 0x18;
 static constexpr dart::compiler::target::word AOT_LoadingUnit_InstanceSize =
-    0x20;
+    0x28;
 static constexpr dart::compiler::target::word
     AOT_TransferableTypedData_InstanceSize = 0x8;
 static constexpr dart::compiler::target::word AOT_Type_InstanceSize = 0x30;
@@ -18366,7 +18366,7 @@
 static constexpr dart::compiler::target::word
     AOT_SubtypeTestCache_InstanceSize = 0x18;
 static constexpr dart::compiler::target::word AOT_LoadingUnit_InstanceSize =
-    0x20;
+    0x28;
 static constexpr dart::compiler::target::word
     AOT_TransferableTypedData_InstanceSize = 0x8;
 static constexpr dart::compiler::target::word AOT_Type_InstanceSize = 0x30;
@@ -19153,7 +19153,7 @@
 static constexpr dart::compiler::target::word
     AOT_SubtypeTestCache_InstanceSize = 0x18;
 static constexpr dart::compiler::target::word AOT_LoadingUnit_InstanceSize =
-    0x20;
+    0x28;
 static constexpr dart::compiler::target::word
     AOT_TransferableTypedData_InstanceSize = 0x8;
 static constexpr dart::compiler::target::word AOT_Type_InstanceSize = 0x30;
@@ -19934,7 +19934,7 @@
 static constexpr dart::compiler::target::word
     AOT_SubtypeTestCache_InstanceSize = 0x18;
 static constexpr dart::compiler::target::word AOT_LoadingUnit_InstanceSize =
-    0x18;
+    0x20;
 static constexpr dart::compiler::target::word
     AOT_TransferableTypedData_InstanceSize = 0x8;
 static constexpr dart::compiler::target::word AOT_Type_InstanceSize = 0x28;
@@ -20717,7 +20717,7 @@
 static constexpr dart::compiler::target::word
     AOT_SubtypeTestCache_InstanceSize = 0x18;
 static constexpr dart::compiler::target::word AOT_LoadingUnit_InstanceSize =
-    0x18;
+    0x20;
 static constexpr dart::compiler::target::word
     AOT_TransferableTypedData_InstanceSize = 0x8;
 static constexpr dart::compiler::target::word AOT_Type_InstanceSize = 0x28;
@@ -22276,7 +22276,7 @@
 static constexpr dart::compiler::target::word
     AOT_SubtypeTestCache_InstanceSize = 0x18;
 static constexpr dart::compiler::target::word AOT_LoadingUnit_InstanceSize =
-    0x20;
+    0x28;
 static constexpr dart::compiler::target::word
     AOT_TransferableTypedData_InstanceSize = 0x8;
 static constexpr dart::compiler::target::word AOT_Type_InstanceSize = 0x30;
diff --git a/runtime/vm/elf.cc b/runtime/vm/elf.cc
index c0f563a..d90bc2f 100644
--- a/runtime/vm/elf.cc
+++ b/runtime/vm/elf.cc
@@ -1827,34 +1827,34 @@
   ASSERT(section_table_->Find(kBuildIdNoteName) == nullptr);
   uint32_t hashes[kBuildIdSegmentNamesLength];
   // Currently, we construct the build ID out of data from two different
-  // sections: the .text section and the .rodata section. We only create
-  // a build ID when we have all four sections and when we have the actual
-  // bytes from those sections.
+  // sections: the .text section and the .rodata section.
   //
   // TODO(dartbug.com/43274): Generate build IDs for separate debugging
   // information for assembly snapshots.
-  //
-  // TODO(dartbug.com/43516): Generate build IDs for snapshots with deferred
-  // sections.
   auto* const text_section = section_table_->Find(kTextName);
   if (text_section == nullptr) return;
   ASSERT(text_section->IsTextSection());
   auto* const text_bits = text_section->AsBitsContainer();
   auto* const data_section = section_table_->Find(kDataName);
-  if (data_section == nullptr) return;
-  ASSERT(data_section->IsDataSection());
-  auto* const data_bits = data_section->AsBitsContainer();
-  // Now try to find
+  ASSERT(data_section == nullptr || data_section->IsDataSection());
+  // Hash each component by first hashing the associated text section and, if
+  // there's not one, hashing the associated data section (if any).
+  //
+  // Any component of the build ID which does not have an associated section
+  // in the result is kept as 0.
+  bool has_any_text = false;
   for (intptr_t i = 0; i < kBuildIdSegmentNamesLength; i++) {
     auto* const name = kBuildIdSegmentNames[i];
     hashes[i] = text_bits->Hash(name);
-    if (hashes[i] == 0) {
-      hashes[i] = data_bits->Hash(name);
+    if (hashes[i] != 0) {
+      has_any_text = true;
+    } else if (data_section != nullptr) {
+      hashes[i] = data_section->AsBitsContainer()->Hash(name);
     }
-    // The symbol wasn't found in either section or there were no bytes
-    // associated with the symbol.
-    if (hashes[i] == 0) return;
   }
+  // If none of the sections in the hash were text sections, then we don't need
+  // a build ID, as it is only used to symbolicize non-symbolic stack traces.
+  if (!has_any_text) return;
   auto const description_bytes = reinterpret_cast<uint8_t*>(hashes);
   const size_t description_length = sizeof(hashes);
   // Now that we have the description field contents, create the section.
diff --git a/runtime/vm/image_snapshot.cc b/runtime/vm/image_snapshot.cc
index 4e383ad..35e7c37 100644
--- a/runtime/vm/image_snapshot.cc
+++ b/runtime/vm/image_snapshot.cc
@@ -82,13 +82,7 @@
 uword Image::instructions_relocated_address() const {
 #if defined(DART_PRECOMPILED_RUNTIME)
   ASSERT(extra_info_ != nullptr);
-  // For assembly snapshots, we need to retrieve this from the initialized BSS.
-  const uword address =
-      compiled_to_elf() ? extra_info_->instructions_relocated_address_
-                        : bss()[BSS::RelocationIndex(
-                              BSS::Relocation::InstructionsRelocatedAddress)];
-  ASSERT(address != kNoRelocatedAddress);
-  return address;
+  return extra_info_->instructions_relocated_address_;
 #else
   return kNoRelocatedAddress;
 #endif
diff --git a/runtime/vm/object.cc b/runtime/vm/object.cc
index 5d99786..63bb5b9 100644
--- a/runtime/vm/object.cc
+++ b/runtime/vm/object.cc
@@ -19731,23 +19731,18 @@
   return buffer.buffer();
 }
 
-LoadingUnitPtr LoadingUnit::New() {
+LoadingUnitPtr LoadingUnit::New(intptr_t id, const LoadingUnit& parent) {
   ASSERT(Object::loadingunit_class() != Class::null());
   // LoadingUnit objects are long living objects, allocate them in the
   // old generation.
-  return Object::Allocate<LoadingUnit>(Heap::kOld);
+  auto result = Object::Allocate<LoadingUnit>(Heap::kOld);
+  NoSafepointScope scope;
+  ASSERT(Utils::IsInt(UntaggedLoadingUnit::IdBits::bitsize(), id));
+  result->untag()->packed_fields_.Update<UntaggedLoadingUnit::IdBits>(id);
+  result->untag()->set_parent(parent.ptr());
+  return result;
 }
 
-LoadingUnitPtr LoadingUnit::parent() const {
-  return untag()->parent();
-}
-void LoadingUnit::set_parent(const LoadingUnit& value) const {
-  untag()->set_parent(value.ptr());
-}
-
-ArrayPtr LoadingUnit::base_objects() const {
-  return untag()->base_objects();
-}
 void LoadingUnit::set_base_objects(const Array& value) const {
   untag()->set_base_objects(value.ptr());
 }
@@ -19757,18 +19752,13 @@
 }
 
 ObjectPtr LoadingUnit::IssueLoad() const {
-  ASSERT(!loaded());
-  ASSERT(!load_outstanding());
-  set_load_outstanding(true);
+  set_load_outstanding();
   return Isolate::Current()->CallDeferredLoadHandler(id());
 }
 
 ObjectPtr LoadingUnit::CompleteLoad(const String& error_message,
                                     bool transient_error) const {
-  ASSERT(!loaded());
-  ASSERT(load_outstanding());
   set_loaded(error_message.IsNull());
-  set_load_outstanding(false);
 
   const Library& lib = Library::Handle(Library::CoreLibrary());
   const String& sel = String::Handle(String::New("_completeLoads"));
@@ -26177,41 +26167,74 @@
 }
 
 #if defined(DART_PRECOMPILED_RUNTIME)
+static bool TryPrintNonSymbolicStackFrameBodyRelative(
+    BaseTextBuffer* buffer,
+    uword call_addr,
+    uword instructions,
+    bool vm,
+    LoadingUnit* unit = nullptr) {
+  const Image image(reinterpret_cast<const uint8_t*>(instructions));
+  if (!image.contains(call_addr)) return false;
+  if (unit != nullptr) {
+    ASSERT(!unit->IsNull());
+    // Add the unit ID to the stack frame, so the correct loading unit
+    // information from the header can be checked.
+    buffer->Printf(" unit %" Pd "", unit->id());
+  }
+  auto const offset = call_addr - instructions;
+  // Only print the relocated address of the call when we know the saved
+  // debugging information (if any) will have the same relocated address.
+  // Also only print 'virt' fields for isolate addresses.
+  if (!vm && image.compiled_to_elf()) {
+    const uword relocated_section_start =
+        image.instructions_relocated_address();
+    buffer->Printf(" virt %" Pp "", relocated_section_start + offset);
+  }
+  const char* symbol = vm ? kVmSnapshotInstructionsAsmSymbol
+                          : kIsolateSnapshotInstructionsAsmSymbol;
+  buffer->Printf(" %s+0x%" Px "\n", symbol, offset);
+  return true;
+}
+
 // Prints the best representation(s) for the call address.
 static void PrintNonSymbolicStackFrameBody(BaseTextBuffer* buffer,
                                            uword call_addr,
                                            uword isolate_instructions,
-                                           uword vm_instructions) {
-  const Image vm_image(reinterpret_cast<const void*>(vm_instructions));
-  const Image isolate_image(
-      reinterpret_cast<const void*>(isolate_instructions));
-
-  if (isolate_image.contains(call_addr)) {
-    auto const symbol_name = kIsolateSnapshotInstructionsAsmSymbol;
-    auto const offset = call_addr - isolate_instructions;
-    // Only print the relocated address of the call when we know the saved
-    // debugging information (if any) will have the same relocated address.
-    if (isolate_image.compiled_to_elf()) {
-      const uword relocated_section_start =
-          isolate_image.instructions_relocated_address();
-      buffer->Printf(" virt %" Pp "", relocated_section_start + offset);
-    }
-    buffer->Printf(" %s+0x%" Px "", symbol_name, offset);
-  } else if (vm_image.contains(call_addr)) {
-    auto const offset = call_addr - vm_instructions;
-    // We currently don't print 'virt' entries for vm addresses, even if
-    // they were compiled to ELF, as we should never encounter these in
-    // non-symbolic stack traces (since stub addresses are stripped).
-    //
-    // In case they leak due to code issues elsewhere, we still print them as
-    // <vm symbol>+<offset>, just to distinguish from other cases.
-    buffer->Printf(" %s+0x%" Px "", kVmSnapshotInstructionsAsmSymbol, offset);
-  } else {
-    // This case should never happen, since these are not addresses within the
-    // VM or app isolate instructions sections, so make it easy to notice.
-    buffer->Printf(" <invalid Dart instruction address>");
+                                           uword vm_instructions,
+                                           const Array& loading_units,
+                                           LoadingUnit* unit) {
+  if (TryPrintNonSymbolicStackFrameBodyRelative(buffer, call_addr,
+                                                vm_instructions,
+                                                /*vm=*/true)) {
+    return;
   }
-  buffer->Printf("\n");
+
+  if (!loading_units.IsNull()) {
+    // All non-VM stack frames should include the loading unit id.
+    const intptr_t unit_count = loading_units.Length();
+    for (intptr_t i = LoadingUnit::kRootId; i < unit_count; i++) {
+      *unit ^= loading_units.At(i);
+      if (!unit->has_instructions_image()) continue;
+      auto const instructions =
+          reinterpret_cast<uword>(unit->instructions_image());
+      if (TryPrintNonSymbolicStackFrameBodyRelative(buffer, call_addr,
+                                                    instructions,
+                                                    /*vm=*/false, unit)) {
+        return;
+      }
+    }
+  } else {
+    if (TryPrintNonSymbolicStackFrameBodyRelative(buffer, call_addr,
+                                                  isolate_instructions,
+                                                  /*vm=*/false)) {
+      return;
+    }
+  }
+
+  // The stack trace printer should never end up here, since these are not
+  // addresses within a loading unit or the VM or app isolate instructions
+  // sections. Thus, make it easy to notice when looking at the stack trace.
+  buffer->Printf(" <invalid Dart instruction address>\n");
 }
 #endif
 
@@ -26282,6 +26305,33 @@
   return false;
 }
 
+#if defined(DART_PRECOMPILED_RUNTIME)
+static void WriteImageBuildId(BaseTextBuffer* buffer,
+                              const char* prefix,
+                              uword image_address) {
+  const auto& build_id = OS::GetAppBuildId(image_address);
+  if (build_id.data != nullptr) {
+    ASSERT(build_id.len > 0);
+    buffer->AddString(prefix);
+    buffer->AddString("'");
+    for (intptr_t i = 0; i < build_id.len; i++) {
+      buffer->Printf("%2.2x", build_id.data[i]);
+    }
+    buffer->AddString("'");
+  }
+}
+
+void WriteStackTraceHeaderLoadingUnitEntry(BaseTextBuffer* buffer,
+                                           intptr_t id,
+                                           uword dso_base,
+                                           uword instructions) {
+  buffer->Printf("loading_unit: %" Pd "", id);
+  WriteImageBuildId(buffer, ", build_id: ", instructions);
+  buffer->Printf(", dso_base: %" Px ", instructions: %" Px "\n", dso_base,
+                 instructions);
+}
+#endif
+
 const char* StackTrace::ToCString() const {
   auto const T = Thread::Current();
   auto const zone = T->zone();
@@ -26291,6 +26341,13 @@
   auto& code_object = Object::Handle(zone);
   auto& code = Code::Handle(zone);
 
+#if defined(DART_PRECOMPILED_RUNTIME)
+  const Array& loading_units =
+      Array::Handle(T->isolate_group()->object_store()->loading_units());
+  auto* const unit =
+      loading_units.IsNull() ? nullptr : &LoadingUnit::Handle(zone);
+#endif
+
   NoSafepointScope no_allocation;
   GrowableArray<const Function*> inlined_functions;
   GrowableArray<TokenPosition> inlined_token_positions;
@@ -26307,17 +26364,18 @@
 #if defined(DART_PRECOMPILED_RUNTIME)
   auto const isolate_instructions = reinterpret_cast<uword>(
       T->isolate_group()->source()->snapshot_instructions);
+#if defined(DEBUG)
+  if (!loading_units.IsNull()) {
+    *unit ^= loading_units.At(LoadingUnit::kRootId);
+    ASSERT(!unit->IsNull());
+    ASSERT(unit->has_instructions_image());
+    ASSERT(reinterpret_cast<uword>(unit->instructions_image()) ==
+           isolate_instructions);
+  }
+#endif
   auto const vm_instructions = reinterpret_cast<uword>(
       Dart::vm_isolate_group()->source()->snapshot_instructions);
   if (FLAG_dwarf_stack_traces_mode) {
-    const Image isolate_instructions_image(
-        reinterpret_cast<const void*>(isolate_instructions));
-    const Image vm_instructions_image(
-        reinterpret_cast<const void*>(vm_instructions));
-    auto const isolate_relocated_address =
-        isolate_instructions_image.instructions_relocated_address();
-    auto const vm_relocated_address =
-        vm_instructions_image.instructions_relocated_address();
     // This prologue imitates Android's debuggerd to make it possible to paste
     // the stack trace into ndk-stack.
     buffer.Printf(
@@ -26338,22 +26396,26 @@
     buffer.Printf("os: %s arch: %s comp: %s sim: %s\n",
                   kHostOperatingSystemName, kTargetArchitectureName,
                   kCompressedPointers, kUsingSimulator);
-    const OS::BuildId& build_id =
-        OS::GetAppBuildId(T->isolate_group()->source()->snapshot_instructions);
-    if (build_id.data != nullptr) {
-      ASSERT(build_id.len > 0);
-      buffer.Printf("build_id: '");
-      for (intptr_t i = 0; i < build_id.len; i++) {
-        buffer.Printf("%2.2x", build_id.data[i]);
+    WriteImageBuildId(&buffer, "build_id: ", isolate_instructions);
+    buffer.AddString("\n");
+    if (!loading_units.IsNull()) {
+      const intptr_t unit_count = loading_units.Length();
+      for (intptr_t i = LoadingUnit::kRootId; i < unit_count; i++) {
+        *unit ^= loading_units.At(i);
+        if (!unit->has_instructions_image()) continue;
+        const uword instructions =
+            reinterpret_cast<uword>(unit->instructions_image());
+        const uword dso_base = OS::GetAppDSOBase(instructions);
+        WriteStackTraceHeaderLoadingUnitEntry(&buffer, i, dso_base,
+                                              instructions);
       }
-      buffer.Printf("'\n");
     }
     // Print the dso_base of the VM and isolate_instructions. We print both here
     // as the VM and isolate may be loaded from different snapshot images.
-    buffer.Printf("isolate_dso_base: %" Px "",
-                  isolate_instructions - isolate_relocated_address);
-    buffer.Printf(", vm_dso_base: %" Px "\n",
-                  vm_instructions - vm_relocated_address);
+    const uword isolate_dso_base = OS::GetAppDSOBase(isolate_instructions);
+    buffer.Printf("isolate_dso_base: %" Px "", isolate_dso_base);
+    const uword vm_dso_base = OS::GetAppDSOBase(vm_instructions);
+    buffer.Printf(", vm_dso_base: %" Px "\n", vm_dso_base);
     buffer.Printf("isolate_instructions: %" Px "", isolate_instructions);
     buffer.Printf(", vm_instructions: %" Px "\n", vm_instructions);
   }
@@ -26427,7 +26489,7 @@
         // prints call addresses instead of return addresses.
         buffer.Printf("    #%02" Pd " abs %" Pp "", frame_index, call_addr);
         PrintNonSymbolicStackFrameBody(&buffer, call_addr, isolate_instructions,
-                                       vm_instructions);
+                                       vm_instructions, loading_units, unit);
         frame_index++;
         continue;
       }
@@ -26439,7 +26501,7 @@
         // non-symbolic stack traces.
         PrintSymbolicStackFrameIndex(&buffer, frame_index);
         PrintNonSymbolicStackFrameBody(&buffer, call_addr, isolate_instructions,
-                                       vm_instructions);
+                                       vm_instructions, loading_units, unit);
         frame_index++;
         continue;
       }
diff --git a/runtime/vm/object.h b/runtime/vm/object.h
index 4da6829..e4ac971 100644
--- a/runtime/vm/object.h
+++ b/runtime/vm/object.h
@@ -7914,7 +7914,7 @@
   COMPILE_ASSERT(kIllegalId == WeakTable::kNoValue);
   static constexpr intptr_t kRootId = 1;
 
-  static LoadingUnitPtr New();
+  static LoadingUnitPtr New(intptr_t id, const LoadingUnit& parent);
 
   static intptr_t InstanceSize() {
     return RoundedAllocationSize(sizeof(UntaggedLoadingUnit));
@@ -7923,26 +7923,64 @@
   static intptr_t LoadingUnitOf(const Function& function);
   static intptr_t LoadingUnitOf(const Code& code);
 
-  LoadingUnitPtr parent() const;
-  void set_parent(const LoadingUnit& value) const;
+  LoadingUnitPtr parent() const { return untag()->parent(); }
 
-  ArrayPtr base_objects() const;
+  ArrayPtr base_objects() const { return untag()->base_objects(); }
   void set_base_objects(const Array& value) const;
 
-  intptr_t id() const { return untag()->id_; }
-  void set_id(intptr_t id) const { StoreNonPointer(&untag()->id_, id); }
+  intptr_t id() const {
+    return untag()->packed_fields_.Read<UntaggedLoadingUnit::IdBits>();
+  }
 
   // True once the VM deserializes this unit's snapshot.
-  bool loaded() const { return untag()->loaded_; }
+  bool loaded() const {
+    return untag()->packed_fields_.Read<UntaggedLoadingUnit::LoadStateBits>() ==
+           UntaggedLoadingUnit::kLoaded;
+  }
+  // value is whether the load succeeded or not.
   void set_loaded(bool value) const {
-    StoreNonPointer(&untag()->loaded_, value);
+    ASSERT(load_outstanding());
+    auto const expected =
+        value ? UntaggedLoadingUnit::kLoaded : UntaggedLoadingUnit::kNotLoaded;
+    auto const got = untag()
+                         ->packed_fields_
+                         .UpdateConditional<UntaggedLoadingUnit::LoadStateBits>(
+                             expected, UntaggedLoadingUnit::kLoadOutstanding);
+    // Check that we're in the expected state afterwards.
+    ASSERT_EQUAL(got, expected);
   }
 
   // True once the VM invokes the embedder's deferred load callback until the
   // embedder calls Dart_DeferredLoadComplete[Error].
-  bool load_outstanding() const { return untag()->load_outstanding_; }
-  void set_load_outstanding(bool value) const {
-    StoreNonPointer(&untag()->load_outstanding_, value);
+  bool load_outstanding() const {
+    return untag()->packed_fields_.Read<UntaggedLoadingUnit::LoadStateBits>() ==
+           UntaggedLoadingUnit::kLoadOutstanding;
+  }
+  void set_load_outstanding() const {
+    auto const previous = UntaggedLoadingUnit::kNotLoaded;
+    ASSERT_EQUAL(
+        untag()->packed_fields_.Read<UntaggedLoadingUnit::LoadStateBits>(),
+        previous);
+    auto const expected = UntaggedLoadingUnit::kLoadOutstanding;
+    auto const got = untag()
+                         ->packed_fields_
+                         .UpdateConditional<UntaggedLoadingUnit::LoadStateBits>(
+                             expected, previous);
+    // Check that we're in the expected state afterwards.
+    ASSERT_EQUAL(got, expected);
+  }
+
+  const uint8_t* instructions_image() const {
+    // The instructions image should only be accessed if the load succeeded.
+    ASSERT(loaded());
+    return untag()->instructions_image_;
+  }
+  void set_instructions_image(const uint8_t* value) const {
+    ASSERT(load_outstanding());
+    StoreNonPointer(&untag()->instructions_image_, value);
+  }
+  bool has_instructions_image() const {
+    return loaded() && instructions_image() != nullptr;
   }
 
   ObjectPtr IssueLoad() const;
diff --git a/runtime/vm/os.cc b/runtime/vm/os.cc
new file mode 100644
index 0000000..1eca71b
--- /dev/null
+++ b/runtime/vm/os.cc
@@ -0,0 +1,30 @@
+// Copyright (c) 2024, 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.
+
+#include "vm/os.h"
+
+#include "platform/assert.h"
+#include "vm/image_snapshot.h"
+#include "vm/native_symbol.h"
+
+namespace dart {
+
+const uint8_t* OS::GetAppDSOBase(const uint8_t* snapshot_instructions) {
+  // Use the relocated address in the Image if the snapshot was compiled
+  // directly to ELF.
+  const Image instructions_image(snapshot_instructions);
+  if (instructions_image.compiled_to_elf()) {
+    return snapshot_instructions -
+           instructions_image.instructions_relocated_address();
+  }
+  uword dso_base;
+  if (NativeSymbolResolver::LookupSharedObject(
+          reinterpret_cast<uword>(snapshot_instructions), &dso_base)) {
+    return reinterpret_cast<const uint8_t*>(dso_base);
+  }
+  UNIMPLEMENTED();
+  return nullptr;
+}
+
+}  // namespace dart
diff --git a/runtime/vm/os.h b/runtime/vm/os.h
index 3372313..378f599 100644
--- a/runtime/vm/os.h
+++ b/runtime/vm/os.h
@@ -120,15 +120,26 @@
 
   DART_NORETURN static void Exit(int code);
 
+  // Retrieves the DSO base for the given instructions image.
+  static const uint8_t* GetAppDSOBase(const uint8_t* snapshot_instructions);
+  static uword GetAppDSOBase(uword snapshot_instructions) {
+    return reinterpret_cast<uword>(
+        GetAppDSOBase(reinterpret_cast<const uint8_t*>(snapshot_instructions)));
+  }
+
   struct BuildId {
     intptr_t len;
     const uint8_t* data;
   };
 
-  // Retrieves the build ID information for the current application isolate.
+  // Retrieves the build ID information for the given instructions image.
   // If found, returns a BuildId with the length of the build ID and a pointer
   // to its contents, otherwise returns a BuildId with contents {0, nullptr}.
   static BuildId GetAppBuildId(const uint8_t* snapshot_instructions);
+  static BuildId GetAppBuildId(uword snapshot_instructions) {
+    return GetAppBuildId(
+        reinterpret_cast<const uint8_t*>(snapshot_instructions));
+  }
 };
 
 }  // namespace dart
diff --git a/runtime/vm/os_android.cc b/runtime/vm/os_android.cc
index e7b502e..9a877f9 100644
--- a/runtime/vm/os_android.cc
+++ b/runtime/vm/os_android.cc
@@ -359,12 +359,7 @@
   if (auto* const image_build_id = instructions_image.build_id()) {
     return {instructions_image.build_id_length(), image_build_id};
   }
-  Dl_info snapshot_info;
-  if (dladdr(snapshot_instructions, &snapshot_info) == 0) {
-    return {0, nullptr};
-  }
-  const uint8_t* dso_base =
-      static_cast<const uint8_t*>(snapshot_info.dli_fbase);
+  const uint8_t* dso_base = GetAppDSOBase(snapshot_instructions);
   const ElfW(Ehdr)& elf_header = *reinterpret_cast<const ElfW(Ehdr)*>(dso_base);
   const ElfW(Phdr)* const phdr_array =
       reinterpret_cast<const ElfW(Phdr)*>(dso_base + elf_header.e_phoff);
diff --git a/runtime/vm/os_fuchsia.cc b/runtime/vm/os_fuchsia.cc
index 16569f7..27ce812 100644
--- a/runtime/vm/os_fuchsia.cc
+++ b/runtime/vm/os_fuchsia.cc
@@ -651,12 +651,7 @@
   if (auto* const image_build_id = instructions_image.build_id()) {
     return {instructions_image.build_id_length(), image_build_id};
   }
-  Dl_info snapshot_info;
-  if (dladdr(snapshot_instructions, &snapshot_info) == 0) {
-    return {0, nullptr};
-  }
-  const uint8_t* dso_base =
-      static_cast<const uint8_t*>(snapshot_info.dli_fbase);
+  const uint8_t* dso_base = GetAppDSOBase(snapshot_instructions);
   const ElfW(Ehdr)& elf_header = *reinterpret_cast<const ElfW(Ehdr)*>(dso_base);
   const ElfW(Phdr)* const phdr_array =
       reinterpret_cast<const ElfW(Phdr)*>(dso_base + elf_header.e_phoff);
diff --git a/runtime/vm/os_linux.cc b/runtime/vm/os_linux.cc
index 8d98395..f16cb55 100644
--- a/runtime/vm/os_linux.cc
+++ b/runtime/vm/os_linux.cc
@@ -665,12 +665,7 @@
   if (auto* const image_build_id = instructions_image.build_id()) {
     return {instructions_image.build_id_length(), image_build_id};
   }
-  Dl_info snapshot_info;
-  if (dladdr(snapshot_instructions, &snapshot_info) == 0) {
-    return {0, nullptr};
-  }
-  const uint8_t* dso_base =
-      static_cast<const uint8_t*>(snapshot_info.dli_fbase);
+  const uint8_t* dso_base = GetAppDSOBase(snapshot_instructions);
   const ElfW(Ehdr)& elf_header = *reinterpret_cast<const ElfW(Ehdr)*>(dso_base);
   const ElfW(Phdr)* const phdr_array =
       reinterpret_cast<const ElfW(Phdr)*>(dso_base + elf_header.e_phoff);
diff --git a/runtime/vm/os_macos.cc b/runtime/vm/os_macos.cc
index 24f2120..33eb5bd 100644
--- a/runtime/vm/os_macos.cc
+++ b/runtime/vm/os_macos.cc
@@ -289,12 +289,7 @@
   if (auto* const image_build_id = instructions_image.build_id()) {
     return {instructions_image.build_id_length(), image_build_id};
   }
-  Dl_info snapshot_info;
-  if (dladdr(snapshot_instructions, &snapshot_info) == 0) {
-    return {0, nullptr};
-  }
-  const uint8_t* dso_base =
-      static_cast<const uint8_t*>(snapshot_info.dli_fbase);
+  const uint8_t* dso_base = GetAppDSOBase(snapshot_instructions);
   const auto& macho_header =
       *reinterpret_cast<const struct mach_header*>(dso_base);
   // We assume host endianness in the Mach-O file.
diff --git a/runtime/vm/raw_object.h b/runtime/vm/raw_object.h
index 01bed8d..862fc79 100644
--- a/runtime/vm/raw_object.h
+++ b/runtime/vm/raw_object.h
@@ -2599,9 +2599,18 @@
   VISIT_FROM(parent)
   COMPRESSED_POINTER_FIELD(ArrayPtr, base_objects)
   VISIT_TO(base_objects)
-  int32_t id_;
-  bool load_outstanding_;
-  bool loaded_;
+  const uint8_t* instructions_image_;
+  AtomicBitFieldContainer<intptr_t> packed_fields_;
+
+  enum LoadState : int8_t {
+    kNotLoaded = 0,  // Ensure this is the default state when zero-initialized.
+    kLoadOutstanding,
+    kLoaded,
+  };
+
+  using LoadStateBits = BitField<decltype(packed_fields_), LoadState, 0, 2>;
+  using IdBits =
+      BitField<decltype(packed_fields_), intptr_t, LoadStateBits::kNextBit>;
 };
 
 class UntaggedError : public UntaggedObject {
diff --git a/runtime/vm/vm_sources.gni b/runtime/vm/vm_sources.gni
index b893dcb..5a3185a 100644
--- a/runtime/vm/vm_sources.gni
+++ b/runtime/vm/vm_sources.gni
@@ -194,6 +194,7 @@
   "object_set.h",
   "object_store.cc",
   "object_store.h",
+  "os.cc",
   "os.h",
   "os_android.cc",
   "os_fuchsia.cc",