[pkg/native_stack_traces] Add support for MacOS universal binaries.

In addition to adding a parser for the universal binary format, this
also requires major reworks to handle files that contain different
DWARF information for different architectures, and how to pass the
architecture down to where it's needed.

Also fix dSYM handling: instead of assuming the name of the MachO file
corresponds exactly to the basename of the dSYM with the extension
stripped, just look for the single file within the
Contents/Resources/DWARF directory.

Also add `unrecognized` enum entries for DW_TAG, DW_AT, and DW_FORM
values that aren't handled.

Issue: https://github.com/flutter/flutter/pull/101586
Change-Id: Ief5edc275ccd1192669252140d128136cd2bed26
Cq-Include-Trybots: luci.dart.try:vm-kernel-nnbd-mac-release-arm64-try,vm-kernel-precomp-mac-product-x64-try,vm-kernel-precomp-nnbd-mac-release-arm64-try,vm-kernel-nnbd-mac-release-x64-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/252821
Commit-Queue: Tess Strickland <sstrickl@google.com>
Reviewed-by: Ryan Macnak <rmacnak@google.com>
This commit is contained in:
Tess Strickland 2022-08-25 13:27:24 +00:00 committed by Commit Bot
parent c02c9753c3
commit 0a4cb4d43e
13 changed files with 1535 additions and 417 deletions

View file

@ -1,3 +1,10 @@
## 0.5.2
- Adjusted logic for finding the DWARF MachO file in a dSYM.
- Added support for retrieving DWARF information from universal
macOS binaries using the new architecture information.
- Added logic to handle unknown DW_AT, DW_FORM, and DW_TAG codes.
## 0.5.1
- Parse new OS and architecture information from non-symbolic stack

View file

