blob: 0119b54f4248fdd623ae5d8774882aa498ca7c4f [file] [log] [blame]
// 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.
// This file is a reimplementation of the header file mach-o/loader.h, which is
// part of the Apple system headers. All comments, which detail the format of
// Mach-O files, have been reproduced from the original header.
import 'dart:io';
import 'dart:typed_data';
import 'src/macho_utils.dart';
class OffsetsAdjuster {
/// A sorted list of non-negative offsets signifying the start of
/// non-overlapping adjustment ranges, so either up to (but not including) the
/// next listed offset or the end of the file.
final offsets = <int>[0];
/// The map of offsets to the adjustment needed for their adjustment range.
final adjustments = <int, int>{0: 0};
/// Adds a new adjustment at the given offset. If there is not an existing
/// adjustment range starting at that offset, the adjustment range containing
/// that offset is split, and all adjustment ranges that start at that offset
/// or at later offsets have their adjustment increased accordingly.
void add(int offset, int adjustment) {
assert(offset >= 0);
assert(adjustment >= 0);
var startingIndex = offsets.lastIndexWhere((o) => o <= offset);
// If the offset didn't already have an adjustment range, split the previous
// range
if (offsets[startingIndex] != offset) {
final newIndex = startingIndex + 1;
// Add a new adjustment range that starts at the offset and has an
// initial adjustment value that's the same as the previous range the
// offset was in.
offsets.insert(newIndex, offset);
adjustments[offset] = adjustments[offsets[startingIndex]]!;
// Start making adjustments at the new range.
startingIndex = newIndex;
}
// Adjust the adjustments for all ranges that start at the given offset
// or later.
for (var i = startingIndex; i < offsets.length; i++) {
final off = offsets[i];
adjustments[off] = adjustments[offset]! + adjustment;
}
}
/// Adjusts the given offset according to the adjustment ranges collected.
int adjust(int offset) {
assert(offset >= 0);
// We should always get a valid index since we add an entry for the start
// of the file.
final rangeStart = offsets.lastWhere((o) => o <= offset);
return offset + adjustments[rangeStart]!;
}
}
enum CpuType {
i386(_typeX86, 'I386'),
x64(_typeX86 | _architectureABI64, 'X64'),
arm(_typeArm, 'ARM'),
arm64(_typeArm | _architectureABI64, 'ARM64');
final int _code;
final String _headerName;
const CpuType(this._code, this._headerName);
/// Marks an architecture as using a 64-bit ABI.
static const _architectureABI64 = 0x0100000;
/// The base type of all X86 architecture variants.
static const _typeX86 = 0x7;
/// The base type of all ARM architecture variants.
static const _typeArm = 0x12;
static const _prefix = 'CPU_TYPE';
static CpuType? fromCode(int code) {
for (final value in values) {
if (value._code == code) {
return value;
}
}
return null;
}
/// Returns whether the architecture represented by a given `cputype` value
/// from the MachO header uses 64-bit pointers.
static bool cpuTypeUses64BitPointers(int code) =>
code & _architectureABI64 == _architectureABI64;
/// Returns whether the architecture represented by this uses 64-bit pointers.
bool get uses64BitPointers => cpuTypeUses64BitPointers(_code);
@override
String toString() => '${_prefix}_$_headerName';
}
enum VirtualMemoryProtection {
none(0x00, 'NONE'),
read(0x01, 'READ'),
write(0x02, 'WRITE'),
execute(0x04, 'EXECUTE');
final int code;
final String _headerName;
const VirtualMemoryProtection(this.code, this._headerName);
static const _prefix = 'VM_PROT';
@override
String toString() => '${_prefix}_$_headerName';
}
enum LoadCommandType {
// Note that entries marked 'obsolete' in the C headers have not been
// translated into enum values below.
//
// We also do not translate entries that we need not understand (even
// if LC_REQ_DYLD is set).
/// A 32-bit segment of the file that is mapped into memory at runtime.
segment(0x1, 'SEGMENT'),
/// The static symbol table.
symbolTable(0x2, 'SYMTAB'),
/// The dynamic symbol table.
dynamicSymbolTable(0xb, 'DYSYMTAB'),
/// A 64-bit segment of the file that is mapped into memory at runtime.
segment64(0x19, 'SEGMENT_64'),
/// A code signature.
codeSignature(0x1d, 'CODE_SIGNATURE'),
/// Information to split segments.
segmentSplitInfo(0x1e, 'SEGMENT_SPLIT_INFO'),
/// 32-bit encrypted segment information.
encryptionInfo(0x21, 'ENCRYPTION_INFO'),
/// Compressed dynamically linked shared library information.
///
/// Note that the C headers include a separate name for the version that has
/// LD_REQ_DYLD set: LC_DYLD_INFO_ONLY. We do not include this name.
dynamicLibraryInfo(0x22, 'DYLD_INFO'),
/// Compressed table of function start addresses.
functionStarts(0x26, 'FUNCTION_STARTS'),
/// Main entry point (replacement for LC_UNIXTHREAD).
main(0x28 | _reqDyld, 'MAIN'),
/// Table of non-instructions in the text segment.
dataInCode(0x29, 'DATA_IN_CODE'),
/// Code signing DRs copied from dynamically linked shared libraries.
dynamicLibraryCodeSigningDRs(0x2b, 'DYLIB_CODE_SIGN_DRS'),
/// 64-bit encrypted segment information.
encryptionInfo64(0x2c, 'ENCRYPTION_INFO_64'),
/// Optimization hints in object files.
linkerOptimizationHint(0x2e, 'LINKER_OPTIMIZATION_HINT'),
/// Arbitrary data included within a MachO file.
note(0x31, 'NOTE'),
/// A trie of dynamically linked shared library exports.
dynamicLibraryExportsTrie(0x33 | _reqDyld, 'DYLD_EXPORTS_TRIE'),
/// Chained fixups for dynamically linked shared libraries.
dynamicLibraryChainedFixups(0x34 | _reqDyld, 'DYLD_CHAINED_FIXUPS'),
/// A fileset entry.
fileSetEntry(0x35 | _reqDyld, 'FILESET_ENTRY');
/// After MacOS X 10.1 when a new load command is added that is required to be
/// understood by the dynamic linker for the image to execute properly, this
/// bit will be or'ed into the load command constant. If the dynamic
/// linker sees such a load command it does not understand will issue a
/// "unknown load command required for execution" error and refuse to use the
/// image. Other load commands without this bit that are not understood will
/// simply be ignored.
///
/// `LC_REQ_DYLD` in the C headers.
static const _reqDyld = 0x80000000;
final int code;
final String _headerName;
const LoadCommandType(this.code, this._headerName);
static const _prefix = 'LC';
static LoadCommandType? fromCode(int code) {
for (final value in values) {
if ((value.code & _reqDyld) != 0) {
/// LC_REQ_DYLD is set on the enum value, so it can only match exactly.
if (value.code == code) {
return value;
}
} else {
/// The LC_REQ_DYLD bit should be ignored for matching purposes.
final stripped = code & ~_reqDyld;
if (value.code == stripped) {
return value;
}
}
}
return null;
}
@override
String toString() => '${_prefix}_$_headerName';
}
/// A load command describes how to load and use various parts of the file
/// contents (e.g., where to find the TEXT and DATA sections).
abstract class MachOLoadCommand {
/// The encoding of this load command's type as a 32-bit value.
///
/// `uint32_t cmd` in the C headers.
final int code;
/// The total size of the load command in bytes, including the type and size
/// fields.
///
/// `uint32_t cmdsize` in the C headers.
final int size;
MachOLoadCommand(this.code, this.size);
static MachOLoadCommand fromStream(MachOReader stream) {
final code = stream.readUint32();
final size = stream.readUint32();
final type = LoadCommandType.fromCode(code);
if (type == null) {
// Not a MachO section that needs to be adjusted for offset changes, so
// just read the bytes without interpretation.
final contents = stream.readBytes(size - 2 * 4);
return MachOGenericLoadCommand(code, size, contents);
}
return switch (type) {
LoadCommandType.segment ||
LoadCommandType.segment64 =>
MachOSegmentCommand.fromStream(code, size, stream),
LoadCommandType.dynamicLibraryInfo =>
MachODyldInfoCommand.fromStream(code, size, stream),
LoadCommandType.symbolTable =>
MachOSymtabCommand.fromStream(code, size, stream),
LoadCommandType.dynamicSymbolTable =>
MachODysymtabCommand.fromStream(code, size, stream),
LoadCommandType.codeSignature ||
LoadCommandType.segmentSplitInfo ||
LoadCommandType.functionStarts ||
LoadCommandType.dataInCode ||
LoadCommandType.dynamicLibraryCodeSigningDRs ||
LoadCommandType.linkerOptimizationHint ||
LoadCommandType.dynamicLibraryExportsTrie ||
LoadCommandType.dynamicLibraryChainedFixups =>
MachOLinkeditDataCommand.fromStream(code, size, stream),
LoadCommandType.encryptionInfo ||
LoadCommandType.encryptionInfo64 =>
MachOEncryptionInfoCommand.fromStream(code, size, stream),
LoadCommandType.main =>
MachOEntryPointCommand.fromStream(code, size, stream),
LoadCommandType.note => MachONoteCommand.fromStream(code, size, stream),
LoadCommandType.fileSetEntry =>
MachOFileSetEntryCommand.fromStream(code, size, stream),
};
}
/// The type for this load command. Returns null for MachOGenericLoadCommand.
LoadCommandType? get type => LoadCommandType.fromCode(code);
/// Whether or not the dynamic linker is required to understand this load
/// command.
bool get mustBeUnderstood => (code & LoadCommandType._reqDyld) != 0;
/// Returns a version of the load command with any file offsets appropriately
/// adjusted as needed.
MachOLoadCommand adjust(OffsetsAdjuster adjuster);
void writeSync(MachOWriter stream) {
stream
..writeUint32(code)
..writeUint32(size);
writeContentsSync(stream);
}
/// Subclasses need to implement this serializer, which should NOT
/// attempt to serialize the cmd and the cmdsize to the stream. That
/// logic is handled by the parent class automatically.
void writeContentsSync(MachOWriter stream);
}
/// A MachO load command that can be copied wholesale without any adjustments.
class MachOGenericLoadCommand extends MachOLoadCommand {
final Uint8List contents;
MachOGenericLoadCommand(super.cmd, super.cmdsize, this.contents);
@override
MachOLoadCommand adjust(OffsetsAdjuster adjuster) => this;
@override
void writeContentsSync(MachOWriter stream) {
stream.writeBytes(contents);
}
}
/// The header of a MachO file. There are 32-bit and 64-bit variations, but
/// the only difference is that 64-bit headers have a reserved field for
/// padding. Thus, we merge the two into a single class.
class MachOHeader {
/// The magic number identifier for the MachO file. Denotes whether the file
/// is 32-bit or 64-bit, and whether its endianness matches the host.
///
/// `uint32_t magic` in the C headers.
final int magic;
/// The CPU type assumed by this MachO file.
///
/// `cpu_type_t cputype` in the C headers, where `cpu_type_t` is
/// `integer_t` which is `int`.
final int cpu;
/// The machine type assumed by this MachO file.
///
/// `cpu_subtype_t cpusubtype` in the C headers, where `cpu_subtype_t` is
/// `integer_t` which is `int`.
final int machine;
/// The type of the MachO file.
///
/// `uint32_t filetype` in the C headers.
final int type;
/// The number of load commands in this MachO file.
///
/// `uint32_t ncmds` in the C headers.
final int loadCommandsCount;
/// The total size of the load commands.
///
/// `uint32_t sizeofcmds` in the C headers.
final int loadCommandsSize;
/// A bit array of flags in the MachO file.
///
/// `uint32_t flags` in the C headers.
final int flags;
/// Reserved space in 64-bit headers, null in 32-bit headers.
///
/// `uint32_t reserved` in the C headers.
final int? reserved;
MachOHeader(this.magic, this.cpu, this.machine, this.type,
this.loadCommandsCount, this.loadCommandsSize, this.flags, this.reserved);
/// Constant for the magic field of a 32-bit mach_header with host endianness.
static const int _magic32 = 0xfeedface;
/// Constant for the magic field of a 32-bit mach_header with swapped
/// endianness.
static const int _cigam32 = 0xcefaedfe;
/// Constant for the magic field of a 64-bit mach_header with host endianness.
static const int _magic64 = 0xfeedfacf;
/// Constant for the magic field of a 64-bit mach_header with swapped
/// endianness.
static const int _cigam64 = 0xcffaedfe;
static MachOHeader? fromStream(RandomAccessFile original) {
// First, read the magic value using host endianness, so we can determine
// whether the file uses host endianness or swapped endianness.
final magic = MachOReader(original, Endian.host).readUint32();
if (!validMagic(magic)) return null;
final is64Bit = magic == _magic64 || magic == _cigam64;
final endian = magicEndian(magic);
// Now recreate the MachOReader with the right endianness.
final stream = MachOReader(original, endian);
final cpu = stream.readInt32();
final machine = stream.readInt32();
final type = stream.readUint32();
final loadCommandsCount = stream.readUint32();
final loadCommandsSize = stream.readUint32();
final flags = stream.readUint32();
final reserved = is64Bit ? stream.readUint32() : null;
return MachOHeader(magic, cpu, machine, type, loadCommandsCount,
loadCommandsSize, flags, reserved);
}
static bool validMagic(int magic) =>
magic == _magic32 ||
magic == _magic64 ||
magic == _cigam32 ||
magic == _cigam64;
static Endian magicEndian(int magic) =>
(magic == _magic64 || magic == _magic32)
? Endian.host
: Endian.host == Endian.big
? Endian.little
: Endian.big;
// A faster check than rechecking the magic number.
bool get is64Bit => reserved != null;
Endian get endian => magicEndian(magic);
// The size of the header when written to disk. Seven 32-bit fields, plus
// an extra 32-bit field for 64-bit headers to align it to word size.
int get size => 7 * 4 + (is64Bit ? 4 : 0);
void writeSync(RandomAccessFile original) {
// Like reading, first we write the magic value using host endianness,
// and then write the rest of the value using the detected endianness.
MachOWriter(original, Endian.host).writeUint32(magic);
final stream = MachOWriter(original, endian);
stream
..writeInt32(cpu)
..writeInt32(machine)
..writeUint32(type)
..writeUint32(loadCommandsCount)
..writeUint32(loadCommandsSize)
..writeUint32(flags);
if (is64Bit) {
stream.writeUint32(reserved!);
}
}
}
/// A segment load command indicates that a part of this file is to be mapped
/// into a task's address space. If the segment has sections, the section
/// structures follow directly after the segment load command and their size
/// is reflected in the [size] of the load command.
///
/// In the C headers, there are two different structs for 32-bit and 64-bit
/// segments. Here, we combine them into one, with the [type] of the load
/// command determining whether we read and write 32-bit or 64-bit values for
/// particular fields.
class MachOSegmentCommand extends MachOLoadCommand {
/// The name of the segment.
///
/// `char segname[16]` in the C headers.
final String name;
/// The memory address at which this segment should be placed.
///
/// `uint64_t vmaddr` for 64-bit segment commands and `uint32_t vmaddr`
/// for 32-bit segment commands in the C headers.
final int memoryAddress;
/// The memory size for this segment.
///
/// `uint64_t vmsize` for 64-bit segment commands and `uint32_t vmsize`
/// for 32-bit segment commands in the C headers.
final int memorySize;
/// The file offset for this segment.
///
/// `uint64_t fileoff` for 64-bit segment commands and `uint32_t fileoff`
/// for 32-bit segment commands in the C headers.
final int fileOffset;
/// The file size of this segment.
///
/// `uint64_t filesize` for 64-bit segment commands and `uint32_t filesize`
/// for 32-bit segment commands in the C headers.
final int fileSize;
/// The maximum VM protection allowed for this segment.
///
/// `vm_prot_t maxprot` in the C headers, where `vm_prot_t` is `int`.
final int maxProtection;
/// The initial VM protection used for this segment.
///
/// `vm_prot_t initprot` in the C headers, where `vm_prot_t` is `int`.
final int initialProtection;
/// The flags set for this segment.
///
/// `uint32_t flags` in the C headers.
final int flags;
/// The list of sections structures in the segment.
///
/// Note that we do not keep a separate field for the number of sections
/// (`uint32_t nsects` in the C headers), but instead just use the length
/// of this list.
final List<MachOSection> sections;
MachOSegmentCommand(
super.code,
super.size,
this.name,
this.memoryAddress,
this.memorySize,
this.fileOffset,
this.fileSize,
this.maxProtection,
this.initialProtection,
this.flags,
this.sections,
);
static const _nameLength = 16;
static MachOSegmentCommand fromStream(
int code, int size, MachOReader stream) {
final is64Bit = LoadCommandType.fromCode(code) == LoadCommandType.segment64;
final wordSize = is64Bit ? 8 : 4;
final name = stream.readFixedLengthNullTerminatedString(_nameLength);
final memoryAddress = stream.readUword(wordSize);
final memorySize = stream.readUword(wordSize);
final fileOffset = stream.readUword(wordSize);
final fileSize = stream.readUword(wordSize);
final maxProtection = stream.readInt32();
final initialProtection = stream.readInt32();
final sectionCount = stream.readUint32();
final flags = stream.readUint32();
final sections = List<MachOSection>.generate(
sectionCount, (_) => MachOSection.fromStream(stream, is64Bit),
growable: false);
return MachOSegmentCommand(
code,
size,
name,
memoryAddress,
memorySize,
fileOffset,
fileSize,
maxProtection,
initialProtection,
flags,
sections);
}
int get _wordSize =>
LoadCommandType.fromCode(code) == LoadCommandType.segment64 ? 8 : 4;
@override
MachOSegmentCommand adjust(OffsetsAdjuster adjuster) => MachOSegmentCommand(
code,
size,
name,
memoryAddress,
memorySize,
adjuster.adjust(fileOffset),
fileSize,
maxProtection,
initialProtection,
flags,
sections.map((s) => s.adjust(adjuster)).toList());
@override
void writeContentsSync(MachOWriter stream) {
stream.writeFixedLengthNullTerminatedString(name, _nameLength);
stream.writeUword(memoryAddress, _wordSize);
stream.writeUword(memorySize, _wordSize);
stream.writeUword(fileOffset, _wordSize);
stream.writeUword(fileSize, _wordSize);
stream.writeInt32(maxProtection);
stream.writeInt32(initialProtection);
stream.writeUint32(sections.length);
stream.writeUint32(flags);
for (final section in sections) {
section.writeContentsSync(stream);
}
}
}
enum SectionType {
/// A standard section that has no special meaning.
regular(0x0, 'REGULAR'),
/// This section has no file contents, but instead any virtual memory for
/// this section is zero-filled at startup. Must be smaller than 4 gigabytes,
/// but can be mixed with other types of sections in a given segment.
zeroFill(0x1, 'ZEROFILL'),
/// This section has no file contents, but instead any virtual memory for
/// this section is zero-filled at startup. Can be larger than 4 gigabytes,
/// but can only be in a segment with the same kind of section.
gigabyteZeroFill(0xc, 'GB_ZEROFILL'),
/// This section has no file contents, but instead any virtual memory for
/// this section is zero-filled at startup. Used for BSS sections.
threadLocalZeroFill(0x12, 'THREAD_LOCAL_ZEROFILL');
final int _code;
final String _headerName;
const SectionType(this._code, this._headerName);
static SectionType? fromFlags(int flags) {
final code = flags & _typeMask;
for (final value in values) {
if (value._code == code) {
return value;
}
}
return null;
}
static const _prefix = 'S';
static const _typeSize = 8;
static const _typeMask = (1 << _typeSize) - 1;
@override
String toString() => '${_prefix}_$_headerName';
}
/// A section describes a specific portion of a segment. The specifics of
/// sections are mostly unimportant for our work here, except that we need
/// to recognize S_ZEROFILL/S_GB_ZEROFILL sections so they can be ignored for
/// file offset purposes.
///
/// In the C headers, sections are represented by two structs, one for 32-bit
/// headers and one for 64-bit headers. Other than whether specific fields
/// are 32-bit or 64-bit, the only other difference is that the 64-bit header
/// contains one more 32-bit reserved field.
class MachOSection {
/// Name of this section.
final String name;
/// Name of the containing segment.
final String segmentName;
/// Memory address of this section.
final int memoryAddress;
/// The size of this section in memory (and in the file contents if not
/// a zero fill section).
final int size;
/// File offset of the contents of this section.
final int fileOffset;
final int alignment;
/// File offset of the relocation entries.
final int relocationsFileOffset;
/// Nuber of relocation entries.
final int relocationsCount;
/// Flags, which encode both the section type and any section attributes.
final int flags;
/// Reserved (for offset or index).
///
/// `uint32_t reserved1` in the C headers.
final int reserved1;
// Reserved (for count or sizeof).
///
/// `uint32_t reserved2` in the C headers.
final int reserved2;
/// Reserved in 64-bit sections for padding purposes. Null in 32-bit sections.
///
/// `uint32_t reserved3` in the C headers.
final int? reserved3;
MachOSection(
this.name,
this.segmentName,
this.memoryAddress,
this.size,
this.fileOffset,
this.alignment,
this.relocationsFileOffset,
this.relocationsCount,
this.flags,
this.reserved1,
this.reserved2,
this.reserved3,
);
static MachOSection fromStream(MachOReader stream, bool is64Bit) {
final wordSize = is64Bit ? 8 : 4;
final name = stream.readFixedLengthNullTerminatedString(_nameLength);
final segmentName = stream
.readFixedLengthNullTerminatedString(MachOSegmentCommand._nameLength);
final memoryAddress = stream.readUword(wordSize);
final size = stream.readUword(wordSize);
final fileOffset = stream.readUint32();
final alignment = stream.readUint32();
final relocationsFileOffset = stream.readUint32();
final relocationsCount = stream.readUint32();
final flags = stream.readUint32();
final reserved1 = stream.readUint32();
final reserved2 = stream.readUint32();
int? reserved3;
if (is64Bit) {
reserved3 = stream.readUint32();
}
return MachOSection(
name,
segmentName,
memoryAddress,
size,
fileOffset,
alignment,
relocationsFileOffset,
relocationsCount,
flags,
reserved1,
reserved2,
reserved3);
}
static const _nameLength = 16;
static const _attributesMask = 0xffffffff & ~SectionType._typeMask;
bool get is64Bit => reserved3 != null;
SectionType? get type => SectionType.fromFlags(flags);
int get attributes => flags & _attributesMask;
bool get isZeroFill {
final cachedType = type; // Don't recalculate for each comparison.
return cachedType == SectionType.zeroFill ||
cachedType == SectionType.gigabyteZeroFill ||
cachedType == SectionType.threadLocalZeroFill;
}
MachOSection adjust(OffsetsAdjuster adjuster) => MachOSection(
name,
segmentName,
memoryAddress,
size,
adjuster.adjust(fileOffset),
alignment,
adjuster.adjust(relocationsFileOffset),
relocationsCount,
flags,
reserved1,
reserved2,
reserved3);
void writeContentsSync(MachOWriter stream) {
final wordSize = is64Bit ? 8 : 4;
stream.writeFixedLengthNullTerminatedString(name, _nameLength);
stream.writeFixedLengthNullTerminatedString(
segmentName, MachOSegmentCommand._nameLength);
stream.writeUword(memoryAddress, wordSize);
stream.writeUword(size, wordSize);
stream.writeUint32(fileOffset);
stream.writeUint32(alignment);
stream.writeUint32(relocationsFileOffset);
stream.writeUint32(relocationsCount);
stream.writeUint32(flags);
stream.writeUint32(reserved1);
stream.writeUint32(reserved2);
if (is64Bit) {
stream.writeUint32(reserved3!);
}
}
}
/// A load command that describes a static symbol table.
class MachOSymtabCommand extends MachOLoadCommand {
/// The file offset of the symbol table.
///
/// `uint32_t symoff` in the C headers.
final int fileOffset;
/// The number of symbol table entries.
///
/// `uint32_t symoff` in the C headers.
final int symbolsCount;
/// The file offset of the string table.
///
/// `uint32_t stroff` in the C headers.
final int stringTableFileOffset;
/// The file size of the string table.
///
/// `uint32_t strsize` in the C headers.
final int stringTableSize;
MachOSymtabCommand(
super.code,
super.size,
this.fileOffset,
this.symbolsCount,
this.stringTableFileOffset,
this.stringTableSize,
);
static MachOSymtabCommand fromStream(int code, int size, MachOReader stream) {
final fileOffset = stream.readUint32();
final symbolsCount = stream.readUint32();
final stringTableFileOffset = stream.readUint32();
final stringTableSize = stream.readUint32();
return MachOSymtabCommand(code, size, fileOffset, symbolsCount,
stringTableFileOffset, stringTableSize);
}
@override
MachOSymtabCommand adjust(OffsetsAdjuster adjuster) => MachOSymtabCommand(
code,
size,
adjuster.adjust(fileOffset),
symbolsCount,
adjuster.adjust(stringTableFileOffset),
stringTableSize);
@override
void writeContentsSync(MachOWriter stream) {
stream.writeUint32(fileOffset);
stream.writeUint32(symbolsCount);
stream.writeUint32(stringTableFileOffset);
stream.writeUint32(stringTableSize);
}
}
/// A load command representing the dynamic symbol table.
class MachODysymtabCommand extends MachOLoadCommand {
/// The index of the local symbols.
///
/// `uint32_t ilocalsym` in the C headers.
final int localSymbolsIndex;
/// The number of local symbols.
///
/// `uint32_t nlocalsym` in the C headers.
final int localSymbolsCount;
/// The index of the externally defined symbols.
///
/// `uint32_t iextdefsym` in the C headers.
final int externalSymbolsIndex;
/// The number of externally defined symbols.
///
/// `uint32_t nextdefsym` in the C headers.
final int externalSymbolsCount;
/// The index of the undefined symbols.
///
/// `uint32_t iundefsym` in the C headers.
final int undefinedSymbolsIndex;
/// The number of undefined symbols.
///
/// `uint32_t nundefsym` in the C headers.
final int undefinedSymbolsCount;
/// The file offset of the table of contents.
///
/// `uint32_t tocoff` in the C headers.
final int tableOfContentsFileOffset;
/// The number of entries in the table of contents.
///
/// `uint32_t ntoc` in the C headers.
final int tableOfContentsEntryCount;
/// The file offset of the module table.
///
/// `uint32_t modtaboff` in the C headers.
final int moduleTableFileOffset;
/// The number of entries in the module table.
///
/// `uint32_t nmodtab` in the C headers.
final int moduleTableEntryCount;
/// The file offset of the referenced symbol table.
///
/// `uint32_t extrefsymoff` in the C headers.
final int referencedSymbolTableFileOffset;
/// The number of entries in the referenced symbol table.
///
/// `uint32_t nextrefsyms` in the C headers.
final int referencedSymbolTableEntryCount;
/// The file offset of the indirect symbol table.
///
/// `uint32_t indirectsymoff` in the C headers.
final int indirectSymbolTableFileOffset;
/// The number of entries in the indirect symbol table.
///
/// `uint32_t nindirectsyms` in the C headers.
final int indirectSymbolTableEntryCount;
/// The file offset of external relocation entries.
///
/// `uint32_t extreloff` in the C headers.
final int externalRelocationsFileOffset;
/// The number of external relocation entries.
///
/// `uint32_t nextrel` in the C headers.
final int externalRelocationsCount;
/// The file offset of local relocation entries.
///
/// `uint32_t locreloff` in the C headers.
final int localRelocationsFileOffset;
/// The number of local relocation entries.
///
/// `uint32_t nlocrel` in the C headers.
final int localRelocationsCount;
MachODysymtabCommand(
super.code,
super.size,
this.localSymbolsIndex,
this.localSymbolsCount,
this.externalSymbolsIndex,
this.externalSymbolsCount,
this.undefinedSymbolsIndex,
this.undefinedSymbolsCount,
this.tableOfContentsFileOffset,
this.tableOfContentsEntryCount,
this.moduleTableFileOffset,
this.moduleTableEntryCount,
this.referencedSymbolTableFileOffset,
this.referencedSymbolTableEntryCount,
this.indirectSymbolTableFileOffset,
this.indirectSymbolTableEntryCount,
this.externalRelocationsFileOffset,
this.externalRelocationsCount,
this.localRelocationsFileOffset,
this.localRelocationsCount);
static MachODysymtabCommand fromStream(
int code, int size, MachOReader stream) {
final localSymbolsIndex = stream.readUint32();
final localSymbolsCount = stream.readUint32();
final externalSymbolsIndex = stream.readUint32();
final externalSymbolsCount = stream.readUint32();
final undefinedSymbolsIndex = stream.readUint32();
final undefinedSymbolsCount = stream.readUint32();
final tableOfContentsFileOffset = stream.readUint32();
final tableOfContentsEntryCount = stream.readUint32();
final moduleTableFileOffset = stream.readUint32();
final moduleTableEntryCount = stream.readUint32();
final referencedSymbolTableFileOffset = stream.readUint32();
final referencedSymbolTableEntryCount = stream.readUint32();
final indirectSymbolTableFileOffset = stream.readUint32();
final indirectSymbolTableEntryCount = stream.readUint32();
final externalRelocationsFileOffset = stream.readUint32();
final externalRelocationsCount = stream.readUint32();
final localRelocationsFileOffset = stream.readUint32();
final localRelocationsCount = stream.readUint32();
return MachODysymtabCommand(
code,
size,
localSymbolsIndex,
localSymbolsCount,
externalSymbolsIndex,
externalSymbolsCount,
undefinedSymbolsIndex,
undefinedSymbolsCount,
tableOfContentsFileOffset,
tableOfContentsEntryCount,
moduleTableFileOffset,
moduleTableEntryCount,
referencedSymbolTableFileOffset,
referencedSymbolTableEntryCount,
indirectSymbolTableFileOffset,
indirectSymbolTableEntryCount,
externalRelocationsFileOffset,
externalRelocationsCount,
localRelocationsFileOffset,
localRelocationsCount);
}
@override
MachODysymtabCommand adjust(OffsetsAdjuster adjuster) => MachODysymtabCommand(
code,
size,
localSymbolsIndex,
localSymbolsCount,
externalSymbolsIndex,
externalSymbolsCount,
undefinedSymbolsIndex,
undefinedSymbolsCount,
adjuster.adjust(tableOfContentsFileOffset),
tableOfContentsEntryCount,
adjuster.adjust(moduleTableFileOffset),
moduleTableEntryCount,
adjuster.adjust(referencedSymbolTableFileOffset),
referencedSymbolTableEntryCount,
adjuster.adjust(indirectSymbolTableFileOffset),
indirectSymbolTableEntryCount,
adjuster.adjust(externalRelocationsFileOffset),
externalRelocationsCount,
adjuster.adjust(localRelocationsFileOffset),
localRelocationsCount);
@override
void writeContentsSync(MachOWriter stream) {
stream.writeUint32(localSymbolsIndex);
stream.writeUint32(localSymbolsCount);
stream.writeUint32(externalSymbolsIndex);
stream.writeUint32(externalSymbolsCount);
stream.writeUint32(undefinedSymbolsIndex);
stream.writeUint32(undefinedSymbolsCount);
stream.writeUint32(tableOfContentsFileOffset);
stream.writeUint32(tableOfContentsEntryCount);
stream.writeUint32(moduleTableFileOffset);
stream.writeUint32(moduleTableEntryCount);
stream.writeUint32(referencedSymbolTableFileOffset);
stream.writeUint32(referencedSymbolTableEntryCount);
stream.writeUint32(indirectSymbolTableFileOffset);
stream.writeUint32(indirectSymbolTableEntryCount);
stream.writeUint32(externalRelocationsFileOffset);
stream.writeUint32(externalRelocationsCount);
stream.writeUint32(localRelocationsFileOffset);
stream.writeUint32(localRelocationsCount);
}
}
/// A load command that contains the offsets and sizes of a blob of data in
/// the __LINKEDIT segment. The contents of the blob is determined by the
/// specific `code` value used for the load command.
class MachOLinkeditDataCommand extends MachOLoadCommand {
/// The file offset for the segment data.
///
/// `uint32_t dataoff` in the C headers.
final int dataFileOffset;
/// The file size of the segment data.
///
/// `uint32_t datasize` in the C headers.
final int dataSize;
MachOLinkeditDataCommand(
super.code,
super.size,
this.dataFileOffset,
this.dataSize,
);
static MachOLinkeditDataCommand fromStream(
int code, final int size, MachOReader stream) {
final dataFileOffset = stream.readUint32();
final dataSize = stream.readUint32();
return MachOLinkeditDataCommand(code, size, dataFileOffset, dataSize);
}
@override
MachOLinkeditDataCommand adjust(OffsetsAdjuster adjuster) =>
MachOLinkeditDataCommand(
code, size, adjuster.adjust(dataFileOffset), dataSize);
@override
void writeContentsSync(MachOWriter stream) {
stream.writeUint32(dataFileOffset);
stream.writeUint32(dataSize);
}
}
/// A load command that contains the offset and size of an encrypted segment.
class MachOEncryptionInfoCommand extends MachOLoadCommand {
/// The file offset for the encrypted segment.
///
/// `uint32_t cryptoff` in the C headers.
final int fileOffset;
/// The file size of the encrypted segment.
///
/// `uint32_t cryptsize` in the C headers.
final int fileSize;
/// The id of the encryption system used for the encrypted segment.
/// A value of 0 means that the segment is not yet encrypted.
///
/// `uint32_t cryptid` in the C headers.
final int encryptionSystem;
/// Padding for 64-bit encryption info load commands. Null for 32-bit
/// encryption info load commands.
///
/// `uint32_t pad` for `encryption_info_command_64` in the C headers.
final int? padding;
MachOEncryptionInfoCommand(super.code, super.size, this.fileOffset,
this.fileSize, this.encryptionSystem, this.padding);
static MachOEncryptionInfoCommand fromStream(
int code, final int size, MachOReader stream) {
final is64Bit =
LoadCommandType.fromCode(code) == LoadCommandType.encryptionInfo64;
final fileOffset = stream.readUint32();
final fileSize = stream.readUint32();
final encryptionSystem = stream.readUint32();
int? padding;
if (is64Bit) {
padding = stream.readUint32();
}
return MachOEncryptionInfoCommand(
code, size, fileOffset, fileSize, encryptionSystem, padding);
}
@override
MachOEncryptionInfoCommand adjust(OffsetsAdjuster adjuster) =>
MachOEncryptionInfoCommand(code, size, adjuster.adjust(fileOffset),
fileSize, encryptionSystem, padding);
@override
void writeContentsSync(MachOWriter stream) {
stream.writeUint32(fileOffset);
stream.writeUint32(fileSize);
stream.writeUint32(encryptionSystem);
if (padding != null) {
stream.writeUint32(padding!);
}
}
}
/// A load command that contains the file offsets and sizes of the new
/// compressed form of the information dyld needs to load the image on
/// MacOS 10.6 and later.
class MachODyldInfoCommand extends MachOLoadCommand {
/// File offset for rebase information.
///
/// `uint32_t rebase_off` in the C headers.
final int rebaseOffset;
/// File size of rebase information.
///
/// `uint32_t rebase_size` in the C headers.
final int rebaseSize;
/// File offset for binding information.
///
/// `uint32_t bind_off` in the C headers.
final int bindingOffset;
/// File size of binding information.
///
/// `uint32_t bind_size` in the C headers.
final int bindingSize;
/// File offset for weak binding information.
///
/// `uint32_t weak_bind_off` in the C headers.
final int weakBindingOffset;
/// File size of weak binding information.
///
/// `uint32_t weak_bind_size` in the C headers.
final int weakBindingSize;
/// File offset for lazy binding information.
///
/// `uint32_t lazy_bind_off` in the C headers.
final int lazyBindingOffset;
/// File size of lazy binding information.
///
/// `uint32_t lazy_bind_size` in the C headers.
final int lazyBindingSize;
/// File offset for export information.
///
/// `uint32_t export_off` in the C headers.
final int exportOffset;
/// File size of export information.
///
/// `uint32_t export_size` in the C headers.
final int exportSize;
MachODyldInfoCommand(
super.code,
super.size,
this.rebaseOffset,
this.rebaseSize,
this.bindingOffset,
this.bindingSize,
this.weakBindingOffset,
this.weakBindingSize,
this.lazyBindingOffset,
this.lazyBindingSize,
this.exportOffset,
this.exportSize);
static MachODyldInfoCommand fromStream(
int code, int size, MachOReader stream) {
final rebaseOffset = stream.readUint32();
final rebaseSize = stream.readUint32();
final bindingOffset = stream.readUint32();
final bindingSize = stream.readUint32();
final weakBindingOffset = stream.readUint32();
final weakBindingSize = stream.readUint32();
final lazyBindingOffset = stream.readUint32();
final lazyBindingSize = stream.readUint32();
final exportOffset = stream.readUint32();
final exportSize = stream.readUint32();
return MachODyldInfoCommand(
code,
size,
rebaseOffset,
rebaseSize,
bindingOffset,
bindingSize,
weakBindingOffset,
weakBindingSize,
lazyBindingOffset,
lazyBindingSize,
exportOffset,
exportSize);
}
@override
MachODyldInfoCommand adjust(OffsetsAdjuster adjuster) => MachODyldInfoCommand(
code,
size,
adjuster.adjust(rebaseOffset),
rebaseSize,
adjuster.adjust(bindingOffset),
bindingSize,
adjuster.adjust(weakBindingOffset),
weakBindingSize,
adjuster.adjust(lazyBindingOffset),
lazyBindingSize,
adjuster.adjust(exportOffset),
exportSize);
@override
void writeContentsSync(MachOWriter stream) {
stream.writeUint32(rebaseOffset);
stream.writeUint32(rebaseSize);
stream.writeUint32(bindingOffset);
stream.writeUint32(bindingSize);
stream.writeUint32(weakBindingOffset);
stream.writeUint32(weakBindingSize);
stream.writeUint32(lazyBindingOffset);
stream.writeUint32(lazyBindingSize);
stream.writeUint32(exportOffset);
stream.writeUint32(exportSize);
}
}
/// A load command used for executables to specify the file offset of the entry
/// point of the program.
class MachOEntryPointCommand extends MachOLoadCommand {
/// The file offset of the entry point to the program (e.g., main()).
///
/// `uint64_t entryoff` in the C headers.
final int entryOffset;
/// When non-zero, specifies the initial stack size.
///
/// `uint64_t stacksize` in the C headers.
final int stackSize;
MachOEntryPointCommand(
super.code, super.size, this.entryOffset, this.stackSize);
static MachOEntryPointCommand fromStream(
int code, int size, MachOReader stream) {
final entryOffset = stream.readUint64();
final stackSize = stream.readUint64();
return MachOEntryPointCommand(code, size, entryOffset, stackSize);
}
@override
MachOEntryPointCommand adjust(OffsetsAdjuster adjuster) =>
MachOEntryPointCommand(
code, size, adjuster.adjust(entryOffset), stackSize);
@override
void writeContentsSync(MachOWriter stream) {
stream.writeUint64(entryOffset);
stream.writeUint64(stackSize);
}
}
/// A load command that references an array of entries in the __LINKEDIT
/// segment, each of which describes a range of data located in the __TEXT
/// segment.
class MachODataInCodeEntry extends MachOLoadCommand {
/// The file offset of the array of data in code entries.
///
/// `uint32_t offset` in the C headers.
final int offset;
/// The length of the array of data in bytes.
///
/// `uint16_t length` in the C headers.
final int length;
/// The kind of entries contained in the array as a `DICE_KIND_` value.
///
/// `uint16_t kind` in the C headers.
final int kind;
MachODataInCodeEntry(
super.code,
super.size,
this.offset,
this.length,
this.kind,
);
static MachODataInCodeEntry fromStream(
int code, int size, MachOReader stream) {
final offset = stream.readUint32();
final length = stream.readUint16();
final kind = stream.readUint16();
return MachODataInCodeEntry(code, size, offset, length, kind);
}
@override
MachODataInCodeEntry adjust(OffsetsAdjuster adjuster) =>
MachODataInCodeEntry(code, size, adjuster.adjust(offset), length, kind);
@override
void writeContentsSync(MachOWriter stream) {
stream.writeUint32(offset);
stream.writeUint16(length);
stream.writeUint16(kind);
}
}
/// A load command that points to an arbitrary block of data in the file.
class MachONoteCommand extends MachOLoadCommand {
/// The owner name for this note.
///
/// `char data_owner[16]` in the C headers.
final String dataOwner;
/// The file offset for the data.
///
/// `uint64_t offset` in the C headers.
final int fileOffset;
/// The file size of the data.
///
/// `uint64_t size` in the C headers.
final int fileSize;
MachONoteCommand(
super.code, super.size, this.dataOwner, this.fileOffset, this.fileSize);
static MachONoteCommand fromStream(int code, int size, MachOReader stream) {
final dataOwner =
stream.readFixedLengthNullTerminatedString(_dataOwnerLength);
final fileOffset = stream.readUint64();
final fileSize = stream.readUint64();
return MachONoteCommand(code, size, dataOwner, fileOffset, fileSize);
}
/// Constructs a note load command given the content of the note-specific
/// fields, using the default values for the code and size fields.
static MachONoteCommand fromFields(
String dataOwner, int fileOffset, int fileSize) =>
MachONoteCommand(LoadCommandType.note.code, _defaultSize, dataOwner,
fileOffset, fileSize);
/// The maximum size of the dataOwner field contents.
static const _dataOwnerLength = 16;
// The total size of any given note load command. A note load command contains
// the 4-byte cmd and cmdsize fields that start every load command, a 16 byte
// name field, and then two additional 8-byte fields.
static const _defaultSize = 4 + 4 + 16 + 8 + 8;
@override
MachONoteCommand adjust(OffsetsAdjuster adjuster) => MachONoteCommand(
code, size, dataOwner, adjuster.adjust(fileOffset), fileSize);
@override
void writeContentsSync(MachOWriter stream) {
stream.writeFixedLengthNullTerminatedString(dataOwner, _dataOwnerLength);
stream.writeUint64(fileOffset);
stream.writeUint64(fileSize);
}
}
/// A load command that specifies a fileset entry.
class MachOFileSetEntryCommand extends MachOLoadCommand {
/// The memory address of the dynamically linked shared library.
///
/// `uint64_t vmaddr` in the C headers.
final int memoryAddress;
/// The file offset of the dynamically linked shared library.
///
/// `uint64_t fileoff` in the C headers.
final int fileOffset;
/// The contained entry id.
///
/// `lc_str entry_id` in the C headers.
final int entryId;
/// Reserved padding.
///
/// `uint32_t reserved` in the C headers.
final int reserved;
MachOFileSetEntryCommand(super.code, super.size, this.memoryAddress,
this.fileOffset, this.entryId, this.reserved);
static MachOFileSetEntryCommand fromStream(
int code, int size, MachOReader stream) {
final memoryAddress = stream.readUint64();
final fileOffset = stream.readUint64();
final entryId = stream.readLCString();
final reserved = stream.readUint32();
return MachOFileSetEntryCommand(
code, size, memoryAddress, fileOffset, entryId, reserved);
}
@override
MachOFileSetEntryCommand adjust(OffsetsAdjuster adjuster) =>
MachOFileSetEntryCommand(code, size, memoryAddress,
adjuster.adjust(fileOffset), entryId, reserved);
@override
void writeContentsSync(MachOWriter stream) {
stream.writeUint64(memoryAddress);
stream.writeUint64(fileOffset);
stream.writeLCString(entryId);
stream.writeUint32(reserved);
}
}
/// The headers and load commands of a MachO file. Note that this class does
/// not contain the contents of the load commands. Instead, those are expected
/// to be copied over from the original file appropriately.
class MachOFile {
/// The header of the MachO file.
final MachOHeader header;
/// The load commands of the MachO file, which represent how to read the
/// non-header contents of the MachO file.
final List<MachOLoadCommand> commands;
/// Whether or not the parsed MachO file has a code signature.
final bool hasCodeSignature;
MachOFile._(this.header, this.commands, this.hasCodeSignature);
static MachOFile fromFile(File file) {
// Ensure the file is long enough to contain the magic bytes.
final fileLength = file.lengthSync();
if (fileLength < 4) {
throw FormatException(
'File was not formatted properly. Length was too short: $fileLength');
}
final stream = file.openSync();
final header = MachOHeader.fromStream(stream);
if (header == null) {
throw FormatException(
'Could not parse a MachO header from the file: ${file.path}');
}
final uses64BitPointers = CpuType.cpuTypeUses64BitPointers(header.cpu);
final reader = MachOReader(stream, header.endian, uses64BitPointers);
final commands = List<MachOLoadCommand>.generate(
header.loadCommandsCount, (_) => MachOLoadCommand.fromStream(reader));
final size = _totalSize(header, commands);
assert(size == stream.positionSync());
final hasCodeSignature =
commands.any((c) => c.type == LoadCommandType.codeSignature);
return MachOFile._(header, commands, hasCodeSignature);
}
/// Returns a new MachOFile that is like the input, but with the empty segment
/// used to reserve header space dropped and with a new note load command
/// inserted prior to the __LINKEDIT segment load command. Any file offsets
/// in other load commands that reference the __LINKEDIT segment are adjusted
/// appropriately.
MachOFile adjustHeaderForSnapshot(int snapshotSize) {
// This is not an idempotent operation.
if (snapshotNote != null) {
throw const FormatException(
'The executable already has a Dart snapshot inserted');
}
final reserved = reservedSegment;
if (reserved == null) {
throw const FormatException('$reservedSegmentName segment not found');
}
final linkedit = linkEditSegment;
if (linkedit == null) {
throw const FormatException('__LINKEDIT segment not found');
}
// We insert the contents of the snapshot where the old linkedit segment
// started in the original executable, aligned appropriately.
final fileOffset = align(linkedit.fileOffset, segmentAlignment);
final fileSize = snapshotSize;
final note =
MachONoteCommand.fromFields(snapshotNoteName, fileOffset, fileSize);
// Now we need to build the new header from these modified pieces.
final newHeader = MachOHeader(
header.magic,
header.cpu,
header.machine,
header.type,
// We remove the reserved section and replace it with the note.
header.loadCommandsCount,
header.loadCommandsSize - reserved.size + note.size,
header.flags,
header.reserved);
// We'll want the __LINKEDIT segment to start at the next aligned file
// offset after the end of the snapshot, so we'll need to adjust all
// file offsets pointing into it (including its own segment) accordingly.
final snapshotEnd =
align(note.fileOffset + note.fileSize, segmentAlignment);
final adjuster = OffsetsAdjuster()
..add(linkedit.fileOffset, snapshotEnd - linkedit.fileOffset);
final newCommands = <MachOLoadCommand>[];
for (final command in commands) {
if (command == reserved) {
// Drop the reserved segment on the floor, as we only add it so there's
// enough header space to add the note.
continue;
}
if (command == linkedit) {
// Insert the new note prior to the __LINKEDIT segment.
newCommands.add(note);
}
newCommands.add(command.adjust(adjuster));
}
final newFile = MachOFile._(newHeader, newCommands, hasCodeSignature);
if (newFile.size > size) {
throw FormatException('Cannot add new note load command to header: '
'new size ${newFile.size} > the old size $size)');
}
return newFile;
}
/// The name of the segment containing all the structs created and maintained
/// by the link editor.
static const _linkEditSegmentName = '__LINKEDIT';
/// Retrieves the segment load command used to reserve header space for the
/// snapshot information. Returns null if not found or if it is of an
/// unexpected form.
MachOSegmentCommand? get reservedSegment {
final reservedIndex = commands.indexWhere(
(c) => c is MachOSegmentCommand && c.name == reservedSegmentName);
if (reservedIndex < 0) {
return null;
}
final reserved = commands[reservedIndex] as MachOSegmentCommand;
assert(reserved.fileSize == 0);
assert(reserved.sections.single.name == reservedSectionName);
return reserved;
}
/// Retrieves the __LINKEDIT segment load command. Returns null if not found.
MachOSegmentCommand? get linkEditSegment {
final linkEditIndex = commands.indexWhere(
(c) => c is MachOSegmentCommand && c.name == _linkEditSegmentName);
if (linkEditIndex < 0) {
return null;
}
// __LINKEDIT should be the last segment load command.
assert(
!commands.skip(linkEditIndex + 1).any((c) => c is MachOSegmentCommand));
return commands[linkEditIndex] as MachOSegmentCommand;
}
/// Retrieves the note load command that points to the snapshot contents in
/// the executable. Returns null if not found.
MachONoteCommand? get snapshotNote {
final snapshotIndex = commands.indexWhere(
(c) => c is MachONoteCommand && c.dataOwner == snapshotNoteName);
if (snapshotIndex < 0) {
return null;
}
return commands[snapshotIndex] as MachONoteCommand;
}
static bool containsSnapshot(File file) =>
MachOFile.fromFile(file).snapshotNote != null;
static int _totalSize(MachOHeader header, List<MachOLoadCommand> commands) =>
commands.fold(header.size, (i, c) => i + c.size);
int get size => _totalSize(header, commands);
/// Writes the MachO file to the given [RandomAccessFile] stream.
void writeSync(RandomAccessFile stream) {
header.writeSync(stream);
final uses64BitPointers = CpuType.cpuTypeUses64BitPointers(header.cpu);
final writer = MachOWriter(stream, header.endian, uses64BitPointers);
// Write all of the commands.
for (final command in commands) {
command.writeSync(writer);
}
}
}