blob: 144112f35960247caccdd679a426a326b8863254 [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:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
int align(int size, int base) {
final int over = size % base;
if (over != 0) {
return size + (base - over);
}
return size;
}
class BytesBacked {
ByteData data;
BytesBacked(this.data);
int get size => data.lengthInBytes;
Future<void> write(RandomAccessFile output) async {
await output.writeFrom(Uint8List.sublistView(data));
}
}
class CoffFileHeader extends BytesBacked {
CoffFileHeader._(ByteData data) : super(data);
static const _fileHeaderSize = 20;
static const _sectionCountOffset = 2;
static const _optionalHeaderSizeOffset = 16;
static CoffFileHeader fromTypedData(TypedData source, int offset) {
if (source.lengthInBytes < offset + _fileHeaderSize) {
throw 'File is truncated within the COFF file header';
}
final buffer = Uint8List(_fileHeaderSize);
buffer.setAll(
0, Uint8List.sublistView(source, offset, offset + _fileHeaderSize));
return CoffFileHeader._(ByteData.sublistView(buffer));
}
int get sectionCount => data.getUint16(_sectionCountOffset, Endian.little);
set sectionCount(int value) =>
data.setUint16(_sectionCountOffset, value, Endian.little);
int get optionalHeaderSize =>
data.getUint16(_optionalHeaderSizeOffset, Endian.little);
}
class CoffOptionalHeader extends BytesBacked {
CoffOptionalHeader._(ByteData data) : super(data);
static const _pe32Magic = 0x10b;
static const _pe32PlusMagic = 0x20b;
static const _magicOffset = 0;
static const _sectionAlignmentOffset = 32;
static const _fileAlignmentOffset = 36;
static const _imageSizeOffset = 56;
static const _headersSizeOffset = 60;
static CoffOptionalHeader fromTypedData(
TypedData source, int offset, int size) {
if (source.lengthInBytes < offset + size) {
throw 'File is truncated within the COFF optional header';
}
final buffer = Uint8List(size);
buffer.setAll(0, Uint8List.sublistView(source, offset, offset + size));
final data = ByteData.sublistView(buffer);
final magic = data.getUint16(_magicOffset, Endian.little);
if (magic != _pe32Magic && magic != _pe32PlusMagic) {
throw 'Not a PE32 or PE32+ image file';
}
return CoffOptionalHeader._(data);
}
// The alignment used for virtual addresses of sections, _not_ file offsets.
int get sectionAlignment =>
data.getUint32(_sectionAlignmentOffset, Endian.little);
// The alignment used for file offsets of section data and other contents.
int get fileAlignment => data.getUint32(_fileAlignmentOffset, Endian.little);
int get headersSize => data.getUint32(_headersSizeOffset, Endian.little);
set headersSize(int value) =>
data.setUint32(_headersSizeOffset, value, Endian.little);
int get imageSize => data.getUint32(_imageSizeOffset, Endian.little);
set imageSize(int value) =>
data.setUint32(_imageSizeOffset, value, Endian.little);
}
class CoffSectionHeader extends BytesBacked {
CoffSectionHeader._(ByteData data) : super(data);
static const _virtualSizeOffset = 8;
static const _virtualAddressOffset = 12;
static const _fileSizeOffset = 16;
static const _fileOffsetOffset = 20;
static const _characteristicsOffset = 36;
static const _discardableFlag = 0x02000000;
String get name => String.fromCharCodes(Uint8List.sublistView(data, 0, 8));
set name(String name) {
// Each section header has only eight bytes for the section name.
// First reset it to zeroes, then copy over the UTF-8 encoded version.
final buffer = Uint8List.sublistView(data, 0, 8);
buffer.fillRange(0, 8, 0);
buffer.setAll(0, utf8.encode(name));
}
int get virtualAddress =>
data.getUint32(_virtualAddressOffset, Endian.little);
set virtualAddress(int offset) =>
data.setUint32(_virtualAddressOffset, offset, Endian.little);
int get virtualSize => data.getUint32(_virtualSizeOffset, Endian.little);
set virtualSize(int offset) =>
data.setUint32(_virtualSizeOffset, offset, Endian.little);
int get fileOffset => data.getUint32(_fileOffsetOffset, Endian.little);
set fileOffset(int offset) =>
data.setUint32(_fileOffsetOffset, offset, Endian.little);
int get fileSize => data.getUint32(_fileSizeOffset, Endian.little);
set fileSize(int offset) =>
data.setUint32(_fileSizeOffset, offset, Endian.little);
int get characteristics =>
data.getUint32(_characteristicsOffset, Endian.little);
set characteristics(int value) =>
data.setUint32(_characteristicsOffset, value, Endian.little);
bool get isDiscardable => characteristics & _discardableFlag != 0;
set isDiscardable(bool value) {
if (value) {
characteristics |= _discardableFlag;
} else {
characteristics &= ~_discardableFlag;
}
}
}
class CoffSectionTable extends BytesBacked {
CoffSectionTable._(ByteData data) : super(data);
static const _entrySize = 40;
static CoffSectionTable fromTypedData(
TypedData source, int offset, int sections) {
final size = sections * _entrySize;
if (source.lengthInBytes < offset + size) {
throw 'File is truncated within the COFF section table';
}
final buffer = Uint8List(size);
buffer.setAll(0, Uint8List.sublistView(source, offset, offset + size));
return CoffSectionTable._(ByteData.sublistView(buffer));
}
Iterable<CoffSectionHeader> get entries sync* {
for (int i = 0; i < size; i += _entrySize) {
yield CoffSectionHeader._(ByteData.sublistView(data, i, i + _entrySize));
}
}
int get addressEnd => entries.fold(
0, (i, entry) => max(i, entry.virtualAddress + entry.virtualSize));
int get offsetEnd =>
entries.fold(0, (i, entry) => max(i, entry.fileOffset + entry.fileSize));
CoffSectionHeader allocateNewSectionHeader() {
final newBuffer = Uint8List(size + _entrySize);
newBuffer.setAll(0, Uint8List.sublistView(data));
data = ByteData.sublistView(newBuffer);
return CoffSectionHeader._(
ByteData.sublistView(data, size - _entrySize, size));
}
}
class CoffHeaders {
final int _coffOffset;
final CoffFileHeader fileHeader;
final CoffOptionalHeader optionalHeader;
final CoffSectionTable sectionTable;
CoffHeaders._(this._coffOffset, this.fileHeader, this.optionalHeader,
this.sectionTable);
static CoffHeaders fromTypedData(TypedData source, int offset) {
final fileHeader = CoffFileHeader.fromTypedData(source, offset);
final optionalHeader = CoffOptionalHeader.fromTypedData(
source, offset + fileHeader.size, fileHeader.optionalHeaderSize);
final sectionTable = CoffSectionTable.fromTypedData(
source,
offset + fileHeader.size + optionalHeader.size,
fileHeader.sectionCount);
return CoffHeaders._(offset, fileHeader, optionalHeader, sectionTable);
}
// Keep in sync with kSnapshotSectionName in snapshot_utils.cc.
static const _snapshotSectionName = "snapshot";
int get size => optionalHeader.headersSize;
void addSnapshotSectionHeader(int length) {
final oldHeadersSize = optionalHeader.headersSize;
final address =
align(sectionTable.addressEnd, optionalHeader.sectionAlignment);
final offset = align(sectionTable.offsetEnd, optionalHeader.fileAlignment);
// Create and fill the new section header entry.
final newHeader = sectionTable.allocateNewSectionHeader();
newHeader.name = _snapshotSectionName;
newHeader.virtualAddress = address;
newHeader.virtualSize = length;
newHeader.fileOffset = offset;
newHeader.fileSize = align(length, optionalHeader.fileAlignment);
newHeader.isDiscardable = true;
// Leave the rest of the header fields with zero values.
// Increment the number of sections in the file header.
fileHeader.sectionCount += 1;
// Adjust the header size stored in the optional header, which must be
// a multiple of fileAlignment.
optionalHeader.headersSize = align(
_coffOffset + fileHeader.size + optionalHeader.size + sectionTable.size,
optionalHeader.fileAlignment);
// If the size of the headers changed, we'll need to adjust the section
// offsets.
final headersSizeDiff = optionalHeader.headersSize - oldHeadersSize;
if (headersSizeDiff > 0) {
// Safety check that section virtual addresses need not be adjusted, as
// that requires rewriting much more of the fields and section contents.
// (Generally, the size of the headers is much smaller than the section
// alignment and so this is not expected to happen.)
if (size ~/ optionalHeader.sectionAlignment !=
oldHeadersSize ~/ optionalHeader.sectionAlignment) {
throw 'Adding the snapshot would require adjusting virtual addresses';
}
assert(headersSizeDiff % optionalHeader.fileAlignment == 0);
for (final entry in sectionTable.entries) {
entry.fileOffset += headersSizeDiff;
}
}
// Adjust the image size stored in the optional header, which must be a
// multiple of section alignment (as it is the size in memory, not on disk).
optionalHeader.imageSize = align(
newHeader.virtualAddress + newHeader.virtualSize,
optionalHeader.sectionAlignment);
}
Future<void> write(RandomAccessFile output) async {
await fileHeader.write(output);
await optionalHeader.write(output);
await sectionTable.write(output);
// Pad to the recorded headers size, which includes the MS-DOS stub.
final written = await output.position();
await output.writeFrom(Uint8List(size - written));
}
}
class PortableExecutable {
final Uint8List source;
final CoffHeaders headers;
final int sourceFileHeaderOffset;
final int sourceSectionContentsOffset;
PortableExecutable._(this.source, this.headers, this.sourceFileHeaderOffset,
this.sourceSectionContentsOffset);
static const _expectedPESignature = <int>[80, 69, 0, 0];
static const _offsetForPEOffset = 0x3c;
static Future<PortableExecutable> fromFile(File file) async {
final source = await file.readAsBytes();
final byteData = ByteData.sublistView(source);
final peOffset = byteData.getUint32(_offsetForPEOffset, Endian.little);
for (int i = 0; i < _expectedPESignature.length; i++) {
if (byteData.getUint8(peOffset + i) != _expectedPESignature[i]) {
throw 'Not a Portable Executable file';
}
}
final fileHeaderOffset = peOffset + _expectedPESignature.length;
final headers = CoffHeaders.fromTypedData(source, fileHeaderOffset);
final sectionContentsOffset = headers.size;
return PortableExecutable._(
source, headers, fileHeaderOffset, sectionContentsOffset);
}
Future<void> _fileAlignSectionEnd(RandomAccessFile output) async {
final current = await output.position();
final padding =
align(current, headers.optionalHeader.fileAlignment) - current;
await output.writeFrom(Uint8List(padding));
}
Future<void> appendSnapshotAndWrite(File output, File snapshot) async {
final stream = await output.open(mode: FileMode.write);
// Write MS-DOS stub.
await stream.writeFrom(source, 0, sourceFileHeaderOffset);
// Write headers with additional snapshot section.
final snapshotBytes = await snapshot.readAsBytes();
headers.addSnapshotSectionHeader(snapshotBytes.length);
await headers.write(stream);
// Write original section contents with alignment padding.
await stream.writeFrom(source, sourceSectionContentsOffset);
await _fileAlignSectionEnd(stream);
// Write snapshot with alignment padding.
await stream.writeFrom(snapshotBytes);
await _fileAlignSectionEnd(stream);
await stream.close();
}
}
// Writes an "appended" dart runtime + script snapshot file in a format
// compatible with Portable Executable files.
Future writeAppendedPortableExecutable(
String dartaotruntimePath, String payloadPath, String outputPath) async {
File originalExecutableFile = File(dartaotruntimePath);
File newSegmentFile = File(payloadPath);
File outputFile = File(outputPath);
final pe = await PortableExecutable.fromFile(originalExecutableFile);
await pe.appendSnapshotAndWrite(outputFile, newSegmentFile);
}