@ -8,6 +8,7 @@ import 'dart:io' as io;
import 'package:args/args.dart' show ArgParser, ArgResults;
import 'package:native_stack_traces/native_stack_traces.dart';
import 'package:native_stack_traces/src/macho.dart' show CpuType;
import 'package:path/path.dart' as path;
ArgParser _createBaseDebugParser(ArgParser parser) => parser
@ -39,6 +40,11 @@ final ArgParser _findParser =
abbr: 'x',
negatable: false,
help: 'Always parse integers as hexadecimal')
..addOption('architecture',
abbr: 'a',
help: 'Architecture on which the program is run',
allowed: CpuType.values.map((v) => v.dartName),
valueHelp: 'ARCH')
..addOption('vm_start',
help: 'Absolute address for start of VM instructions',
valueHelp: 'PC')
@ -217,7 +223,7 @@ void find(ArgResults options) {
return usageError('need both VM start and isolate start');
}
var vmStart = dwarf.vmStartAddress;
var vmStart = dwarf.vmStartAddress();
if (options['vm_start'] != null) {
final address = tryParseIntAddress(options['vm_start']);
if (address == null) {
@ -226,8 +232,12 @@ void find(ArgResults options) {
}
vmStart = address;
}
if (vmStart == null) {
return usageError('no VM start address found, one must be specified '
'with --vm_start');
}
var isolateStart = dwarf.isolateStartAddress;
var isolateStart = dwarf.isolateStartAddress();
if (options['isolate_start'] != null) {
final address = tryParseIntAddress(options['isolate_start']);
if (address == null) {
@ -236,8 +246,15 @@ void find(ArgResults options) {
}
isolateStart = address;
}
if (isolateStart == null) {
return usageError('no isolate start address found, one must be specified '
'with --isolate_start');
}
final header = StackTraceHeader.fromStarts(isolateStart, vmStart);
final arch = options['architecture'];
final header =
StackTraceHeader.fromStarts(isolateStart, vmStart, architecture: arch);
final locations = <PCOffset>[];
for (final String s in [
@ -253,7 +270,7 @@ void find(ArgResults options) {
for (final offset in locations) {
final addr = dwarf.virtualAddressOf(offset);
final frames = dwarf
.callInfoFor(addr, includeInternalFrames: verbose)
.callInfoForPCOffset(offset, includeInternalFrames: verbose)
?.map((CallInfo c) => ' $c');
final addrString =
addr > 0 ? '0x${addr.toRadixString(16)}' : addr.toString();
@ -307,7 +324,7 @@ Future<void> dump(ArgResults options) async {
}
final dwarf = _loadFromFile(options.rest.first, usageError);
if (dwarf == null) {
return usageError("'${options.rest.first}' contains no DWARF information");
return;
}
final output = options['output'] != null

View file

@ -45,10 +45,12 @@ class StackTraceHeader {
bool? get compressedPointers => _compressed;
bool? get usingSimulator => _simulated;
static StackTraceHeader fromStarts(int isolateStart, int vmStart) =>
static StackTraceHeader fromStarts(int isolateStart, int vmStart,
{String? architecture}) =>
StackTraceHeader()
.._isolateStart = isolateStart
.._vmStart = vmStart;
.._vmStart = vmStart
.._arch = architecture;
/// Try and parse the given line as one of the recognized lines in the
/// header of a non-symbolic stack trace.

File diff suppressed because it is too large Load diff

View file

@ -14,13 +14,18 @@ abstract class DwarfContainerSymbol {
}
abstract class DwarfContainer {
/// Returns the architecture of the container as reported by Dart (e.g.,
/// 'x64' or 'arm'). Returns null if the architecture of the container does
/// not match any expected Dart architecture.
String? get architecture;
Reader debugInfoReader(Reader containerReader);
Reader lineNumberInfoReader(Reader containerReader);
Reader abbreviationsTableReader(Reader containerReader);
DwarfContainerSymbol? staticSymbolAt(int address);
int get vmStartAddress;
int get isolateStartAddress;
int? get vmStartAddress;
int? get isolateStartAddress;
String? get buildId;
@ -28,4 +33,11 @@ abstract class DwarfContainer {
DwarfContainerStringTable? get debugLineStringTable;
void writeToStringBuffer(StringBuffer buffer);
@override
String toString() {
final buffer = StringBuffer();
writeToStringBuffer(buffer);
return buffer.toString();
}
}

View file

@ -61,11 +61,89 @@ int _readElfNative(Reader reader) {
}
}
/// The identification block at the start of an ELF header, which includes
/// the magic bytes for file type identification, word size and endian
/// information, etc.
class ElfIdentification {
final int wordSize;
final Endian endian;
ElfIdentification._(this.wordSize, this.endian);
static ElfIdentification? fromReader(Reader reader) {
final start = reader.offset;
final bytes = Uint8List.sublistView(reader.readRawBytes(_EI_NIDENT));
// Reset reader in case of failures/null returns below.
reader.seek(start, absolute: true);
// Check magic bytes at start. Return null for a mismatch here.
if (bytes[_EI_MAG0] != _ELFMAG0) return null;
if (bytes[_EI_MAG1] != _ELFMAG1) return null;
if (bytes[_EI_MAG2] != _ELFMAG2) return null;
if (bytes[_EI_MAG3] != _ELFMAG3) return null;
// Check this first since it only has one good value currently.
if (bytes[_EI_VERSION] != _EV_CURRENT) {
throw FormatException('Unexpected e_ident[EI_VERSION] value');
}
int? wordSize;
switch (bytes[_EI_CLASS]) {
case _ELFCLASS32:
wordSize = 4;
break;
case _ELFCLASS64:
wordSize = 8;
break;
default:
throw FormatException('Unexpected e_ident[EI_CLASS] value');
}
Endian? endian;
switch (bytes[_EI_DATA]) {
case _ELFDATA2LSB:
endian = Endian.little;
break;
case _ELFDATA2MSB:
endian = Endian.big;
break;
default:
throw FormatException('Unexpected e_ident[EI_DATA] value');
}
// Successfully read, so position the reader after the identification block.
reader.seek(start + _EI_NIDENT, absolute: true);
return ElfIdentification._(wordSize, endian);
}
// Offsets into the identification block.
static const _EI_MAG0 = 0;
static const _EI_MAG1 = 1;
static const _EI_MAG2 = 2;
static const _EI_MAG3 = 3;
static const _EI_CLASS = 4;
static const _EI_DATA = 5;
static const _EI_VERSION = 6;
static const _EI_NIDENT = 16;
// Constants used within the ELF specification.
static const _ELFMAG0 = 0x7f;
static const _ELFMAG1 = 0x45; // E
static const _ELFMAG2 = 0x4c; // L
static const _ELFMAG3 = 0x46; // F
static const _ELFCLASS32 = 1;
static const _ELFCLASS64 = 2;
static const _ELFDATA2LSB = 1;
static const _ELFDATA2MSB = 2;
static const _EV_CURRENT = 1;
}
/// The header of the ELF file, which includes information necessary to parse
/// the rest of the file.
class ElfHeader {
final int wordSize;
final Endian endian;
final ElfIdentification elfIdent;
final int type;
final int machine;
final int entry;
final int flags;
final int headerSize;
@ -78,8 +156,9 @@ class ElfHeader {
final int sectionHeaderStringsIndex;
ElfHeader._(
this.wordSize,
this.endian,
this.elfIdent,
this.type,
this.machine,
this.entry,
this.flags,
this.headerSize,
@ -93,27 +172,18 @@ class ElfHeader {
static ElfHeader? fromReader(Reader reader) {
final start = reader.offset;
final fileSize = reader.length;
final fileSize = reader.remaining;
for (final sigByte in _ELFMAG.codeUnits) {
if (reader.readByte() != sigByte) {
reader.seek(start, absolute: true);
return null;
}
}
final elfIdent = ElfIdentification.fromReader(reader);
if (elfIdent == null) return null;
int wordSize;
switch (reader.readByte()) {
case _ELFCLASS32:
wordSize = 4;
break;
case _ELFCLASS64:
wordSize = 8;
break;
default:
throw FormatException('Unexpected e_ident[EI_CLASS] value');
}
final calculatedHeaderSize = 0x18 + 3 * wordSize + 0x10;
// Make sure the word size and endianness of the reader are set according
// to the values parsed from the ELF identification block.
assert(reader.offset == start + ElfIdentification._EI_NIDENT);
reader.wordSize = elfIdent.wordSize;
reader.endian = elfIdent.endian;
final calculatedHeaderSize = 0x18 + 3 * elfIdent.wordSize + 0x10;
if (fileSize < calculatedHeaderSize) {
throw FormatException('ELF file too small for header: '
@ -121,30 +191,11 @@ class ElfHeader {
'calculated header size $calculatedHeaderSize');
}
Endian endian;
switch (reader.readByte()) {
case _ELFDATA2LSB:
endian = Endian.little;
break;
case _ELFDATA2MSB:
endian = Endian.big;
break;
default:
throw FormatException('Unexpected e_indent[EI_DATA] value');
}
final type = _readElfHalf(reader);
final machine = _readElfHalf(reader);
if (reader.readByte() != 0x01) {
throw FormatException('Unexpected e_ident[EI_VERSION] value');
}
// After this point, we need the reader to be correctly set up re: word
// size and endianness, since we start reading more than single bytes.
reader.endian = endian;
reader.wordSize = wordSize;
// Skip rest of e_ident/e_type/e_machine, i.e. move to e_version.
reader.seek(0x14, absolute: true);
if (_readElfWord(reader) != 0x01) {
// This word should also be set to EV_CURRENT.
if (_readElfWord(reader) != ElfIdentification._EV_CURRENT) {
throw FormatException('Unexpected e_version value');
}
@ -187,8 +238,9 @@ class ElfHeader {
}
return ElfHeader._(
wordSize,
endian,
elfIdent,
type,
machine,
entry,
flags,
headerSize,
@ -201,16 +253,39 @@ class ElfHeader {
sectionHeaderStringsIndex);
}
// The architectures currently output by the Dart built-in ELF writer.
static const _EM_386 = 3;
static const _EM_ARM = 40;
static const _EM_X86_64 = 62;
static const _EM_AARCH64 = 183;
static const _EM_RISCV = 243;
String? get architecture {
switch (machine) {
case _EM_ARM:
assert(wordSize == 4);
return "arm";
case _EM_AARCH64:
assert(wordSize == 8);
return "arm64";
case _EM_386:
assert(wordSize == 4);
return "ia32";
case _EM_X86_64:
assert(wordSize == 8);
return "x64";
case _EM_RISCV:
return wordSize == 8 ? "riscv64" : "riscv32";
default:
return null;
}
}
int get wordSize => elfIdent.wordSize;
Endian get endian => elfIdent.endian;
int get programHeaderSize => programHeaderCount * programHeaderEntrySize;
int get sectionHeaderSize => sectionHeaderCount * sectionHeaderEntrySize;
// Constants used within the ELF specification.
static const _ELFMAG = '\x7fELF';
static const _ELFCLASS32 = 0x01;
static const _ELFCLASS64 = 0x02;
static const _ELFDATA2LSB = 0x01;
static const _ELFDATA2MSB = 0x02;
void writeToStringBuffer(StringBuffer buffer) {
buffer
..write('Format is ')
@ -225,6 +300,10 @@ class ElfHeader {
break;
}
buffer
..write('Type: 0x')
..writeln(paddedHex(type, 2))
..write('Machine: 0x')
..writeln(paddedHex(type, 2))
..write('Entry point: 0x')
..writeln(paddedHex(entry, wordSize))
..write('Flags: 0x')
@ -362,8 +441,8 @@ class ProgramHeader {
}
static ProgramHeader fromReader(Reader reader, ElfHeader header) {
final programReader = reader.refocusedCopy(
header.programHeaderOffset, header.programHeaderSize);
final programReader =
reader.shrink(header.programHeaderOffset, header.programHeaderSize);
final entries =
programReader.readRepeated(ProgramHeaderEntry.fromReader).toList();
return ProgramHeader._(entries);
@ -520,8 +599,8 @@ class SectionHeader {
SectionHeader._(this.entries);
static SectionHeader fromReader(Reader reader, ElfHeader header) {
final headerReader = reader.refocusedCopy(
header.sectionHeaderOffset, header.sectionHeaderSize);
final headerReader =
reader.shrink(header.sectionHeaderOffset, header.sectionHeaderSize);
final entries =
headerReader.readRepeated(SectionHeaderEntry.fromReader).toList();
final nameTableEntry = entries[header.sectionHeaderStringsIndex];
@ -588,7 +667,9 @@ class Section {
int get length => headerEntry.size;
// Convenience function for preparing a reader to read a particular section.
Reader refocusedCopy(Reader reader) => reader.refocusedCopy(offset, length);
// Requires a reader for the entire ELF data where the reader's start is
// the start of the ELF data.
Reader shrink(Reader reader) => reader.shrink(offset, length);
void writeToStringBuffer(StringBuffer buffer) {
buffer
@ -616,7 +697,7 @@ class Note extends Section {
Note._(entry, this.type, this.name, this.description) : super._(entry);
static Note fromReader(Reader originalReader, SectionHeaderEntry entry) {
final reader = originalReader.refocusedCopy(entry.offset, entry.size);
final reader = originalReader.shrink(entry.offset, entry.size);
final nameLength = reader.readBytes(4);
final descriptionLength = reader.readBytes(4);
final type = reader.readBytes(4);
@ -657,7 +738,7 @@ class StringTable extends Section implements DwarfContainerStringTable {
StringTable._(entry, this._entries) : super._(entry);
static StringTable fromReader(Reader reader, SectionHeaderEntry entry) {
final sectionReader = reader.refocusedCopy(entry.offset, entry.size);
final sectionReader = reader.shrink(entry.offset, entry.size);
final entries = Map.fromEntries(sectionReader
.readRepeatedWithOffsets((r) => r.readNullTerminatedString()));
return StringTable._(entry, entries);
@ -813,7 +894,7 @@ class SymbolTable extends Section {
super._(entry);
static SymbolTable fromReader(Reader reader, SectionHeaderEntry entry) {
final sectionReader = reader.refocusedCopy(entry.offset, entry.size);
final sectionReader = reader.shrink(entry.offset, entry.size);
final entries = sectionReader.readRepeated(Symbol.fromReader).toList();
return SymbolTable._(entry, entries);
}
@ -879,7 +960,7 @@ class DynamicTable extends Section {
: super._(entry);
static DynamicTable fromReader(Reader reader, SectionHeaderEntry entry) {
final sectionReader = reader.refocusedCopy(entry.offset, entry.size);
final sectionReader = reader.shrink(entry.offset, entry.size);
final entries = <int, int>{};
while (true) {
// Each entry is a tag and a value, both native word sized.
@ -954,7 +1035,7 @@ class DynamicTable extends Section {
}
/// Information parsed from an Executable and Linking Format (ELF) file.
class Elf implements DwarfContainer {
class Elf extends DwarfContainer {
final ElfHeader _header;
final ProgramHeader _programHeader;
final SectionHeader _sectionHeader;
@ -1040,8 +1121,8 @@ class Elf implements DwarfContainer {
// make sure we have a reader that a) makes no assumptions about the
// endianness or word size, since we'll read those in the header and b)
// has an internal offset of 0 so absolute offsets can be used directly.
final reader = Reader.fromTypedData(ByteData.sublistView(
elfReader.bdata, elfReader.bdata.offsetInBytes + elfReader.offset));
final reader = Reader.fromTypedData(
ByteData.sublistView(elfReader.bdata, elfReader.offset));
final header = ElfHeader.fromReader(reader);
// Only happens if the file didn't start with the expected magic number.
if (header == null) return null;
@ -1124,37 +1205,27 @@ class Elf implements DwarfContainer {
debugStringTable, debugLineStringTable);
}
@override
String? get architecture => _header.architecture;
@override
Reader abbreviationsTableReader(Reader containerReader) =>
namedSections('.debug_abbrev').single.refocusedCopy(containerReader);
namedSections('.debug_abbrev').single.shrink(containerReader);
@override
Reader lineNumberInfoReader(Reader containerReader) =>
namedSections('.debug_line').single.refocusedCopy(containerReader);
namedSections('.debug_line').single.shrink(containerReader);
@override
Reader debugInfoReader(Reader containerReader) =>
namedSections('.debug_info').single.refocusedCopy(containerReader);
namedSections('.debug_info').single.shrink(containerReader);
@override
int get vmStartAddress {
final vmStartSymbol = dynamicSymbolFor(constants.vmSymbolName);
if (vmStartSymbol == null) {
throw FormatException(
'Expected a dynamic symbol with name ${constants.vmSymbolName}');
}
return vmStartSymbol.value;
}
int? get vmStartAddress => dynamicSymbolFor(constants.vmSymbolName)?.value;
@override
int get isolateStartAddress {
final isolateStartSymbol = dynamicSymbolFor(constants.isolateSymbolName);
if (isolateStartSymbol == null) {
throw FormatException(
'Expected a dynamic symbol with name ${constants.isolateSymbolName}');
}
return isolateStartSymbol.value;
}
int? get isolateStartAddress =>
dynamicSymbolFor(constants.isolateSymbolName)?.value;
@override
String? get buildId {
@ -1210,11 +1281,4 @@ class Elf implements DwarfContainer {
buffer.writeln();
}
}
@override
String toString() {
var buffer = StringBuffer();
writeToStringBuffer(buffer);
return buffer.toString();
}
}

View file

@ -4,6 +4,7 @@
// ignore_for_file: constant_identifier_names
import 'dart:io';
import 'dart:typed_data';
import 'package:path/path.dart' as path;
@ -128,6 +129,7 @@ class LoadCommand {
final start = reader.offset; // cmdsize includes size of cmd and cmdsize.
final cmd = _readMachOUint32(reader);
final cmdsize = _readMachOUint32(reader);
assert(reader.remaining >= cmdsize - (reader.offset - start));
LoadCommand command = LoadCommand._(cmd, cmdsize);
switch (cmd) {
case LC_SEGMENT:
@ -268,7 +270,7 @@ class Section {
nreloc, flags, reserved1, reserved2, reserved3);
}
Reader refocus(Reader reader) => reader.refocusedCopy(offset, size);
Reader shrink(Reader reader) => reader.shrink(offset, size);
void writeToStringBuffer(StringBuffer buffer) {
buffer
@ -308,9 +310,8 @@ class SymbolTableCommand extends LoadCommand {
SymbolTable load(Reader reader) {
final stringTable =
StringTable.fromReader(reader.refocusedCopy(_stroff, _strsize));
return SymbolTable.fromReader(
reader.refocusedCopy(_symoff), _nsyms, stringTable);
StringTable.fromReader(reader.shrink(_stroff, _strsize));
return SymbolTable.fromReader(reader.shrink(_symoff), _nsyms, stringTable);
}
@override
@ -342,22 +343,32 @@ class MachOHeader {
static const _MH_MAGIC_64 = 0xfeedfacf;
static const _MH_CIGAM_64 = 0xcffaedfe;
static const _MH_DSYM = 0xa;
static int? _wordSize(int magic) => (magic == _MH_MAGIC || magic == _MH_CIGAM)
? 4
: (magic == _MH_MAGIC_64 || magic == _MH_CIGAM_64)
? 8
: null;
static Endian? _endian(int magic) =>
(magic == _MH_MAGIC || magic == _MH_MAGIC_64)
? Endian.host
: (magic == _MH_CIGAM || magic == _MH_CIGAM_64)
? (Endian.host == Endian.big ? Endian.little : Endian.big)
: null;
static MachOHeader? fromReader(Reader reader) {
final start = reader.offset;
// Initially assume host endianness.
reader.endian = Endian.host;
final magic = _readMachOUint32(reader);
if (magic == _MH_MAGIC || magic == _MH_CIGAM) {
reader.wordSize = 4;
} else if (magic == _MH_MAGIC_64 || magic == _MH_CIGAM_64) {
reader.wordSize = 8;
} else {
// Not an expected magic value, so not a supported Mach-O file.
return null;
}
if (magic == _MH_CIGAM || magic == _MH_CIGAM_64) {
reader.endian = Endian.host == Endian.big ? Endian.little : Endian.big;
}
final wordSize = _wordSize(magic);
final endian = _endian(magic);
// Not an expected magic value, so not a supported Mach-O file.
if (wordSize == null || endian == null) return null;
reader.wordSize = wordSize;
reader.endian = endian;
final cputype = _readMachOUint32(reader);
final cpusubtype = _readMachOUint32(reader);
final filetype = _readMachOUint32(reader);
@ -370,6 +381,10 @@ class MachOHeader {
sizeofcmds, flags, reserved, size);
}
int get wordSize => _wordSize(magic)!;
Endian get endian => _endian(magic)!;
bool get isDSYM => filetype == _MH_DSYM;
void writeToStringBuffer(StringBuffer buffer) {
buffer
..write('Magic: 0x')
@ -407,7 +422,7 @@ class MachOHeader {
}
}
class MachO implements DwarfContainer {
class MachO extends DwarfContainer {
final MachOHeader _header;
final List<LoadCommand> _commands;
final SymbolTable _symbolTable;
@ -423,17 +438,18 @@ class MachO implements DwarfContainer {
// make sure we have a reader that a) makes no assumptions about the
// endianness or word size, since we'll read those in the header and b)
// has an internal offset of 0 so absolute offsets can be used directly.
final reader = Reader.fromTypedData(ByteData.sublistView(machOReader.bdata,
machOReader.bdata.offsetInBytes + machOReader.offset));
final reader = machOReader.shrink(machOReader.offset);
final header = MachOHeader.fromReader(reader);
if (header == null) return null;
final commandReader =
reader.refocusedCopy(reader.offset, header.sizeofcmds);
final commandReader = reader.shrink(reader.offset, header.sizeofcmds);
final commands =
List.of(commandReader.readRepeated(LoadCommand.fromReader));
assert(commands.length == header.ncmds);
// This MachO file can't contain any debugging information.
if (commands.isEmpty) return null;
final symbolTable =
commands.whereType<SymbolTableCommand>().single.load(reader);
@ -441,7 +457,6 @@ class MachO implements DwarfContainer {
.whereType<SegmentCommand?>()
.firstWhere((sc) => sc!.segname == '__DWARF', orElse: () => null);
if (dwarfSegment == null) {
print("No DWARF information in Mach-O file");
return null;
}
@ -449,7 +464,7 @@ class MachO implements DwarfContainer {
StringTable? debugStringTable;
if (debugStringTableSection != null) {
debugStringTable =
StringTable.fromReader(debugStringTableSection.refocus(reader));
StringTable.fromReader(debugStringTableSection.shrink(reader));
}
final debugLineStringTableSection =
@ -457,7 +472,7 @@ class MachO implements DwarfContainer {
StringTable? debugLineStringTable;
if (debugLineStringTableSection != null) {
debugLineStringTable =
StringTable.fromReader(debugLineStringTableSection.refocus(reader));
StringTable.fromReader(debugLineStringTableSection.shrink(reader));
}
// Set the wordSize and endian of the original reader before returning.
@ -472,41 +487,41 @@ class MachO implements DwarfContainer {
if (!fileName.endsWith('.dSYM')) {
return fileName;
}
var baseName = path.basename(fileName);
baseName = baseName.substring(0, baseName.length - '.dSYM'.length);
return path.join(fileName, 'Contents', 'Resources', 'DWARF', baseName);
final dwarfDir =
Directory(path.join(fileName, 'Contents', 'Resources', 'DWARF'));
// The DWARF directory inside the .dSYM should contain a single MachO file.
final machoFile = dwarfDir.listSync().single as File;
return machoFile.path;
}
static MachO? fromFile(String fileName) =>
MachO.fromReader(Reader.fromFile(MachO.handleDSYM(fileName)));
@override
Reader abbreviationsTableReader(Reader reader) =>
_dwarfSegment.sections['__debug_abbrev']!.refocus(reader);
@override
Reader lineNumberInfoReader(Reader reader) =>
_dwarfSegment.sections['__debug_line']!.refocus(reader);
@override
Reader debugInfoReader(Reader reader) =>
_dwarfSegment.sections['__debug_info']!.refocus(reader);
bool get isDSYM => _header.isDSYM;
Reader applyWordSizeAndEndian(Reader reader) =>
Reader.fromTypedData(reader.bdata,
wordSize: _header.wordSize, endian: _header.endian);
@override
int get vmStartAddress {
if (!_symbolTable.containsKey(constants.vmSymbolName)) {
throw FormatException(
'Expected a dynamic symbol with name ${constants.vmSymbolName}');
}
return _symbolTable[constants.vmSymbolName]!.value;
}
String? get architecture => CpuType.fromCode(_header.cputype)?.dartName;
@override
int get isolateStartAddress {
if (!_symbolTable.containsKey(constants.isolateSymbolName)) {
throw FormatException(
'Expected a dynamic symbol with name ${constants.isolateSymbolName}');
}
return _symbolTable[constants.isolateSymbolName]!.value;
}
Reader abbreviationsTableReader(Reader containerReader) =>
_dwarfSegment.sections['__debug_abbrev']!.shrink(containerReader);
@override
Reader lineNumberInfoReader(Reader containerReader) =>
_dwarfSegment.sections['__debug_line']!.shrink(containerReader);
@override
Reader debugInfoReader(Reader containerReader) =>
_dwarfSegment.sections['__debug_info']!.shrink(containerReader);
@override
int? get vmStartAddress => _symbolTable[constants.vmSymbolName]?.value;
@override
int? get isolateStartAddress =>
_symbolTable[constants.isolateSymbolName]?.value;
@override
String? get buildId => null;
@ -550,6 +565,49 @@ class MachO implements DwarfContainer {
buffer.writeln('');
}
}
}
class UniversalBinaryArch {
final int cputype;
final int cpusubtype;
final int offset;
final int size;
final int align;
UniversalBinaryArch._(
this.cputype, this.cpusubtype, this.offset, this.size, this.align);
static UniversalBinaryArch fromReader(Reader reader) {
final cputype = _readMachOUint32(reader);
final cpusubtype = _readMachOUint32(reader);
final offset = _readMachOUint32(reader);
final size = _readMachOUint32(reader);
final align = _readMachOUint32(reader);
return UniversalBinaryArch._(cputype, cpusubtype, offset, size, align);
}
// Given a reader for the entire universal binary, returns a reader
// for only this architecture's contents.
Reader shrink(Reader reader) => reader.shrink(offset, size);
void writeToStringBuffer(StringBuffer buffer) {
buffer
..write(' Cpu Type: 0x')
..writeln(paddedHex(cputype, 4));
buffer
..write(' Cpu Subtype: 0x')
..writeln(paddedHex(cpusubtype, 4));
buffer
..write(' Offset: 0x')
..writeln(paddedHex(offset, 4));
buffer
..write(' Size: ')
..writeln(size);
buffer
..write(' Alignment: ')
..writeln(align);
}
@override
String toString() {
@ -558,3 +616,219 @@ class MachO implements DwarfContainer {
return buffer.toString();
}
}
class UniversalBinaryHeader {
final int magic;
final List<UniversalBinaryArch> _arches;
UniversalBinaryHeader._(this.magic, this._arches);
static const _FAT_MAGIC = 0xcafebabe;
static const _FAT_CIGAM = 0xbebafeca;
static UniversalBinaryHeader? fromReader(Reader originalReader) {
assert(originalReader.offset == 0);
// Make sure we have a reader that makes no assumptions about the
// endianness, since we'll read that in the header.
final reader =
Reader.fromTypedData(ByteData.sublistView(originalReader.bdata));
reader.wordSize = 4;
reader.endian = Endian.host;
final magic = _readMachOUint32(reader);
if (magic == _FAT_CIGAM) {
reader.endian = Endian.host == Endian.big ? Endian.little : Endian.big;
} else if (magic != _FAT_MAGIC) {
// Not a universal binary.
return null;
}
final archCount = _readMachOUint32(reader);
final arches = <UniversalBinaryArch>[];
for (int i = 0; i < archCount; i++) {
arches.add(UniversalBinaryArch.fromReader(reader));
}
return UniversalBinaryHeader._(magic, arches);
}
void writeToStringBuffer(StringBuffer buffer) {
buffer
..write('Magic: 0x')
..writeln(paddedHex(magic, 4));
buffer
..write('Number of architectures: ')
..writeln(_arches.length);
for (int i = 0; i < _arches.length; i++) {
buffer
..write('Arch ')
..write(i)
..writeln(':');
_arches[i].writeToStringBuffer(buffer);
}
}
@override
String toString() {
final buffer = StringBuffer();
writeToStringBuffer(buffer);
return buffer.toString();
}
}
/// Represents Dart architectures that have valid CPU type values in MachO.
enum CpuType {
arm(_CPU_ARCH_ARM, "arm"),
arm64(_CPU_ARCH_ARM | _CPU_ARCH_ABI64, "arm64"),
i386(_CPU_ARCH_X86, "ia32"),
x64(_CPU_ARCH_X86 | _CPU_ARCH_ABI64, "x64");
static const _CPU_ARCH_ABI64 = 0x01000000;
static const _CPU_ARCH_X86 = 7;
static const _CPU_ARCH_ARM = 12;
static const _prefix = 'CPU_ARCH';
/// The 32-bit MachO encoding for this architecture.
final int code;
/// The name of this architecture as reported by Dart, e.g., in
/// non-symbolic stack traces.
final String dartName;
const CpuType(this.code, this.dartName);
static CpuType? fromCode(int code) {
for (final value in values) {
if (value.code == code) return value;
}
return null;
}
static CpuType? fromDartName(String arch) {
for (final value in values) {
if (value.dartName == arch) return value;
}
return null;
}
/// Whether this CpuType represents a 64-bit architecture.
bool get is64Bit => code & _CPU_ARCH_ABI64 != 0;
@override
String toString() => '${_prefix}_${name.toUpperCase()}';
}
class UniversalBinary {
final UniversalBinaryHeader _header;
final Map<CpuType, UniversalBinaryArch> _arches;
final Map<UniversalBinaryArch, MachO> _contents;
UniversalBinary._(this._header, this._arches, this._contents);
static UniversalBinary? fromReader(Reader originalReader) {
// Universal binary files contain absolute offsets from the start of the
// file, so make sure we have a reader that has an internal offset of 0 so
// absolute offsets can be used directly.
final reader = originalReader.shrink(originalReader.offset);
final header = UniversalBinaryHeader.fromReader(reader);
if (header == null) {
return null;
}
final arches = <CpuType, UniversalBinaryArch>{};
final contents = <UniversalBinaryArch, MachO>{};
for (final arch in header._arches) {
final cpuType = CpuType.fromCode(arch.cputype);
if (cpuType == null) continue;
final archReader = arch.shrink(reader);
final macho = MachO.fromReader(archReader);
// The MachO parser either failed (likely due to a lack of debugging
// information) or this contains debugging information for something other
// than a Dart snapshot (since it contains neither a VM or isolate
// instruction section symbol).
if ((macho == null) ||
((macho.vmStartAddress == null) &&
(macho.isolateStartAddress == null))) {
continue;
}
if (macho.isDSYM) {
// Always take a dSYM section above a non-dSYM section. If there are
// multiple dSYM sections for some reason, the last one read is fine.
arches[cpuType] = arch;
} else if (!arches.containsKey(cpuType)) {
arches[cpuType] = arch;
}
contents[arch] = macho;
}
return UniversalBinary._(header, arches, contents);
}
static UniversalBinary? fromFile(String fileName) =>
UniversalBinary.fromReader(Reader.fromFile(MachO.handleDSYM(fileName)));
Iterable<CpuType> get architectures => _arches.keys;
Reader? readerForCpuType(Reader originalReader, CpuType cpuType) {
final arch = _arches[cpuType];
if (arch == null) return null;
final macho = _contents[arch]!;
// Universal binary files contain absolute offsets from the start of the
// file, so make sure to feed arch.shrink a reader that has an internal
// offset of 0.
return macho.applyWordSizeAndEndian(
arch.shrink(originalReader.shrink(originalReader.offset)));
}
DwarfContainer? containerForCpuType(CpuType cpuType) {
final arch = _arches[cpuType];
if (arch == null) return null;
return _contents[arch];
}
void writeToStringBuffer(StringBuffer buffer) {
buffer
..writeln('----------------------------------------')
..writeln(' Universal Binary Header')
..writeln('----------------------------------------')
..writeln('');
_header.writeToStringBuffer(buffer);
for (final cpuType in _arches.keys) {
buffer
..writeln('')
..writeln('')
..writeln('----------------------------------------------------------')
..writeln(' DWARF-containing Mach-O Contents for $cpuType')
..writeln('----------------------------------------------------------')
..writeln('');
_contents[_arches[cpuType]!]!.writeToStringBuffer(buffer);
}
}
@override
String toString() {
final buffer = StringBuffer();
writeToStringBuffer(buffer);
return buffer.toString();
}
}
const _magicByteOffset = 0;
const _cpuTypeByteOffset = 4;
const _fileTypeByteOffset = 12;
/// Used by certain Dart tests to create a MachO file that only contains a
/// header for the given architecture.
Uint8List? emptyMachOForArchitecture(String dartName) {
final cpuType = CpuType.fromDartName(dartName);
if (cpuType == null) return null;
// 4 bytes * 7 fields + 4 padding bytes on 64-bit architectures.
final contents = Uint8List(32);
// We'll leave most of the fields at 0.
final byteData = ByteData.sublistView(contents);
// Use the host endian magic number marker corresponding to the bit size.
byteData.setUint32(
_magicByteOffset,
cpuType.is64Bit ? MachOHeader._MH_MAGIC_64 : MachOHeader._MH_MAGIC,
Endian.host);
byteData.setUint32(_cpuTypeByteOffset, cpuType.code, Endian.host);
// Just to set it to a valid file type, even though there are no contents.
byteData.setUint32(_fileTypeByteOffset, MachOHeader._MH_DSYM, Endian.host);
return contents;
}

View file

@ -6,8 +6,8 @@ import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
String paddedHex(int value, [int bytes = 0]) =>
value.toRadixString(16).padLeft(2 * bytes, '0');
String paddedHex(int value, [int? bytes]) =>
value.toRadixString(16).padLeft(2 * (bytes ?? 0), '0');
class Reader {
final ByteData bdata;
@ -35,23 +35,25 @@ class Reader {
_endian = endian,
bdata = ByteData.sublistView(File(path).readAsBytesSync());
/// Returns a reader focused on a different portion of the underlying buffer.
/// If size is not provided, then the new reader extends to the end of the
/// buffer.
Reader refocusedCopy(int pos, [int? size]) {
assert(pos >= 0 && pos < bdata.buffer.lengthInBytes);
// Similar to the sublistView constructor for classes in dart:typed-data.
// That is, it creates a new reader that views a subset of the underlying
// buffer currently viewed by [this]. Thus, [pos] and [size] are relative
// to the view of [this], not to the underlying buffer as a whole.
Reader shrink(int pos, [int? size]) {
assert(pos >= 0 && pos <= length);
if (size != null) {
assert(size >= 0 && (pos + size) <= bdata.buffer.lengthInBytes);
assert(size >= 0 && (pos + size) <= length);
} else {
size = bdata.buffer.lengthInBytes - pos;
size = length - pos;
}
return Reader.fromTypedData(ByteData.view(bdata.buffer, pos, size),
return Reader.fromTypedData(ByteData.sublistView(bdata, pos, pos + size),
wordSize: _wordSize, endian: _endian);
}
int get start => bdata.offsetInBytes;
int get offset => _offset;
int get length => bdata.lengthInBytes;
int get remaining => length - offset;
bool get done => _offset >= length;
Uint8List get bytes => Uint8List.sublistView(bdata);
@ -65,7 +67,7 @@ class Reader {
int readBytes(int size, {bool signed = false}) {
if (_offset + size > length) {
throw ArgumentError('attempt to read $size bytes with only '
'${length - _offset} bytes remaining in the reader');
'$remaining bytes remaining in the reader');
}
final start = _offset;
_offset += size;
@ -93,7 +95,7 @@ class Reader {
ByteData readRawBytes(int size) {
if (offset + size > length) {
throw ArgumentError('attempt to read $size bytes with only '
'${length - _offset} bytes remaining in the reader');
'$remaining bytes remaining in the reader');
}
final start = _offset;
_offset += size;
@ -206,19 +208,19 @@ class Reader {
..writeln();
buffer
..write('Start: 0x')
..write(paddedHex(start, _wordSize ?? 0))
..write(paddedHex(start, _wordSize))
..write(' (')
..write(start)
..writeln(')');
buffer
..write('Offset: 0x')
..write(paddedHex(offset, _wordSize ?? 0))
..write(paddedHex(offset, _wordSize))
..write(' (')
..write(offset)
..writeln(')');
buffer
..write('Length: 0x')
..write(paddedHex(length, _wordSize ?? 0))
..write(paddedHex(length, _wordSize))
..write(' (')
..write(length)
..writeln(')');

View file

@ -1,5 +1,5 @@
name: native_stack_traces
version: 0.5.1
version: 0.5.2
description: Utilities for working with non-symbolic stack traces.
repository: https://github.com/dart-lang/sdk/tree/main/pkg/native_stack_traces

View file

@ -12,6 +12,7 @@ import "dart:io";
import 'package:expect/expect.dart';
import 'package:native_stack_traces/native_stack_traces.dart';
import 'package:native_stack_traces/src/macho.dart';
import 'package:path/path.dart' as path;
import 'use_flag_test_helper.dart';
@ -110,58 +111,101 @@ main(List<String> args) async {
// Check with DWARF in generated snapshot.
await compareTraces(nonDwarfTrace1, output1, output2, scriptDwarfSnapshot);
// 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 ((Platform.isLinux || Platform.isMacOS) && !isSimulator) {
final scriptAssembly = path.join(tempDir, 'dwarf_assembly.S');
final scriptDwarfAssemblyDebugInfo =
path.join(tempDir, 'dwarf_assembly_info.so');
final scriptDwarfAssemblySnapshot =
path.join(tempDir, 'dwarf_assembly.so');
// We get a separate .dSYM bundle on MacOS.
final scriptDwarfAssemblyDebugSnapshot =
scriptDwarfAssemblySnapshot + (Platform.isMacOS ? '.dSYM' : '');
await run(genSnapshot, <String>[
// We test --dwarf-stack-traces-mode, not --dwarf-stack-traces, because
// the latter is a handler that sets the former and also may change
// other flags. This way, we limit the difference between the two
// snapshots and also directly test the flag saved as a VM global flag.
'--dwarf-stack-traces-mode',
'--save-debugging-info=$scriptDwarfAssemblyDebugInfo',
'--snapshot-kind=app-aot-assembly',
'--assembly=$scriptAssembly',
scriptDill,
]);
await assembleSnapshot(scriptAssembly, scriptDwarfAssemblySnapshot,
debug: true);
// Run the resulting Dwarf-AOT compiled script.
final assemblyOutput1 = await runTestProgram(aotRuntime, <String>[
'--dwarf-stack-traces-mode',
scriptDwarfAssemblySnapshot,
scriptDill,
]);
final assemblyOutput2 = await runTestProgram(aotRuntime, <String>[
'--no-dwarf-stack-traces-mode',
scriptDwarfAssemblySnapshot,
scriptDill,
]);
// Check with DWARF in assembled snapshot.
await compareTraces(nonDwarfTrace1, assemblyOutput1, assemblyOutput2,
scriptDwarfAssemblyDebugSnapshot,
fromAssembly: true);
// Check with DWARF from separate debugging information.
await compareTraces(nonDwarfTrace1, assemblyOutput1, assemblyOutput2,
scriptDwarfAssemblyDebugInfo,
fromAssembly: true);
}
await testAssembly(tempDir, scriptDill, nonDwarfTrace1);
});
}
const _lipoBinary = "/usr/bin/lipo";
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.S');
final scriptDwarfAssemblyDebugInfo =
path.join(tempDir, 'dwarf_assembly_info.so');
final scriptDwarfAssemblySnapshot = path.join(tempDir, 'dwarf_assembly.so');
// We get a separate .dSYM bundle on MacOS.
final scriptDwarfAssemblyDebugSnapshot =
scriptDwarfAssemblySnapshot + (Platform.isMacOS ? '.dSYM' : '');
await run(genSnapshot, <String>[
// We test --dwarf-stack-traces-mode, not --dwarf-stack-traces, because
// the latter is a handler that sets the former and also may change
// other flags. This way, we limit the difference between the two
// snapshots and also directly test the flag saved as a VM global flag.
'--dwarf-stack-traces-mode',
'--save-debugging-info=$scriptDwarfAssemblyDebugInfo',
'--snapshot-kind=app-aot-assembly',
'--assembly=$scriptAssembly',
scriptDill,
]);
await assembleSnapshot(scriptAssembly, scriptDwarfAssemblySnapshot,
debug: true);
// Run the resulting Dwarf-AOT compiled script.
final assemblyOutput1 = await runTestProgram(aotRuntime, <String>[
'--dwarf-stack-traces-mode',
scriptDwarfAssemblySnapshot,
scriptDill,
]);
final assemblyOutput2 = await runTestProgram(aotRuntime, <String>[
'--no-dwarf-stack-traces-mode',
scriptDwarfAssemblySnapshot,
scriptDill,
]);
// Check with DWARF in assembled snapshot.
await compareTraces(nonDwarfTrace, assemblyOutput1, assemblyOutput2,
scriptDwarfAssemblyDebugSnapshot,
fromAssembly: true);
// Check with DWARF from separate debugging information.
await compareTraces(nonDwarfTrace, assemblyOutput1, assemblyOutput2,
scriptDwarfAssemblyDebugInfo,
fromAssembly: true);
// 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) {
// Don't create an empty file for the current architecture.
if (arch == 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(
_lipoBinary, <String>[...machoFiles, '-create', '-output', binaryPath]);
await compareTraces(
nonDwarfTrace, assemblyOutput1, assemblyOutput2, binaryPath,
fromAssembly: true);
}
final scriptDwarfAssemblyDebugSnapshotFile =
MachO.handleDSYM(scriptDwarfAssemblyDebugSnapshot);
await testUniversalBinary(path.join(tempDir, "ub-single"),
<String>[scriptDwarfAssemblyDebugSnapshotFile]);
await testUniversalBinary(path.join(tempDir, "ub-multiple"),
<String>[...emptyFiles.values, scriptDwarfAssemblyDebugSnapshotFile]);
}
class DwarfTestOutput {
final List<String> trace;
final int allocateObjectInstructionsOffset;
@ -206,21 +250,22 @@ Future<void> compareTraces(List<String> nonDwarfTrace, DwarfTestOutput output1,
// Check that build IDs match for traces from running ELF snapshots.
if (!fromAssembly) {
Expect.isNotNull(dwarf!.buildId);
print('Dwarf build ID: "${dwarf.buildId!}"');
final dwarfBuildId = dwarf!.buildId();
Expect.isNotNull(dwarfBuildId);
print('Dwarf build ID: "${dwarfBuildId!}"');
// We should never generate an all-zero build ID.
Expect.notEquals(dwarf.buildId, "00000000000000000000000000000000");
Expect.notEquals(dwarfBuildId, "00000000000000000000000000000000");
// This is a common failure case as well, when HashBitsContainer ends up
// hashing over seemingly empty sections.
Expect.notEquals(dwarf.buildId, "01000000010000000100000001000000");
Expect.notEquals(dwarfBuildId, "01000000010000000100000001000000");
final buildId1 = buildId(output1.trace);
Expect.isFalse(buildId1.isEmpty);
print('Trace 1 build ID: "${buildId1}"');
Expect.equals(dwarf.buildId, buildId1);
Expect.equals(dwarfBuildId, buildId1);
final buildId2 = buildId(output2.trace);
Expect.isFalse(buildId2.isEmpty);
print('Trace 2 build ID: "${buildId2}"');
Expect.equals(dwarf.buildId, buildId2);
Expect.equals(dwarfBuildId, buildId2);
}
final decoder = DwarfStackTraceDecoder(dwarf!);
@ -388,3 +433,21 @@ final _dsoBaseRE = RegExp(r'isolate_dso_base: ([a-f\d]+)');
Iterable<int> dsoBaseAddresses(Iterable<String> lines) =>
parseUsingAddressRegExp(_dsoBaseRE, lines);
// We only list architectures supported by the current CpuType enum in
// pkg:native_stack_traces/src/macho.dart.
const _machOArchNames = <String, String>{
"ARM": "arm",
"ARM64": "arm64",
"IA32": "ia32",
"X64": "x64",
};
String? get dartNameForCurrentArchitecture {
for (final entry in _machOArchNames.entries) {
if (buildDir.endsWith(entry.key)) {
return entry.value;
}
}
return null;
}

View file

@ -14,6 +14,7 @@ import "dart:io";
import 'package:expect/expect.dart';
import 'package:native_stack_traces/native_stack_traces.dart';
import 'package:native_stack_traces/src/macho.dart';
import 'package:path/path.dart' as path;
import 'use_flag_test_helper.dart';
@ -112,58 +113,101 @@ main(List<String> args) async {
// Check with DWARF in generated snapshot.
await compareTraces(nonDwarfTrace1, output1, output2, scriptDwarfSnapshot);
// 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 ((Platform.isLinux || Platform.isMacOS) && !isSimulator) {
final scriptAssembly = path.join(tempDir, 'dwarf_assembly.S');
final scriptDwarfAssemblyDebugInfo =
path.join(tempDir, 'dwarf_assembly_info.so');
final scriptDwarfAssemblySnapshot =
path.join(tempDir, 'dwarf_assembly.so');
// We get a separate .dSYM bundle on MacOS.
final scriptDwarfAssemblyDebugSnapshot =
scriptDwarfAssemblySnapshot + (Platform.isMacOS ? '.dSYM' : '');
await run(genSnapshot, <String>[
// We test --dwarf-stack-traces-mode, not --dwarf-stack-traces, because
// the latter is a handler that sets the former and also may change
// other flags. This way, we limit the difference between the two
// snapshots and also directly test the flag saved as a VM global flag.
'--dwarf-stack-traces-mode',
'--save-debugging-info=$scriptDwarfAssemblyDebugInfo',
'--snapshot-kind=app-aot-assembly',
'--assembly=$scriptAssembly',
scriptDill,
]);
await assembleSnapshot(scriptAssembly, scriptDwarfAssemblySnapshot,
debug: true);
// Run the resulting Dwarf-AOT compiled script.
final assemblyOutput1 = await runTestProgram(aotRuntime, <String>[
'--dwarf-stack-traces-mode',
scriptDwarfAssemblySnapshot,
scriptDill,
]);
final assemblyOutput2 = await runTestProgram(aotRuntime, <String>[
'--no-dwarf-stack-traces-mode',
scriptDwarfAssemblySnapshot,
scriptDill,
]);
// Check with DWARF in assembled snapshot.
await compareTraces(nonDwarfTrace1, assemblyOutput1, assemblyOutput2,
scriptDwarfAssemblyDebugSnapshot,
fromAssembly: true);
// Check with DWARF from separate debugging information.
await compareTraces(nonDwarfTrace1, assemblyOutput1, assemblyOutput2,
scriptDwarfAssemblyDebugInfo,
fromAssembly: true);
}
await testAssembly(tempDir, scriptDill, nonDwarfTrace1);
});
}
const _lipoBinary = "/usr/bin/lipo";
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.S');
final scriptDwarfAssemblyDebugInfo =
path.join(tempDir, 'dwarf_assembly_info.so');
final scriptDwarfAssemblySnapshot = path.join(tempDir, 'dwarf_assembly.so');
// We get a separate .dSYM bundle on MacOS.
final scriptDwarfAssemblyDebugSnapshot =
scriptDwarfAssemblySnapshot + (Platform.isMacOS ? '.dSYM' : '');
await run(genSnapshot, <String>[
// We test --dwarf-stack-traces-mode, not --dwarf-stack-traces, because
// the latter is a handler that sets the former and also may change
// other flags. This way, we limit the difference between the two
// snapshots and also directly test the flag saved as a VM global flag.
'--dwarf-stack-traces-mode',
'--save-debugging-info=$scriptDwarfAssemblyDebugInfo',
'--snapshot-kind=app-aot-assembly',
'--assembly=$scriptAssembly',
scriptDill,
]);
await assembleSnapshot(scriptAssembly, scriptDwarfAssemblySnapshot,
debug: true);
// Run the resulting Dwarf-AOT compiled script.
final assemblyOutput1 = await runTestProgram(aotRuntime, <String>[
'--dwarf-stack-traces-mode',
scriptDwarfAssemblySnapshot,
scriptDill,
]);
final assemblyOutput2 = await runTestProgram(aotRuntime, <String>[
'--no-dwarf-stack-traces-mode',
scriptDwarfAssemblySnapshot,
scriptDill,
]);
// Check with DWARF in assembled snapshot.
await compareTraces(nonDwarfTrace, assemblyOutput1, assemblyOutput2,
scriptDwarfAssemblyDebugSnapshot,
fromAssembly: true);
// Check with DWARF from separate debugging information.
await compareTraces(nonDwarfTrace, assemblyOutput1, assemblyOutput2,
scriptDwarfAssemblyDebugInfo,
fromAssembly: true);
// 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) {
// Don't create an empty file for the current architecture.
if (arch == 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(
_lipoBinary, <String>[...machoFiles, '-create', '-output', binaryPath]);
await compareTraces(
nonDwarfTrace, assemblyOutput1, assemblyOutput2, binaryPath,
fromAssembly: true);
}
final scriptDwarfAssemblyDebugSnapshotFile =
MachO.handleDSYM(scriptDwarfAssemblyDebugSnapshot);
await testUniversalBinary(path.join(tempDir, "ub-single"),
<String>[scriptDwarfAssemblyDebugSnapshotFile]);
await testUniversalBinary(path.join(tempDir, "ub-multiple"),
<String>[...emptyFiles.values, scriptDwarfAssemblyDebugSnapshotFile]);
}
class DwarfTestOutput {
final List<String> trace;
final int allocateObjectInstructionsOffset;
@ -208,21 +252,22 @@ Future<void> compareTraces(List<String> nonDwarfTrace, DwarfTestOutput output1,
// Check that build IDs match for traces from running ELF snapshots.
if (!fromAssembly) {
Expect.isNotNull(dwarf.buildId);
print('Dwarf build ID: "${dwarf.buildId}"');
final dwarfBuildId = dwarf.buildId();
Expect.isNotNull(dwarfBuildId);
print('Dwarf build ID: "$dwarfBuildId"');
// We should never generate an all-zero build ID.
Expect.notEquals(dwarf.buildId, "00000000000000000000000000000000");
Expect.notEquals(dwarfBuildId, "00000000000000000000000000000000");
// This is a common failure case as well, when HashBitsContainer ends up
// hashing over seemingly empty sections.
Expect.notEquals(dwarf.buildId, "01000000010000000100000001000000");
Expect.notEquals(dwarfBuildId, "01000000010000000100000001000000");
final buildId1 = buildId(output1.trace);
Expect.isFalse(buildId1.isEmpty);
print('Trace 1 build ID: "${buildId1}"');
Expect.equals(dwarf.buildId, buildId1);
Expect.equals(dwarfBuildId, buildId1);
final buildId2 = buildId(output2.trace);
Expect.isFalse(buildId2.isEmpty);
print('Trace 2 build ID: "${buildId2}"');
Expect.equals(dwarf.buildId, buildId2);
Expect.equals(dwarfBuildId, buildId2);
}
final decoder = DwarfStackTraceDecoder(dwarf);
@ -390,3 +435,21 @@ final _dsoBaseRE = RegExp(r'isolate_dso_base: ([a-f\d]+)');
Iterable<int> dsoBaseAddresses(Iterable<String> lines) =>
parseUsingAddressRegExp(_dsoBaseRE, lines);
// We only list architectures supported by the current CpuType enum in
// pkg:native_stack_traces/src/macho.dart.
const _machOArchNames = <String, String>{
"ARM": "arm",
"ARM64": "arm64",
"IA32": "ia32",
"X64": "x64",
};
String get dartNameForCurrentArchitecture {
for (final entry in _machOArchNames.entries) {
if (buildDir.endsWith(entry.key)) {
return entry.value;
}
}
return null;
}

View file

@ -55,36 +55,77 @@ Future<void> checkStackTrace(String rawStack, Dwarf dwarf,
await Stream.value(rawStack).transform(const LineSplitter()).toList();
final pcOffsets = collectPCOffsets(rawLines).toList();
Expect.isNotEmpty(pcOffsets);
print('PCOffsets:');
for (final offset in pcOffsets) {
print('* $offset');
}
print('');
// We should have at least enough PC addresses to cover the frames we'll be
// checking.
Expect.isTrue(pcOffsets.length >= expectedCallsInfo.length);
final isolateStart = dwarf.isolateStartAddress(pcOffsets.first.architecture);
Expect.isNotNull(isolateStart);
print('Isolate start offset: 0x${isolateStart!.toRadixString(16)}');
// The addresses of the stack frames in the separate DWARF debugging info.
final virtualAddresses =
pcOffsets.map((o) => dwarf.virtualAddressOf(o)).toList();
print('Virtual addresses from PCOffsets:');
for (final address in virtualAddresses) {
print('* 0x${address.toRadixString(16)}');
}
print('');
// Some double-checks using other information in the non-symbolic stack trace.
final dsoBase = dsoBaseAddresses(rawLines).single;
print('DSO base address: 0x${dsoBase.toRadixString(16)}');
final absoluteIsolateStart = isolateStartAddresses(rawLines).single;
print('Absolute isolate start address: '
'0x${absoluteIsolateStart.toRadixString(16)}');
final absolutes = absoluteAddresses(rawLines);
// The relocated addresses of the stack frames in the loaded DSO. These is
// only guaranteed to be the same as virtualAddresses if the built-in ELF
// generator was used to create the snapshot.
final relocatedAddresses = absolutes.map((a) => a - dsoBase);
final explicits = explicitVirtualAddresses(rawLines);
print('Relocated absolute addresses:');
for (final address in relocatedAddresses) {
print('* 0x${address.toRadixString(16)}');
}
print('');
// Explicits will be empty if not generating ELF snapshots directly, which
// means we can't depend on virtual addresses in the snapshot lining up with
// those in the separate debugging information.
final explicits = explicitVirtualAddresses(rawLines);
if (explicits.isNotEmpty) {
print('Explicit virtual addresses:');
for (final address in explicits) {
print('* 0x${address.toRadixString(16)}');
}
print('');
// Direct-to-ELF snapshots should have a build ID.
Expect.isNotNull(dwarf.buildId);
Expect.deepEquals(relocatedAddresses, virtualAddresses);
Expect.deepEquals(explicits, virtualAddresses);
// This is an ELF snapshot, so check that these two are the same.
Expect.deepEquals(virtualAddresses, relocatedAddresses);
}
final gotCallsInfo = <List<DartCallInfo>>[];
for (final addr in virtualAddresses) {
final externalCallInfo = dwarf.callInfoFor(addr);
for (final offset in pcOffsets) {
final externalCallInfo = dwarf.callInfoForPCOffset(offset);
Expect.isNotNull(externalCallInfo);
final allCallInfo = dwarf.callInfoFor(addr, includeInternalFrames: true);
final allCallInfo =
dwarf.callInfoForPCOffset(offset, includeInternalFrames: true);
Expect.isNotNull(allCallInfo);
for (final call in externalCallInfo!) {
Expect.isTrue(call is DartCallInfo, "got non-Dart call info ${call}");
@ -237,3 +278,8 @@ final _dsoBaseRE = RegExp(r'isolate_dso_base: ([a-f\d]+)');
Iterable<int> dsoBaseAddresses(Iterable<String> lines) =>
parseUsingAddressRegExp(_dsoBaseRE, lines);
final _isolateStartRE = RegExp(r'isolate_instructions: ([a-f\d]+)');
Iterable<int> isolateStartAddresses(Iterable<String> lines) =>
parseUsingAddressRegExp(_isolateStartRE, lines);

View file

@ -57,36 +57,77 @@ Future<void> checkStackTrace(String rawStack, Dwarf dwarf,
await Stream.value(rawStack).transform(const LineSplitter()).toList();
final pcOffsets = collectPCOffsets(rawLines).toList();
Expect.isNotEmpty(pcOffsets);
print('PCOffsets:');
for (final offset in pcOffsets) {
print('* $offset');
}
print('');
// We should have at least enough PC addresses to cover the frames we'll be
// checking.
Expect.isTrue(pcOffsets.length >= expectedCallsInfo.length);
final isolateStart = dwarf.isolateStartAddress(pcOffsets.first.architecture);
Expect.isNotNull(isolateStart);
print('Isolate start offset: 0x${isolateStart.toRadixString(16)}');
// The addresses of the stack frames in the separate DWARF debugging info.
final virtualAddresses =
pcOffsets.map((o) => dwarf.virtualAddressOf(o)).toList();
print('Virtual addresses from PCOffsets:');
for (final address in virtualAddresses) {
print('* 0x${address.toRadixString(16)}');
}
print('');
// Some double-checks using other information in the non-symbolic stack trace.
final dsoBase = dsoBaseAddresses(rawLines).single;
print('DSO base address: 0x${dsoBase.toRadixString(16)}');
final absoluteIsolateStart = isolateStartAddresses(rawLines).single;
print('Absolute isolate start address: '
'0x${absoluteIsolateStart.toRadixString(16)}');
final absolutes = absoluteAddresses(rawLines);
// The relocated addresses of the stack frames in the loaded DSO. These is
// only guaranteed to be the same as virtualAddresses if the built-in ELF
// generator was used to create the snapshot.
final relocatedAddresses = absolutes.map((a) => a - dsoBase);
final explicits = explicitVirtualAddresses(rawLines);
print('Relocated absolute addresses:');
for (final address in relocatedAddresses) {
print('* 0x${address.toRadixString(16)}');
}
print('');
// Explicits will be empty if not generating ELF snapshots directly, which
// means we can't depend on virtual addresses in the snapshot lining up with
// those in the separate debugging information.
final explicits = explicitVirtualAddresses(rawLines);
if (explicits.isNotEmpty) {
print('Explicit virtual addresses:');
for (final address in explicits) {
print('* 0x${address.toRadixString(16)}');
}
print('');
// Direct-to-ELF snapshots should have a build ID.
Expect.isNotNull(dwarf.buildId);
Expect.deepEquals(relocatedAddresses, virtualAddresses);
Expect.deepEquals(explicits, virtualAddresses);
// This is an ELF snapshot, so check that these two are the same.
Expect.deepEquals(virtualAddresses, relocatedAddresses);
}
final gotCallsInfo = <List<DartCallInfo>>[];
for (final addr in virtualAddresses) {
final externalCallInfo = dwarf.callInfoFor(addr);
for (final offset in pcOffsets) {
final externalCallInfo = dwarf.callInfoForPCOffset(offset);
Expect.isNotNull(externalCallInfo);
final allCallInfo = dwarf.callInfoFor(addr, includeInternalFrames: true);
final allCallInfo =
dwarf.callInfoForPCOffset(offset, includeInternalFrames: true);
Expect.isNotNull(allCallInfo);
for (final call in externalCallInfo) {
Expect.isTrue(call is DartCallInfo, "got non-Dart call info ${call}");
@ -239,3 +280,8 @@ final _dsoBaseRE = RegExp(r'isolate_dso_base: ([a-f\d]+)');
Iterable<int> dsoBaseAddresses(Iterable<String> lines) =>
parseUsingAddressRegExp(_dsoBaseRE, lines);
final _isolateStartRE = RegExp(r'isolate_instructions: ([a-f\d]+)');
Iterable<int> isolateStartAddresses(Iterable<String> lines) =>
parseUsingAddressRegExp(_isolateStartRE, lines);