blob: 99dce1a20712dc9bc4def2a7c232a90ebe7904e9 [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.
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import './macho.dart';
extension ByteReader on RandomAccessFile {
Uint32 readUint32() {
Uint8List rawBytes = readSync(4);
var byteView = ByteData.view(rawBytes.buffer);
return Uint32(byteView.getUint32(0, Endian.little));
}
Uint64 readUint64() {
Uint8List rawBytes = readSync(8);
var byteView = ByteData.view(rawBytes.buffer);
return Uint64(byteView.getUint64(0, Endian.little));
}
Int32 readInt32() {
Uint8List rawBytes = readSync(4);
var byteView = ByteData.view(rawBytes.buffer);
return Int32(byteView.getInt32(0, Endian.little));
}
}
class MachOFile {
IMachOHeader? header;
// The headerMaxOffset is set during parsing based on the maximum offset for
// segment offsets. Assuming the header start at byte 0 (that seems to always
// be the case), this number represents the total size of the header, which
// often includes a significant amount of zero-padding.
int headerMaxOffset = 0;
// We keep track on whether a code signature was seen so we can recreate it
// in the case that the binary has a CD hash that nededs updating.
bool hasCodeSignature = false;
// This wil contain all of the "load commands" in this MachO file. A load
// command is really a typed schema that indicates various parts of the MachO
// file (e.g. where to find the TEXT and DATA sections).
List<IMachOLoadCommand> commands =
List<IMachOLoadCommand>.empty(growable: true);
MachOFile();
// Returns the number of bytes read from the file.
Future<int> loadFromFile(File file) async {
// Ensure the file is long enough to contain the magic bytes.
final int fileLength = await file.length();
if (fileLength < 4) {
throw FormatException(
"File was not formatted properly. Length was too short: $fileLength");
}
// Read the first 4 bytes to see what type of MachO file this is.
var stream = await file.open();
var magic = stream.readUint32();
bool is64Bit = magic == MachOConstants.MH_MAGIC_64 ||
magic == MachOConstants.MH_CIGAM_64;
await stream.setPosition(0);
// Set the max header offset to the maximum file size so that when we read
// in the header we can correctly set the total header size.
headerMaxOffset = (1 << 63) - 1;
header = await _headerFromStream(stream, is64Bit);
if (header == null) {
throw FormatException(
"Could not parse a MachO header from the file: ${file.path}");
} else {
commands = await _commandsFromStream(stream, header!);
}
return stream.positionSync();
}
Future<MachOSymtabCommand> parseSymtabFromStream(
final Uint32 cmdsize, RandomAccessFile stream) async {
final symoff = stream.readUint32();
final nsyms = stream.readUint32();
final stroff = stream.readUint32();
final strsize = stream.readUint32();
return MachOSymtabCommand(cmdsize, symoff, nsyms, stroff, strsize);
}
Future<MachODysymtabCommand> parseDysymtabFromStream(
final Uint32 cmdsize, RandomAccessFile stream) async {
final ilocalsym = stream.readUint32();
final nlocalsym = stream.readUint32();
final iextdefsym = stream.readUint32();
final nextdefsym = stream.readUint32();
final iundefsym = stream.readUint32();
final nundefsym = stream.readUint32();
final tocoff = stream.readUint32();
final ntoc = stream.readUint32();
final modtaboff = stream.readUint32();
final nmodtab = stream.readUint32();
final extrefsymoff = stream.readUint32();
final nextrefsyms = stream.readUint32();
final indirectsymoff = stream.readUint32();
final nindirectsyms = stream.readUint32();
final extreloff = stream.readUint32();
final nextrel = stream.readUint32();
final locreloff = stream.readUint32();
final nlocrel = stream.readUint32();
return MachODysymtabCommand(
cmdsize,
ilocalsym,
nlocalsym,
iextdefsym,
nextdefsym,
iundefsym,
nundefsym,
tocoff,
ntoc,
modtaboff,
nmodtab,
extrefsymoff,
nextrefsyms,
indirectsymoff,
nindirectsyms,
extreloff,
nextrel,
locreloff,
nlocrel);
}
Future<MachOLinkeditDataCommand> parseLinkeditDataCommand(
final Uint32 cmd, final Uint32 cmdsize, RandomAccessFile stream) async {
final dataoff = stream.readUint32();
final datasize = stream.readUint32();
return MachOLinkeditDataCommand(
cmd,
cmdsize,
dataoff,
datasize,
);
}
Future<MachODyldInfoCommand> parseDyldInfoFromStream(
final Uint32 cmd, final Uint32 cmdsize, RandomAccessFile stream) async {
// Note that we're relying on the fact that the mirror returns the list of
// fields in the same order they're defined ni the class definition.
final rebaseOff = stream.readUint32();
final rebaseSize = stream.readUint32();
final bindOff = stream.readUint32();
final bindSize = stream.readUint32();
final weakBindOff = stream.readUint32();
final weakBindSize = stream.readUint32();
final lazyBindOff = stream.readUint32();
final lazyBindSize = stream.readUint32();
final exportOff = stream.readUint32();
final exportSize = stream.readUint32();
return MachODyldInfoCommand(
cmd,
cmdsize,
rebaseOff,
rebaseSize,
bindOff,
bindSize,
weakBindOff,
weakBindSize,
lazyBindOff,
lazyBindSize,
exportOff,
exportSize);
}
Future<MachOSegmentCommand64> parseSegmentCommand64FromStream(
final Uint32 cmdsize, RandomAccessFile stream) async {
final Uint8List segname = await stream.read(16);
final vmaddr = stream.readUint64();
final vmsize = stream.readUint64();
final fileoff = stream.readUint64();
final filesize = stream.readUint64();
final maxprot = stream.readInt32();
final initprot = stream.readInt32();
final nsects = stream.readUint32();
final flags = stream.readUint32();
if (nsects.asInt() == 0 && filesize.asInt() != 0) {
headerMaxOffset = min(headerMaxOffset, fileoff.asInt());
}
final sections = List.filled(nsects.asInt(), 0).map((_) {
final Uint8List sectname = stream.readSync(16);
final Uint8List segname = stream.readSync(16);
final addr = stream.readUint64();
final size = stream.readUint64();
final offset = stream.readUint32();
final align = stream.readUint32();
final reloff = stream.readUint32();
final nreloc = stream.readUint32();
final flags = stream.readUint32();
final reserved1 = stream.readUint32();
final reserved2 = stream.readUint32();
final reserved3 = stream.readUint32();
final notZerofill =
(flags & MachOConstants.S_ZEROFILL) != MachOConstants.S_ZEROFILL;
if (offset > 0 && size > 0 && notZerofill) {
headerMaxOffset = min(headerMaxOffset, offset.asInt());
}
return MachOSection64(sectname, segname, addr, size, offset, align,
reloff, nreloc, flags, reserved1, reserved2, reserved3);
}).toList();
return MachOSegmentCommand64(cmdsize, segname, vmaddr, vmsize, fileoff,
filesize, maxprot, initprot, nsects, flags, sections);
}
Future<IMachOHeader> _headerFromStream(
RandomAccessFile stream, bool is64Bit) async {
final magic = stream.readUint32();
final cputype = stream.readUint32();
final cpusubtype = stream.readUint32();
final filetype = stream.readUint32();
final ncmds = stream.readUint32();
final sizeofcmds = stream.readUint32();
final flags = stream.readUint32();
if (is64Bit) {
final reserved = stream.readUint32();
return MachOHeader(magic, cputype, cpusubtype, filetype, ncmds,
sizeofcmds, flags, reserved);
} else {
return MachOHeader32(
magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags);
}
}
void writeLoadCommandToStream(
IMachOLoadCommand command, RandomAccessFile stream) {
command.writeSync(stream);
}
void writeSync(RandomAccessFile stream) {
// Write the header.
stream.writeUint32(header!.magic);
stream.writeUint32(header!.cputype);
stream.writeUint32(header!.cpusubtype);
stream.writeUint32(header!.filetype);
stream.writeUint32(header!.ncmds);
stream.writeUint32(header!.sizeofcmds);
stream.writeUint32(header!.flags);
if (header is MachOHeader) {
stream.writeUint32(header!.reserved);
}
// Write all of the commands.
for (var command in commands) {
writeLoadCommandToStream(command, stream);
}
// Pad the header according to the offset.
final int paddingAmount = headerMaxOffset - stream.positionSync();
if (paddingAmount > 0) {
stream.writeFromSync(List.filled(paddingAmount, 0));
}
}
Future<List<IMachOLoadCommand>> _commandsFromStream(
RandomAccessFile stream, IMachOHeader header) async {
final loadCommands = List<MachOLoadCommand>.empty(growable: true);
for (int i = 0; i < header.ncmds.asInt(); i++) {
final cmd = stream.readUint32();
final cmdsize = stream.readUint32();
// We need to read cmdsize bytes to get to the next command definition,
// but the cmdsize does includes the 2 bytes we just read (cmd +
// cmdsize) so we need to subtract those.
await stream
.setPosition((await stream.position()) + cmdsize.asInt() - 2 * 4);
loadCommands.add(MachOLoadCommand(cmd, cmdsize));
}
// Un-read all the bytes we just read.
var loadCommandsOffset = loadCommands
.map((command) => command.cmdsize)
.reduce((value, element) => value + element);
await stream
.setPosition((await stream.position()) - loadCommandsOffset.asInt());
final commands = List<IMachOLoadCommand>.empty(growable: true);
for (int i = 0; i < header.ncmds.asInt(); i++) {
final cmd = stream.readUint32();
final cmdsize = stream.readUint32();
// TODO(sarietta): Handle all MachO load command types. For now, since
// this implementation is exclusively being used to handle generating
// MacOS-compatible MachO executables for compiled dart scripts, only the
// load commands that are currently implemented are strictly necessary. It
// may be useful to handle all cases and pull this functionality out to a
// separate MachO library.
if (cmd == MachOConstants.LC_SEGMENT_64) {
commands.add(await parseSegmentCommand64FromStream(cmdsize, stream));
} else if (cmd == MachOConstants.LC_DYLD_INFO_ONLY ||
cmd == MachOConstants.LC_DYLD_INFO) {
commands.add(await parseDyldInfoFromStream(cmd, cmdsize, stream));
} else if (cmd == MachOConstants.LC_SYMTAB) {
commands.add(await parseSymtabFromStream(cmdsize, stream));
} else if (cmd == MachOConstants.LC_DYSYMTAB) {
commands.add(await parseDysymtabFromStream(cmdsize, stream));
} else if (cmd == MachOConstants.LC_CODE_SIGNATURE ||
cmd == MachOConstants.LC_SEGMENT_SPLIT_INFO ||
cmd == MachOConstants.LC_FUNCTION_STARTS ||
cmd == MachOConstants.LC_DATA_IN_CODE ||
cmd == MachOConstants.LC_DYLIB_CODE_SIGN_DRS) {
if (cmd == MachOConstants.LC_CODE_SIGNATURE) {
hasCodeSignature = true;
}
commands.add(await parseLinkeditDataCommand(cmd, cmdsize, stream));
} else if (cmd == MachOConstants.LC_SEGMENT ||
cmd == MachOConstants.LC_SYMSEG ||
cmd == MachOConstants.LC_THREAD ||
cmd == MachOConstants.LC_UNIXTHREAD ||
cmd == MachOConstants.LC_LOADFVMLIB ||
cmd == MachOConstants.LC_IDFVMLIB ||
cmd == MachOConstants.LC_IDENT ||
cmd == MachOConstants.LC_FVMFILE ||
cmd == MachOConstants.LC_PREPAGE ||
cmd == MachOConstants.LC_LOAD_DYLIB ||
cmd == MachOConstants.LC_ID_DYLIB ||
cmd == MachOConstants.LC_LOAD_DYLINKER ||
cmd == MachOConstants.LC_ID_DYLINKER ||
cmd == MachOConstants.LC_PREBOUND_DYLIB ||
cmd == MachOConstants.LC_ROUTINES ||
cmd == MachOConstants.LC_SUB_FRAMEWORK ||
cmd == MachOConstants.LC_SUB_UMBRELLA ||
cmd == MachOConstants.LC_SUB_CLIENT ||
cmd == MachOConstants.LC_SUB_LIBRARY ||
cmd == MachOConstants.LC_TWOLEVEL_HINTS ||
cmd == MachOConstants.LC_PREBIND_CKSUM ||
cmd == MachOConstants.LC_LOAD_WEAK_DYLIB ||
cmd == MachOConstants.LC_ROUTINES_64 ||
cmd == MachOConstants.LC_UUID ||
cmd == MachOConstants.LC_RPATH ||
cmd == MachOConstants.LC_REEXPORT_DYLIB ||
cmd == MachOConstants.LC_LAZY_LOAD_DYLIB ||
cmd == MachOConstants.LC_ENCRYPTION_INFO ||
cmd == MachOConstants.LC_LOAD_UPWARD_DYLIB ||
cmd == MachOConstants.LC_VERSION_MIN_MACOSX ||
cmd == MachOConstants.LC_VERSION_MIN_IPHONEOS ||
cmd == MachOConstants.LC_DYLD_ENVIRONMENT ||
cmd == MachOConstants.LC_MAIN ||
cmd == MachOConstants.LC_SOURCE_VERSION ||
cmd == MachOConstants.LC_BUILD_VERSION) {
// cmdsize includes the size of the contents + cmd + cmdsize
final contents = await stream.read(cmdsize.asInt() - 2 * 4);
commands.add(MachOGenericLoadCommand(cmd, cmdsize, contents));
} else {
// cmdsize includes the size of the contents + cmd + cmdsize
final contents = await stream.read(cmdsize.asInt() - 2 * 4);
commands.add(MachOGenericLoadCommand(cmd, cmdsize, contents));
final cmdString = "0x${cmd.asInt().toRadixString(16)}";
print("Found unknown MachO load command: $cmdString");
}
}
return commands;
}
}