blob: 1e0b38ed388717ac781c8c386c73baf83ef53f4b [file] [log] [blame] [edit]
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// ignore_for_file: constant_identifier_names
import 'dart:io';
import 'dart:typed_data';
import 'package:path/path.dart' as path;
import 'constants.dart' as constants;
import 'dwarf_container.dart';
import 'reader.dart';
int _readMachOUint8(Reader reader) => reader.readByte(signed: false);
int _readMachOUint16(Reader reader) => reader.readBytes(2, signed: false);
int _readMachOUint32(Reader reader) => reader.readBytes(4, signed: false);
int _readMachOUword(Reader reader) =>
reader.readBytes(reader.wordSize, signed: false);
class StringTable implements DwarfContainerStringTable {
final Map<int, String> _stringsByOffset;
StringTable._(this._stringsByOffset);
static StringTable fromReader(Reader reader) => StringTable._(Map.fromEntries(
reader.readRepeatedWithOffsets((r) => r.readNullTerminatedString())));
@override
String? operator [](int index) {
// Fast case: Index is for the start of a null terminated string.
if (_stringsByOffset.containsKey(index)) {
return _stringsByOffset[index];
}
// We can index into null terminated string entries for suffixes of
// that string, so do a linear search to find the appropriate entry.
for (final kv in _stringsByOffset.entries) {
final start = index - kv.key;
if (start >= 0 && start <= kv.value.length) {
return kv.value.substring(start);
}
}
return null;
}
void writeToStringBuffer(StringBuffer buffer) {
for (final k in _stringsByOffset.keys) {
buffer
..write(k.toString().padLeft(8, ' '))
..write(' => ')
..writeln(_stringsByOffset[k]);
}
}
@override
String toString() {
final buffer = StringBuffer();
writeToStringBuffer(buffer);
return buffer.toString();
}
}
class Symbol implements DwarfContainerSymbol {
final int index;
final int type;
final int sect;
final int desc;
@override
final int value;
@override
late final String name;
Symbol._(this.index, this.type, this.sect, this.desc, this.value);
static Symbol fromReader(Reader reader) {
final index = _readMachOUint32(reader);
final type = _readMachOUint8(reader);
final sect = _readMachOUint8(reader);
final desc = _readMachOUint16(reader);
final value = _readMachOUword(reader);
return Symbol._(index, type, sect, desc, value);
}
}
class SymbolTable {
final Map<String, Symbol> _symbols;
SymbolTable._(this._symbols);
static SymbolTable fromReader(
Reader reader, int nsyms, StringTable stringTable) {
final symbols = <String, Symbol>{};
for (int i = 0; i < nsyms; i++) {
final symbol = Symbol.fromReader(reader);
final index = symbol.index;
final name = stringTable[index];
if (name == null) {
throw FormatException('Index $index not found in string table');
}
symbol.name = name;
symbols[name] = symbol;
}
return SymbolTable._(symbols);
}
Iterable<String> get keys => _symbols.keys;
Iterable<Symbol> get values => _symbols.values;
Symbol? operator [](String name) => _symbols[name];
bool containsKey(String name) => _symbols.containsKey(name);
}
class LoadCommand {
final int cmd;
final int cmdsize;
LoadCommand._(this.cmd, this.cmdsize);
static const LC_REQ_DYLD = 0x80000000;
static const LC_SEGMENT = 0x1;
static const LC_SYMTAB = 0x2;
static const LC_ID_DYLIB = 0xd;
static const LC_SEGMENT_64 = 0x19;
static const LC_UUID = 0x1b;
static const LC_RPATH = 0x1c | LC_REQ_DYLD;
static const LC_CODE_SIGNATURE = 0x1d; // Only used in vm/dart tests.
static const LC_BUILD_VERSION = 0x32;
static LoadCommand fromReader(Reader reader) {
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:
case LC_SEGMENT_64:
command = SegmentCommand.fromReader(reader, cmd, cmdsize);
break;
case LC_SYMTAB:
command = SymbolTableCommand.fromReader(reader, cmd, cmdsize);
break;
case LC_UUID:
command = UuidCommand.fromReader(reader, cmd, cmdsize);
break;
case LC_BUILD_VERSION:
command = BuildVersionCommand.fromReader(reader, cmd, cmdsize);
break;
case LC_ID_DYLIB:
command = DylibCommand.fromReader(reader, cmd, cmdsize);
case LC_RPATH:
command = RunPathCommand.fromReader(reader, cmd, cmdsize);
default:
break;
}
reader.seek(start + cmdsize, absolute: true);
return command;
}
void writeToStringBuffer(StringBuffer buffer) {
buffer
..write('Uninterpreted command 0x')
..write(cmd.toRadixString(16))
..write(' of size ')
..writeln(cmdsize);
}
@override
String toString() {
StringBuffer buffer = StringBuffer();
writeToStringBuffer(buffer);
return buffer.toString();
}
}
class SegmentCommand extends LoadCommand {
final String segname;
final int vmaddr;
final int vmsize;
final int fileoff;
final int filesize;
final int maxprot;
final int initprot;
final int nsects;
final int flags;
final Map<String, Section> sections;
SegmentCommand._(
super.cmd,
super.cmdsize,
this.segname,
this.vmaddr,
this.vmsize,
this.fileoff,
this.filesize,
this.maxprot,
this.initprot,
this.nsects,
this.flags,
this.sections)
: super._();
static SegmentCommand fromReader(Reader reader, int cmd, int cmdsize) {
final segname = reader.readFixedLengthNullTerminatedString(16);
final vmaddr = _readMachOUword(reader);
final vmsize = _readMachOUword(reader);
final fileoff = _readMachOUword(reader);
final filesize = _readMachOUword(reader);
final maxprot = _readMachOUint32(reader);
final initprot = _readMachOUint32(reader);
final nsects = _readMachOUint32(reader);
final flags = _readMachOUint32(reader);
final sections = <String, Section>{};
for (int i = 0; i < nsects; i++) {
final section = Section.fromReader(reader);
sections[section.sectname] = section;
}
return SegmentCommand._(cmd, cmdsize, segname, vmaddr, vmsize, fileoff,
filesize, maxprot, initprot, nsects, flags, sections);
}
@override
void writeToStringBuffer(StringBuffer buffer) {
buffer
..write('Segment "')
..write(segname)
..write('" of size ')
..write(filesize)
..write(' at offset 0x')
..writeln(fileoff.toRadixString(16));
buffer.writeln('Sections:');
for (final section in sections.values) {
section.writeToStringBuffer(buffer);
buffer.writeln();
}
}
}
class Section {
String sectname;
String segname;
int addr;
int size;
int offset;
int align;
int reloff;
int nreloc;
int flags;
int reserved1;
int reserved2;
int? reserved3;
Section._(
this.sectname,
this.segname,
this.addr,
this.size,
this.offset,
this.align,
this.reloff,
this.nreloc,
this.flags,
this.reserved1,
this.reserved2,
this.reserved3);
static Section fromReader(Reader reader) {
final sectname = reader.readFixedLengthNullTerminatedString(16);
final segname = reader.readFixedLengthNullTerminatedString(16);
final addr = _readMachOUword(reader);
final size = _readMachOUword(reader);
final offset = _readMachOUint32(reader);
final align = _readMachOUint32(reader);
final reloff = _readMachOUint32(reader);
final nreloc = _readMachOUint32(reader);
final flags = _readMachOUint32(reader);
final reserved1 = _readMachOUint32(reader);
final reserved2 = _readMachOUint32(reader);
final reserved3 = (reader.wordSize == 8) ? _readMachOUint32(reader) : null;
return Section._(sectname, segname, addr, size, offset, align, reloff,
nreloc, flags, reserved1, reserved2, reserved3);
}
Reader shrink(Reader reader) => reader.shrink(offset, size);
void writeToStringBuffer(StringBuffer buffer) {
buffer
..write('Section "')
..write(sectname)
..write('" of size ')
..write(size)
..write(' at offset 0x')
..write(paddedHex(offset, 4));
}
@override
String toString() {
StringBuffer buffer = StringBuffer();
writeToStringBuffer(buffer);
return buffer.toString();
}
}
class SymbolTableCommand extends LoadCommand {
final int _symoff;
final int _nsyms;
final int _stroff;
final int _strsize;
SymbolTableCommand._(super.cmd, super.cmdsize, this._symoff, this._nsyms,
this._stroff, this._strsize)
: super._();
static SymbolTableCommand fromReader(Reader reader, int cmd, int cmdsize) {
final symoff = _readMachOUint32(reader);
final nsyms = _readMachOUint32(reader);
final stroff = _readMachOUint32(reader);
final strsize = _readMachOUint32(reader);
return SymbolTableCommand._(cmd, cmdsize, symoff, nsyms, stroff, strsize);
}
SymbolTable load(Reader reader) {
final stringTable =
StringTable.fromReader(reader.shrink(_stroff, _strsize));
return SymbolTable.fromReader(reader.shrink(_symoff), _nsyms, stringTable);
}
@override
void writeToStringBuffer(StringBuffer buffer) {
buffer
..write('Symbol table with ')
..write(_nsyms)
..write(' symbols of size ')
..writeln(cmdsize);
}
}
class UuidCommand extends LoadCommand {
Uint8List uuid;
static const kUuidSize = 16;
UuidCommand._(super.cmd, super.cmdsize, this.uuid) : super._();
static UuidCommand fromReader(Reader reader, int cmd, int cmdsize) {
final uuid = Uint8List.sublistView(
reader.bytes, reader.offset, reader.offset + kUuidSize);
return UuidCommand._(cmd, cmdsize, uuid);
}
String get uuidString => uuid.map((i) => paddedHex(i, 1)).join();
@override
void writeToStringBuffer(StringBuffer buffer) {
buffer
..write('UUID: ')
..write(uuidString);
}
}
class DylibInfo {
final String name;
final int timestamp;
final int currentVersion;
final int compatibilityVersion;
const DylibInfo._(this.name, this.timestamp, this.currentVersion,
this.compatibilityVersion);
static DylibInfo fromReader(Reader reader, int cmdsize) {
final start = reader.offset - 8; // cmd + cmdsize
final offset = _readMachOUint32(reader);
final timestamp = _readMachOUint32(reader);
final currentVersion = _readMachOUint32(reader);
final compatibilityVersion = _readMachOUint32(reader);
reader.seek(start + offset, absolute: true);
final name = reader.readNullTerminatedString(maxSize: cmdsize - offset);
return DylibInfo._(name, timestamp, currentVersion, compatibilityVersion);
}
void writeToStringBuffer(StringBuffer buffer) {
buffer
..write(' Name: ')
..writeln(name)
..write(' Timestamp: ')
..writeln(timestamp)
..write(' Current version: ')
..writeln(currentVersion)
..write(' Compatibility version: ')
..writeln(compatibilityVersion);
}
@override
String toString() {
final buffer = StringBuffer();
writeToStringBuffer(buffer);
return buffer.toString();
}
}
class DylibCommand extends LoadCommand {
final DylibInfo info;
DylibCommand._(super.cmd, super.cmdsize, this.info) : super._();
static DylibCommand fromReader(Reader reader, int cmd, int cmdsize) =>
DylibCommand._(cmd, cmdsize, DylibInfo.fromReader(reader, cmdsize));
@override
void writeToStringBuffer(StringBuffer buffer) {
if (cmd == LoadCommand.LC_ID_DYLIB) {
buffer.writeln('LC_ID_DYLIB:');
info.writeToStringBuffer(buffer);
} else {
throw StateError("Unexpected command code $cmd");
}
}
}
class RunPathCommand extends LoadCommand {
String path;
RunPathCommand._(super.cmd, super.cmdsize, this.path) : super._();
static RunPathCommand fromReader(Reader reader, int cmd, int cmdsize) {
final start = reader.offset - 8; // cmd + cmdsize
final offset = _readMachOUint32(reader);
reader.seek(start + offset, absolute: true);
final path = reader.readNullTerminatedString(maxSize: cmdsize - offset);
return RunPathCommand._(cmd, cmdsize, path);
}
@override
void writeToStringBuffer(StringBuffer buffer) {
buffer
..write('Run path: ')
..write(path);
}
}
class Version {
final int x;
final int y;
final int z;
const Version(this.x, this.y, this.z);
static Version fromInt(int v) {
final x = (v & 0xffffffff) >> 16;
final y = (v & 0xffff) >> 8;
final z = v & 0xff;
return Version(x, y, z);
}
@override
int get hashCode => Object.hash(x, y, z);
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Version && x == other.x && y == other.y && z == other.z;
}
@override
String toString() => '$x.$y.$z';
}
enum Platform {
PLATFORM_UNKNOWN(0, 'Unknown'),
PLATFORM_ANY(0xFFFFFFFF, 'any'),
PLATFORM_MACOS(0x1, 'MacOS'),
PLATFORM_IOS(0x2, 'iOS'),
PLATFORM_TVOS(0x3, 'TVOS'),
PLATFORM_WATCHOS(0x4, 'WatchOS'),
PLATFORM_IOSSIMULATOR(0x7, 'iOS Simulator'),
PLATFORM_TVOSSIMULATOR(0x8, 'TVOS Simulator'),
PLATFORM_WATCHOSSIMULATOR(0x9, 'WatchOS Simulator');
final int id;
final String description;
const Platform(this.id, this.description);
static Platform? fromId(int id) {
for (final platform in values) {
if (platform.id == id) {
return platform;
}
}
return null;
}
@override
String toString() => description;
}
enum BuildTool {
TOOL_CLANG(1, 'clang'),
TOOL_SWIFT(2, 'Swift'),
TOOL_LD(3, 'ld'),
TOOL_LLD(4, 'lld');
final int id;
final String description;
const BuildTool(this.id, this.description);
static BuildTool? fromId(int id) {
for (final tool in values) {
if (tool.id == id) {
return tool;
}
}
return null;
}
@override
String toString() => description;
}
class BuildToolVersion {
final int _toolId;
final BuildTool? tool;
final Version version;
const BuildToolVersion._(this._toolId, this.tool, this.version);
factory BuildToolVersion.fromReader(Reader reader) {
final id = _readMachOUint32(reader);
final tool = BuildTool.fromId(id);
final version = Version.fromInt(_readMachOUint32(reader));
return BuildToolVersion._(id, tool, version);
}
void writeToStringBuffer(StringBuffer buffer) {
buffer
..write(' - ')
..write(tool ?? 'Unknown (0x${paddedHex(_toolId, 4)})')
..write(' (')
..write(version)
..writeln(')');
}
@override
String toString() {
final buffer = StringBuffer();
writeToStringBuffer(buffer);
return buffer.toString();
}
}
class BuildVersionCommand extends LoadCommand {
final int _platformId;
final Platform? platform;
final Version minOS;
final Version sdk;
final List<BuildToolVersion> toolVersions;
BuildVersionCommand._(super.cmd, super.cmdsize, this._platformId,
this.platform, this.minOS, this.sdk, this.toolVersions)
: super._();
static BuildVersionCommand fromReader(Reader reader, int cmd, int cmdsize) {
final platformId = _readMachOUint32(reader);
final platform = Platform.fromId(platformId);
final minOS = Version.fromInt(_readMachOUint32(reader));
final sdk = Version.fromInt(_readMachOUint32(reader));
final toolCount = _readMachOUint32(reader);
final toolVersions = <BuildToolVersion>[];
for (int i = 0; i < toolCount; i++) {
toolVersions.add(BuildToolVersion.fromReader(reader));
}
return BuildVersionCommand._(
cmd, cmdsize, platformId, platform, minOS, sdk, toolVersions);
}
@override
void writeToStringBuffer(StringBuffer buffer) {
buffer
..writeln('Build version:')
..write(' Platform: ')
..writeln(platform ?? 'Unknown (0x${paddedHex(_platformId, 4)})')
..write(' Minimum OS: ')
..writeln(minOS)
..write(' Minimum SDK: ')
..writeln(sdk);
if (toolVersions.isNotEmpty) {
buffer.writeln(' Tools:');
for (final v in toolVersions) {
v.writeToStringBuffer(buffer);
}
}
}
}
class MachOHeader {
final int magic;
final int cputype;
final int cpusubtype;
final int filetype;
final int ncmds;
final int sizeofcmds;
final int flags;
final int? reserved;
final int size;
MachOHeader._(this.magic, this.cputype, this.cpusubtype, this.filetype,
this.ncmds, this.sizeofcmds, this.flags, this.reserved, this.size);
static const _MH_MAGIC = 0xfeedface;
static const _MH_CIGAM = 0xcefaedfe;
static const _MH_MAGIC_64 = 0xfeedfacf;
static const _MH_CIGAM_64 = 0xcffaedfe;
static 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);
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);
final ncmds = _readMachOUint32(reader);
final sizeofcmds = _readMachOUint32(reader);
final flags = _readMachOUint32(reader);
final reserved = reader.wordSize == 8 ? _readMachOUint32(reader) : null;
final size = reader.offset - start;
return MachOHeader._(magic, cputype, cpusubtype, filetype, ncmds,
sizeofcmds, flags, reserved, size);
}
int get wordSize => _wordSize(magic)!;
Endian get endian => _endian(magic)!;
bool get isDSYM => filetype == _MH_DSYM;
void writeToStringBuffer(StringBuffer buffer) {
buffer
..write('Magic: 0x')
..writeln(paddedHex(magic, 4));
buffer
..write('Cpu Type: 0x')
..writeln(paddedHex(cputype, 4));
buffer
..write('Cpu Subtype: 0x')
..writeln(paddedHex(cpusubtype, 4));
buffer
..write('Filetype: 0x')
..writeln(paddedHex(filetype, 4));
buffer
..write('Number of commands: ')
..writeln(ncmds);
buffer
..write('Size of commands: ')
..writeln(sizeofcmds);
buffer
..write('Flags: 0x')
..writeln(paddedHex(flags, 4));
if (reserved != null) {
buffer
..write('Reserved: 0x')
..writeln(paddedHex(reserved!, 4));
}
}
@override
String toString() {
final buffer = StringBuffer();
writeToStringBuffer(buffer);
return buffer.toString();
}
}
class MachO extends DwarfContainer {
final MachOHeader _header;
final List<LoadCommand> _commands;
final SymbolTable _symbolTable;
final SegmentCommand? _dwarfSegment;
final StringTable? _debugStringTable;
final StringTable? _debugLineStringTable;
MachO._(this._header, this._commands, this._symbolTable, this._dwarfSegment,
this._debugStringTable, this._debugLineStringTable);
static MachO? fromReader(Reader machOReader) {
// MachO files contain absolute offsets from the start of the file, so
// make sure we have a reader that a) makes no assumptions about the
// endianness or word size, since we'll read those in the header and b)
// has an internal offset of 0 so absolute offsets can be used directly.
final reader = machOReader.shrink(machOReader.offset);
final header = MachOHeader.fromReader(reader);
if (header == null) return null;
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);
final dwarfSegment = commands
.whereType<SegmentCommand?>()
.firstWhere((sc) => sc!.segname == '__DWARF', orElse: () => null);
StringTable? debugStringTable;
StringTable? debugLineStringTable;
if (dwarfSegment != null) {
final debugStringTableSection = dwarfSegment.sections['__debug_str'];
if (debugStringTableSection != null) {
debugStringTable =
StringTable.fromReader(debugStringTableSection.shrink(reader));
}
final debugLineStringTableSection =
dwarfSegment.sections['__debug_line_str'];
if (debugLineStringTableSection != null) {
debugLineStringTable =
StringTable.fromReader(debugLineStringTableSection.shrink(reader));
}
}
// Set the wordSize and endian of the original reader before returning.
machOReader.wordSize = reader.wordSize;
machOReader.endian = reader.endian;
return MachO._(header, commands, symbolTable, dwarfSegment,
debugStringTable, debugLineStringTable);
}
static String handleDSYM(String fileName) {
if (!fileName.endsWith('.dSYM')) {
return fileName;
}
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)));
bool get isDSYM => _header.isDSYM;
bool get hasDwarf => _dwarfSegment != null;
Reader applyWordSizeAndEndian(Reader reader) =>
Reader.fromTypedData(reader.bdata,
wordSize: _header.wordSize, endian: _header.endian);
Iterable<LoadCommand> get commands => _commands;
Iterable<T> commandsWhereType<T extends LoadCommand>() =>
_commands.whereType<T>();
@override
String? get architecture => CpuType.fromCode(_header.cputype)?.dartName;
@override
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 =>
_commands.whereType<UuidCommand>().firstOrNull?.uuidString;
@override
DwarfContainerStringTable? get debugStringTable => _debugStringTable;
@override
DwarfContainerStringTable? get debugLineStringTable => _debugLineStringTable;
@override
Symbol? staticSymbolAt(int address) {
Symbol? bestSym;
for (final symbol in _symbolTable.values) {
if (symbol.value > address) continue;
// Pick the symbol with a value closest to the given address.
if (bestSym == null || (bestSym.value < symbol.value)) {
bestSym = symbol;
}
}
return bestSym;
}
@override
Iterable<Symbol> get staticSymbols => _symbolTable.values;
@override
void writeToStringBuffer(StringBuffer buffer) {
buffer
..writeln('----------------------------------------')
..writeln(' Header')
..writeln('----------------------------------------')
..writeln('');
_header.writeToStringBuffer(buffer);
buffer
..writeln('')
..writeln('')
..writeln('----------------------------------------')
..writeln(' Load commands')
..writeln('----------------------------------------')
..writeln('');
for (final command in _commands) {
command.writeToStringBuffer(buffer);
buffer.writeln('');
}
}
}
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() {
final buffer = StringBuffer();
writeToStringBuffer(buffer);
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 (!arches.containsKey(cpuType)) {
arches[cpuType] = arch;
} else 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 (!contents[arches[cpuType]!]!.hasDwarf) {
// If the old section didn't have DWARF information but the new one
// does, take it instead.
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(' Selected 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;
}