Files
sdk/pkg/dart2native/lib/dart2native_pe.dart
Tess Strickland eff0832b0f [dart2native] Remove unnecessary error in PE creation.
After writing the new PE header and original section contents of the
dartaotruntime PE executable, pad to the expected file offset of the
new section being added, not just to file alignment.

Also fixes a case where the new section's file offset returned from appendSnapshotAndWrite could be incorrect if the header size changed, which causes the file offsets for all sections, including the new one,
to be updated.

TEST=pkg/dartdev/test/commands/compile_test.dart

Change-Id: I060176f6771e9138f8d1a99590d455fc76bb573e
Cq-Include-Trybots: luci.dart.try:pkg-win-release-try,pkg-win-release-arm64-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/410720
Reviewed-by: Slava Egorov <vegorov@google.com>
Commit-Queue: Tess Strickland <sstrickl@google.com>
2025-02-20 08:09:53 -08:00

365 lines
13 KiB
Dart

// 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 over = size % base;
if (over != 0) {
return size + (base - over);
}
return size;
}
class BytesBacked {
ByteData _data;
ByteData get data => _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._(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 const FormatException(
'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._(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 const FormatException(
'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 const FormatException('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._(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._(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 const FormatException(
'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 (var entryIndex = 0; entryIndex < size; entryIndex += _entrySize) {
yield CoffSectionHeader._(
ByteData.sublistView(data, entryIndex, entryIndex + _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;
// Add a section header for the new "snapshot" section with the given length.
//
// Returns the new section header.
CoffSectionHeader 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 StateError(
'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);
return newHeader;
}
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 (var i = 0; i < _expectedPESignature.length; i++) {
if (byteData.getUint8(peOffset + i) != _expectedPESignature[i]) {
throw ArgumentError('Not a Portable Executable file', 'file');
}
}
final fileHeaderOffset = peOffset + _expectedPESignature.length;
final headers = CoffHeaders.fromTypedData(source, fileHeaderOffset);
final sectionContentsOffset = headers.size;
return PortableExecutable._(
source, headers, fileHeaderOffset, sectionContentsOffset);
}
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();
final oldOffsetEnd = headers.sectionTable.offsetEnd;
final snapshotSectionHeader =
headers.addSnapshotSectionHeader(snapshotBytes.length);
await headers.write(stream);
// Write original section contents.
await stream.writeFrom(source, sourceSectionContentsOffset, oldOffsetEnd);
var currentOffset = await stream.position();
// Pad the original contents to the file offset of the new section.
final expectedSnapshotOffset = snapshotSectionHeader.fileOffset;
if (currentOffset < expectedSnapshotOffset) {
final padding = expectedSnapshotOffset - currentOffset;
await stream.writeFrom(Uint8List(padding));
currentOffset = await stream.position();
}
// Verify that snapshot section will start at the expected offset
// and throw an error otherwise.
if (expectedSnapshotOffset != currentOffset) {
throw StateError('Unexpected snapshot section offset: '
'expected $expectedSnapshotOffset, got $currentOffset');
}
// Write snapshot with alignment padding.
await stream.writeFrom(snapshotBytes);
currentOffset = await stream.position();
final padding = align(currentOffset, headers.optionalHeader.fileAlignment) -
currentOffset;
if (padding > 0) {
await stream.writeFrom(Uint8List(padding));
}
await stream.close();
}
}
// Writes an "appended" dart runtime + script snapshot file in a format
// compatible with Portable Executable files.
//
// WARNING: this method is used within google3, so don't try to refactor so
// [dartaotruntimePath] is a constant inside this file.
Future<void> writeAppendedPortableExecutable(
String dartaotruntimePath, String payloadPath, String outputPath) async {
final originalExecutableFile = File(dartaotruntimePath);
final newSegmentFile = File(payloadPath);
final outputFile = File(outputPath);
final pe = await PortableExecutable.fromFile(originalExecutableFile);
await pe.appendSnapshotAndWrite(outputFile, newSegmentFile);
}