|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Reflection.Internal;
using System.Reflection.Metadata;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using System.Threading;
using ImmutableArrayExtensions = System.Linq.ImmutableArrayExtensions;
namespace System.Reflection.PortableExecutable
{
/// <summary>
/// Portable Executable format reader.
/// </summary>
/// <remarks>
/// The implementation is thread-safe, that is multiple threads can read data from the reader in parallel.
/// Disposal of the reader is not thread-safe (see <see cref="Dispose"/>).
/// </remarks>
public sealed partial class PEReader : IDisposable
{
/// <summary>
/// True if the PE image has been loaded into memory by the OS loader.
/// </summary>
public bool IsLoadedImage { get; }
// May be null in the event that the entire image is not
// deemed necessary and we have been instructed to read
// the image contents without being lazy.
//
// _lazyPEHeaders are not null in that case.
private MemoryBlockProvider? _peImage;
// If we read the data from the image lazily (peImage != null) we defer reading the PE headers.
private PEHeaders? _lazyPEHeaders;
private AbstractMemoryBlock? _lazyMetadataBlock;
private AbstractMemoryBlock? _lazyImageBlock;
private AbstractMemoryBlock?[]? _lazyPESectionBlocks;
/// <summary>
/// Creates a Portable Executable reader over a PE image stored in memory.
/// </summary>
/// <param name="peImage">Pointer to the start of the PE image.</param>
/// <param name="size">The size of the PE image.</param>
/// <exception cref="ArgumentNullException"><paramref name="peImage"/> is <see cref="IntPtr.Zero"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="size"/> is negative.</exception>
/// <remarks>
/// The memory is owned by the caller and not released on disposal of the <see cref="PEReader"/>.
/// The caller is responsible for keeping the memory alive and unmodified throughout the lifetime of the <see cref="PEReader"/>.
/// The content of the image is not read during the construction of the <see cref="PEReader"/>
/// </remarks>
public unsafe PEReader(byte* peImage, int size)
: this(peImage, size, isLoadedImage: false)
{
}
/// <summary>
/// Creates a Portable Executable reader over a PE image stored in memory.
/// </summary>
/// <param name="peImage">Pointer to the start of the PE image.</param>
/// <param name="size">The size of the PE image.</param>
/// <param name="isLoadedImage">True if the PE image has been loaded into memory by the OS loader.</param>
/// <exception cref="ArgumentNullException"><paramref name="peImage"/> is <see cref="IntPtr.Zero"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="size"/> is negative.</exception>
/// <remarks>
/// The memory is owned by the caller and not released on disposal of the <see cref="PEReader"/>.
/// The caller is responsible for keeping the memory alive and unmodified throughout the lifetime of the <see cref="PEReader"/>.
/// The content of the image is not read during the construction of the <see cref="PEReader"/>
/// </remarks>
public unsafe PEReader(byte* peImage, int size, bool isLoadedImage)
{
if (peImage is null)
{
Throw.ArgumentNull(nameof(peImage));
}
if (size < 0)
{
throw new ArgumentOutOfRangeException(nameof(size));
}
_peImage = new ExternalMemoryBlockProvider(peImage, size);
IsLoadedImage = isLoadedImage;
}
/// <summary>
/// Creates a Portable Executable reader over a PE image stored in a stream.
/// </summary>
/// <param name="peStream">PE image stream.</param>
/// <exception cref="ArgumentNullException"><paramref name="peStream"/> is null.</exception>
/// <remarks>
/// Ownership of the stream is transferred to the <see cref="PEReader"/> upon successful validation of constructor arguments. It will be
/// disposed by the <see cref="PEReader"/> and the caller must not manipulate it.
/// </remarks>
public PEReader(Stream peStream)
: this(peStream, PEStreamOptions.Default)
{
}
/// <summary>
/// Creates a Portable Executable reader over a PE image stored in a stream beginning at its current position and ending at the end of the stream.
/// </summary>
/// <param name="peStream">PE image stream.</param>
/// <param name="options">
/// Options specifying how sections of the PE image are read from the stream.
///
/// Unless <see cref="PEStreamOptions.LeaveOpen"/> is specified, ownership of the stream is transferred to the <see cref="PEReader"/>
/// upon successful argument validation. It will be disposed by the <see cref="PEReader"/> and the caller must not manipulate it.
///
/// Unless <see cref="PEStreamOptions.PrefetchMetadata"/> or <see cref="PEStreamOptions.PrefetchEntireImage"/> is specified no data
/// is read from the stream during the construction of the <see cref="PEReader"/>. Furthermore, the stream must not be manipulated
/// by caller while the <see cref="PEReader"/> is alive and undisposed.
///
/// If <see cref="PEStreamOptions.PrefetchMetadata"/> or <see cref="PEStreamOptions.PrefetchEntireImage"/>, the <see cref="PEReader"/>
/// will have read all of the data requested during construction. As such, if <see cref="PEStreamOptions.LeaveOpen"/> is also
/// specified, the caller retains full ownership of the stream and is assured that it will not be manipulated by the <see cref="PEReader"/>
/// after construction.
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="peStream"/> is null.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="options"/> has an invalid value.</exception>
/// <exception cref="IOException">Error reading from the stream (only when prefetching data).</exception>
/// <exception cref="BadImageFormatException"><see cref="PEStreamOptions.PrefetchMetadata"/> is specified and the PE headers of the image are invalid.</exception>
public PEReader(Stream peStream, PEStreamOptions options)
: this(peStream, options, 0)
{
}
/// <summary>
/// Creates a Portable Executable reader over a PE image of the given size beginning at the stream's current position.
/// </summary>
/// <param name="peStream">PE image stream.</param>
/// <param name="size">PE image size.</param>
/// <param name="options">
/// Options specifying how sections of the PE image are read from the stream.
///
/// Unless <see cref="PEStreamOptions.LeaveOpen"/> is specified, ownership of the stream is transferred to the <see cref="PEReader"/>
/// upon successful argument validation. It will be disposed by the <see cref="PEReader"/> and the caller must not manipulate it.
///
/// Unless <see cref="PEStreamOptions.PrefetchMetadata"/> or <see cref="PEStreamOptions.PrefetchEntireImage"/> is specified no data
/// is read from the stream during the construction of the <see cref="PEReader"/>. Furthermore, the stream must not be manipulated
/// by caller while the <see cref="PEReader"/> is alive and undisposed.
///
/// If <see cref="PEStreamOptions.PrefetchMetadata"/> or <see cref="PEStreamOptions.PrefetchEntireImage"/>, the <see cref="PEReader"/>
/// will have read all of the data requested during construction. As such, if <see cref="PEStreamOptions.LeaveOpen"/> is also
/// specified, the caller retains full ownership of the stream and is assured that it will not be manipulated by the <see cref="PEReader"/>
/// after construction.
/// </param>
/// <exception cref="ArgumentOutOfRangeException">Size is negative or extends past the end of the stream.</exception>
/// <exception cref="IOException">Error reading from the stream (only when prefetching data).</exception>
/// <exception cref="BadImageFormatException"><see cref="PEStreamOptions.PrefetchMetadata"/> is specified and the PE headers of the image are invalid.</exception>
public unsafe PEReader(Stream peStream, PEStreamOptions options, int size)
{
if (peStream is null)
{
Throw.ArgumentNull(nameof(peStream));
}
if (!peStream.CanRead || !peStream.CanSeek)
{
throw new ArgumentException(SR.StreamMustSupportReadAndSeek, nameof(peStream));
}
if (!options.IsValid())
{
throw new ArgumentOutOfRangeException(nameof(options));
}
IsLoadedImage = (options & PEStreamOptions.IsLoadedImage) != 0;
long start = peStream.Position;
int actualSize = StreamExtensions.GetAndValidateSize(peStream, size, nameof(peStream));
bool closeStream = true;
try
{
if ((options & (PEStreamOptions.PrefetchMetadata | PEStreamOptions.PrefetchEntireImage)) == 0)
{
_peImage = new StreamMemoryBlockProvider(peStream, start, actualSize, (options & PEStreamOptions.LeaveOpen) != 0);
closeStream = false;
}
else
{
// Read in the entire image or metadata blob:
if ((options & PEStreamOptions.PrefetchEntireImage) != 0)
{
var imageBlock = StreamMemoryBlockProvider.ReadMemoryBlockNoLock(peStream, start, actualSize);
_lazyImageBlock = imageBlock;
_peImage = new ExternalMemoryBlockProvider(imageBlock.Pointer, imageBlock.Size);
// if the caller asked for metadata initialize the PE headers (calculates metadata offset):
if ((options & PEStreamOptions.PrefetchMetadata) != 0)
{
InitializePEHeaders();
}
}
else
{
// The peImage is left null, but the lazyMetadataBlock is initialized up front.
_lazyPEHeaders = new PEHeaders(peStream, actualSize, IsLoadedImage);
if (_lazyPEHeaders.MetadataStartOffset != -1)
{
_lazyMetadataBlock = StreamMemoryBlockProvider.ReadMemoryBlockNoLock(peStream, _lazyPEHeaders.MetadataStartOffset, _lazyPEHeaders.MetadataSize);
}
}
// We read all we need, the stream is going to be closed.
}
}
finally
{
if (closeStream && (options & PEStreamOptions.LeaveOpen) == 0)
{
peStream.Dispose();
}
}
}
/// <summary>
/// Creates a Portable Executable reader over a PE image stored in a byte array.
/// </summary>
/// <param name="peImage">PE image.</param>
/// <remarks>
/// The content of the image is not read during the construction of the <see cref="PEReader"/>
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="peImage"/> is null.</exception>
public PEReader(ImmutableArray<byte> peImage)
{
if (peImage.IsDefault)
{
Throw.ArgumentNull(nameof(peImage));
}
_peImage = new ByteArrayMemoryProvider(peImage);
}
/// <summary>
/// Disposes all memory allocated by the reader.
/// </summary>
/// <remarks>
/// <see cref="Dispose"/> can be called multiple times (but not in parallel).
/// It is not safe to call <see cref="Dispose"/> in parallel with any other operation on the <see cref="PEReader"/>
/// or reading from <see cref="PEMemoryBlock"/>s retrieved from the reader.
/// </remarks>
public void Dispose()
{
_lazyPEHeaders = null;
_peImage?.Dispose();
_peImage = null;
_lazyImageBlock?.Dispose();
_lazyImageBlock = null;
_lazyMetadataBlock?.Dispose();
_lazyMetadataBlock = null;
var peSectionBlocks = _lazyPESectionBlocks;
if (peSectionBlocks != null)
{
foreach (var block in peSectionBlocks)
{
block?.Dispose();
}
_lazyPESectionBlocks = null;
}
}
private MemoryBlockProvider GetPEImage()
{
var peImage = _peImage;
if (peImage == null)
{
if (_lazyPEHeaders == null)
{
Throw.PEReaderDisposed();
}
Throw.InvalidOperation_PEImageNotAvailable();
}
return peImage;
}
/// <summary>
/// Gets the PE headers.
/// </summary>
/// <exception cref="BadImageFormatException">The headers contain invalid data.</exception>
/// <exception cref="IOException">Error reading from the stream.</exception>
public PEHeaders PEHeaders
{
get
{
if (_lazyPEHeaders == null)
{
InitializePEHeaders();
Debug.Assert(_lazyPEHeaders != null);
}
return _lazyPEHeaders;
}
}
/// <exception cref="IOException">Error reading from the stream.</exception>
private void InitializePEHeaders()
{
StreamConstraints constraints;
Stream stream = GetPEImage().GetStream(out constraints);
PEHeaders headers;
if (constraints.GuardOpt != null)
{
lock (constraints.GuardOpt)
{
headers = ReadPEHeadersNoLock(stream, constraints.ImageStart, constraints.ImageSize, IsLoadedImage);
}
}
else
{
headers = ReadPEHeadersNoLock(stream, constraints.ImageStart, constraints.ImageSize, IsLoadedImage);
}
Interlocked.CompareExchange(ref _lazyPEHeaders, headers, null);
}
/// <exception cref="IOException">Error reading from the stream.</exception>
private static PEHeaders ReadPEHeadersNoLock(Stream stream, long imageStartPosition, int imageSize, bool isLoadedImage)
{
Debug.Assert(imageStartPosition >= 0 && imageStartPosition <= stream.Length);
stream.Seek(imageStartPosition, SeekOrigin.Begin);
return new PEHeaders(stream, imageSize, isLoadedImage);
}
/// <summary>
/// Returns a view of the entire image as a pointer and length.
/// </summary>
/// <exception cref="InvalidOperationException">PE image not available.</exception>
private AbstractMemoryBlock GetEntireImageBlock()
{
if (_lazyImageBlock == null)
{
var newBlock = GetPEImage().GetMemoryBlock();
if (Interlocked.CompareExchange(ref _lazyImageBlock, newBlock, null) != null)
{
// another thread created the block already, we need to dispose ours:
newBlock.Dispose();
}
}
return _lazyImageBlock;
}
/// <exception cref="IOException">IO error while reading from the underlying stream.</exception>
/// <exception cref="InvalidOperationException">PE image doesn't have metadata.</exception>
private AbstractMemoryBlock GetMetadataBlock()
{
if (!HasMetadata)
{
throw new InvalidOperationException(SR.PEImageDoesNotHaveMetadata);
}
if (_lazyMetadataBlock == null)
{
var newBlock = GetPEImage().GetMemoryBlock(PEHeaders.MetadataStartOffset, PEHeaders.MetadataSize);
if (Interlocked.CompareExchange(ref _lazyMetadataBlock, newBlock, null) != null)
{
// another thread created the block already, we need to dispose ours:
newBlock.Dispose();
}
}
return _lazyMetadataBlock;
}
/// <exception cref="IOException">IO error while reading from the underlying stream.</exception>
/// <exception cref="InvalidOperationException">PE image not available.</exception>
private AbstractMemoryBlock GetPESectionBlock(int index)
{
Debug.Assert(index >= 0 && index < PEHeaders.SectionHeaders.Length);
var peImage = GetPEImage();
if (_lazyPESectionBlocks == null)
{
Interlocked.CompareExchange(ref _lazyPESectionBlocks, new AbstractMemoryBlock[PEHeaders.SectionHeaders.Length], null);
}
AbstractMemoryBlock? existingBlock = Volatile.Read(ref _lazyPESectionBlocks[index]);
if (existingBlock != null)
{
return existingBlock;
}
AbstractMemoryBlock newBlock;
if (IsLoadedImage)
{
newBlock = peImage.GetMemoryBlock(
PEHeaders.SectionHeaders[index].VirtualAddress,
PEHeaders.SectionHeaders[index].VirtualSize);
}
else
{
// Virtual size can be smaller than size in the image
// since the size in the image is aligned.
// Trim the alignment.
//
// Virtual size can also be larger than size in the image.
// When loaded sizeInImage bytes are mapped from the image
// and the rest of the bytes are zeroed out.
// Only return data stored in the image.
int size = Math.Min(
PEHeaders.SectionHeaders[index].VirtualSize,
PEHeaders.SectionHeaders[index].SizeOfRawData);
newBlock = peImage.GetMemoryBlock(PEHeaders.SectionHeaders[index].PointerToRawData, size);
}
if (Interlocked.CompareExchange(ref _lazyPESectionBlocks[index], newBlock, null) != null)
{
// another thread created the block already, we need to dispose ours:
newBlock.Dispose();
}
return _lazyPESectionBlocks[index]!;
}
/// <summary>
/// Return true if the reader can access the entire PE image.
/// </summary>
/// <remarks>
/// Returns false if the <see cref="PEReader"/> is constructed from a stream and only part of it is prefetched into memory.
/// </remarks>
public bool IsEntireImageAvailable => _lazyImageBlock != null || _peImage != null;
/// <summary>
/// Gets a pointer to and size of the PE image if available (<see cref="IsEntireImageAvailable"/>).
/// </summary>
/// <exception cref="InvalidOperationException">The entire PE image is not available.</exception>
public PEMemoryBlock GetEntireImage()
{
return new PEMemoryBlock(GetEntireImageBlock());
}
/// <summary>
/// Returns true if the PE image contains CLI metadata.
/// </summary>
/// <exception cref="BadImageFormatException">The PE headers contain invalid data.</exception>
/// <exception cref="IOException">Error reading from the underlying stream.</exception>
public bool HasMetadata
{
get { return PEHeaders.MetadataSize > 0; }
}
/// <summary>
/// Loads PE section that contains CLI metadata.
/// </summary>
/// <exception cref="InvalidOperationException">The PE image doesn't contain metadata (<see cref="HasMetadata"/> returns false).</exception>
/// <exception cref="BadImageFormatException">The PE headers contain invalid data.</exception>
/// <exception cref="IOException">IO error while reading from the underlying stream.</exception>
public PEMemoryBlock GetMetadata()
{
return new PEMemoryBlock(GetMetadataBlock());
}
/// <summary>
/// Loads PE section that contains the specified <paramref name="relativeVirtualAddress"/> into memory
/// and returns a memory block that starts at <paramref name="relativeVirtualAddress"/> and ends at the end of the containing section.
/// </summary>
/// <param name="relativeVirtualAddress">Relative Virtual Address of the data to read.</param>
/// <returns>
/// An empty block if <paramref name="relativeVirtualAddress"/> doesn't represent a location in any of the PE sections of this PE image.
/// </returns>
/// <exception cref="BadImageFormatException">The PE headers contain invalid data.</exception>
/// <exception cref="IOException">IO error while reading from the underlying stream.</exception>
/// <exception cref="InvalidOperationException">PE image not available.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="relativeVirtualAddress"/> is negative.</exception>
public PEMemoryBlock GetSectionData(int relativeVirtualAddress)
{
if (relativeVirtualAddress < 0)
{
Throw.ArgumentOutOfRange(nameof(relativeVirtualAddress));
}
int sectionIndex = PEHeaders.GetContainingSectionIndex(relativeVirtualAddress);
if (sectionIndex < 0)
{
return default(PEMemoryBlock);
}
var block = GetPESectionBlock(sectionIndex);
int relativeOffset = relativeVirtualAddress - PEHeaders.SectionHeaders[sectionIndex].VirtualAddress;
if (relativeOffset > block.Size)
{
return default(PEMemoryBlock);
}
return new PEMemoryBlock(block, relativeOffset);
}
/// <summary>
/// Loads PE section of the specified name into memory and returns a memory block that spans the section.
/// </summary>
/// <param name="sectionName">Name of the section.</param>
/// <returns>
/// An empty block if no section of the given <paramref name="sectionName"/> exists in this PE image.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="sectionName"/> is null.</exception>
/// <exception cref="InvalidOperationException">PE image not available.</exception>
public PEMemoryBlock GetSectionData(string sectionName)
{
if (sectionName is null)
{
Throw.ArgumentNull(nameof(sectionName));
}
int sectionIndex = PEHeaders.IndexOfSection(sectionName);
if (sectionIndex < 0)
{
return default(PEMemoryBlock);
}
return new PEMemoryBlock(GetPESectionBlock(sectionIndex));
}
/// <summary>
/// Reads all Debug Directory table entries.
/// </summary>
/// <exception cref="BadImageFormatException">Bad format of the entry.</exception>
/// <exception cref="IOException">IO error while reading from the underlying stream.</exception>
/// <exception cref="InvalidOperationException">PE image not available.</exception>
public ImmutableArray<DebugDirectoryEntry> ReadDebugDirectory()
{
Debug.Assert(PEHeaders.PEHeader != null);
var debugDirectory = PEHeaders.PEHeader.DebugTableDirectory;
if (debugDirectory.Size == 0)
{
return ImmutableArray<DebugDirectoryEntry>.Empty;
}
int position;
if (!PEHeaders.TryGetDirectoryOffset(debugDirectory, out position))
{
throw new BadImageFormatException(SR.InvalidDirectoryRVA);
}
if (debugDirectory.Size % DebugDirectoryEntry.Size != 0)
{
throw new BadImageFormatException(SR.InvalidDirectorySize);
}
using (AbstractMemoryBlock block = GetPEImage().GetMemoryBlock(position, debugDirectory.Size))
{
return ReadDebugDirectoryEntries(block.GetReader());
}
}
internal static ImmutableArray<DebugDirectoryEntry> ReadDebugDirectoryEntries(BlobReader reader)
{
int entryCount = reader.Length / DebugDirectoryEntry.Size;
var builder = ImmutableArray.CreateBuilder<DebugDirectoryEntry>(entryCount);
for (int i = 0; i < entryCount; i++)
{
// Reserved, must be zero.
int characteristics = reader.ReadInt32();
if (characteristics != 0)
{
throw new BadImageFormatException(SR.InvalidDebugDirectoryEntryCharacteristics);
}
uint stamp = reader.ReadUInt32();
ushort majorVersion = reader.ReadUInt16();
ushort minorVersion = reader.ReadUInt16();
var type = (DebugDirectoryEntryType)reader.ReadInt32();
int dataSize = reader.ReadInt32();
int dataRva = reader.ReadInt32();
int dataPointer = reader.ReadInt32();
builder.Add(new DebugDirectoryEntry(stamp, majorVersion, minorVersion, type, dataSize, dataRva, dataPointer));
}
return builder.MoveToImmutable();
}
private AbstractMemoryBlock GetDebugDirectoryEntryDataBlock(DebugDirectoryEntry entry)
{
int dataOffset = IsLoadedImage ? entry.DataRelativeVirtualAddress : entry.DataPointer;
return GetPEImage().GetMemoryBlock(dataOffset, entry.DataSize);
}
/// <summary>
/// Reads the data pointed to by the specified Debug Directory entry and interprets them as CodeView.
/// </summary>
/// <exception cref="ArgumentException"><paramref name="entry"/> is not a CodeView entry.</exception>
/// <exception cref="BadImageFormatException">Bad format of the data.</exception>
/// <exception cref="IOException">IO error while reading from the underlying stream.</exception>
/// <exception cref="InvalidOperationException">PE image not available.</exception>
public CodeViewDebugDirectoryData ReadCodeViewDebugDirectoryData(DebugDirectoryEntry entry)
{
if (entry.Type != DebugDirectoryEntryType.CodeView)
{
Throw.InvalidArgument(SR.Format(SR.UnexpectedDebugDirectoryType, nameof(DebugDirectoryEntryType.CodeView)), nameof(entry));
}
using (var block = GetDebugDirectoryEntryDataBlock(entry))
{
return DecodeCodeViewDebugDirectoryData(block);
}
}
// internal for testing
internal static CodeViewDebugDirectoryData DecodeCodeViewDebugDirectoryData(AbstractMemoryBlock block)
{
var reader = block.GetReader();
if (reader.ReadByte() != (byte)'R' ||
reader.ReadByte() != (byte)'S' ||
reader.ReadByte() != (byte)'D' ||
reader.ReadByte() != (byte)'S')
{
throw new BadImageFormatException(SR.UnexpectedCodeViewDataSignature);
}
Guid guid = reader.ReadGuid();
int age = reader.ReadInt32();
string path = reader.ReadUtf8NullTerminated();
return new CodeViewDebugDirectoryData(guid, age, path);
}
/// <summary>
/// Reads the data pointed to by the specified Debug Directory entry and interprets them as PDB Checksum entry.
/// </summary>
/// <exception cref="ArgumentException"><paramref name="entry"/> is not a PDB Checksum entry.</exception>
/// <exception cref="BadImageFormatException">Bad format of the data.</exception>
/// <exception cref="IOException">IO error while reading from the underlying stream.</exception>
/// <exception cref="InvalidOperationException">PE image not available.</exception>
public PdbChecksumDebugDirectoryData ReadPdbChecksumDebugDirectoryData(DebugDirectoryEntry entry)
{
if (entry.Type != DebugDirectoryEntryType.PdbChecksum)
{
Throw.InvalidArgument(SR.Format(SR.UnexpectedDebugDirectoryType, nameof(DebugDirectoryEntryType.PdbChecksum)), nameof(entry));
}
using (var block = GetDebugDirectoryEntryDataBlock(entry))
{
return DecodePdbChecksumDebugDirectoryData(block);
}
}
// internal for testing
internal static PdbChecksumDebugDirectoryData DecodePdbChecksumDebugDirectoryData(AbstractMemoryBlock block)
{
var reader = block.GetReader();
var algorithmName = reader.ReadUtf8NullTerminated();
byte[]? checksum = reader.ReadBytes(reader.RemainingBytes);
if (algorithmName.Length == 0 || checksum.Length == 0)
{
throw new BadImageFormatException(SR.InvalidPdbChecksumDataFormat);
}
return new PdbChecksumDebugDirectoryData(
algorithmName,
ImmutableCollectionsMarshal.AsImmutableArray(checksum));
}
/// <summary>
/// Opens a Portable PDB associated with this PE image.
/// </summary>
/// <param name="peImagePath">
/// The path to the PE image. The path is used to locate the PDB file located in the directory containing the PE file.
/// </param>
/// <param name="pdbFileStreamProvider">
/// If specified, called to open a <see cref="Stream"/> for a given file path.
/// The provider is expected to either return a readable and seekable <see cref="Stream"/>,
/// or <c>null</c> if the target file doesn't exist or should be ignored for some reason.
///
/// The provider shall throw <see cref="IOException"/> if it fails to open the file due to an unexpected IO error.
/// </param>
/// <param name="pdbReaderProvider">
/// If successful, a new instance of <see cref="MetadataReaderProvider"/> to be used to read the Portable PDB,.
/// </param>
/// <param name="pdbPath">
/// If successful and the PDB is found in a file, the path to the file. Returns <c>null</c> if the PDB is embedded in the PE image itself.
/// </param>
/// <returns>
/// True if the PE image has a PDB associated with it and the PDB has been successfully opened.
/// </returns>
/// <remarks>
/// Implements a simple PDB file lookup based on the content of the PE image Debug Directory.
/// A sophisticated tool might need to follow up with additional lookup on search paths or symbol server.
///
/// The method looks the PDB up in the following steps in the listed order:
/// 1) Check for a matching PDB file of the name found in the CodeView entry in the directory containing the PE file (the directory of <paramref name="peImagePath"/>).
/// 2) Check for a PDB embedded in the PE image itself.
///
/// The first PDB that matches the information specified in the Debug Directory is returned.
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="peImagePath"/> or <paramref name="pdbFileStreamProvider"/> is null.</exception>
/// <exception cref="InvalidOperationException">The stream returned from <paramref name="pdbFileStreamProvider"/> doesn't support read and seek operations.</exception>
/// <exception cref="BadImageFormatException">No matching PDB file is found due to an error: The PE image or the PDB is invalid.</exception>
/// <exception cref="IOException">No matching PDB file is found due to an error: An IO error occurred while reading the PE image or the PDB.</exception>
public bool TryOpenAssociatedPortablePdb(string peImagePath, Func<string, Stream?> pdbFileStreamProvider, out MetadataReaderProvider? pdbReaderProvider, out string? pdbPath)
{
if (peImagePath is null)
{
Throw.ArgumentNull(nameof(peImagePath));
}
if (pdbFileStreamProvider is null)
{
Throw.ArgumentNull(nameof(pdbFileStreamProvider));
}
pdbReaderProvider = null;
pdbPath = null;
string? peImageDirectory;
try
{
peImageDirectory = Path.GetDirectoryName(peImagePath);
}
catch (Exception e)
{
throw new ArgumentException(e.Message, nameof(peImagePath), e);
}
Exception? errorToReport = null;
var entries = ReadDebugDirectory();
// First try .pdb file specified in CodeView data (we prefer .pdb file on disk over embedded PDB
// since embedded PDB needs decompression which is less efficient than memory-mapping the file).
var codeViewEntry = ImmutableArrayExtensions.FirstOrDefault(entries, e => e.IsPortableCodeView);
if (codeViewEntry.DataSize != 0 &&
TryOpenCodeViewPortablePdb(codeViewEntry, peImageDirectory!, pdbFileStreamProvider, out pdbReaderProvider, out pdbPath, ref errorToReport))
{
return true;
}
// if it failed try Embedded Portable PDB (if available):
var embeddedPdbEntry = ImmutableArrayExtensions.FirstOrDefault(entries, e => e.Type == DebugDirectoryEntryType.EmbeddedPortablePdb);
if (embeddedPdbEntry.DataSize != 0)
{
bool openedEmbeddedPdb = false;
pdbReaderProvider = null;
TryOpenEmbeddedPortablePdb(embeddedPdbEntry, ref openedEmbeddedPdb, ref pdbReaderProvider, ref errorToReport);
if (openedEmbeddedPdb)
return true;
}
// Report any metadata and IO errors. PDB might exist but we couldn't read some metadata.
// The caller might chose to ignore the failure or report it to the user.
if (errorToReport != null)
{
Debug.Assert(errorToReport is BadImageFormatException || errorToReport is IOException);
ExceptionDispatchInfo.Capture(errorToReport).Throw();
}
return false;
}
private bool TryOpenCodeViewPortablePdb(DebugDirectoryEntry codeViewEntry, string peImageDirectory, Func<string, Stream?> pdbFileStreamProvider, out MetadataReaderProvider? provider, out string? pdbPath, ref Exception? errorToReport)
{
pdbPath = null;
provider = null;
CodeViewDebugDirectoryData data;
try
{
data = ReadCodeViewDebugDirectoryData(codeViewEntry);
}
catch (Exception e) when (e is BadImageFormatException || e is IOException)
{
errorToReport ??= e;
return false;
}
var id = new BlobContentId(data.Guid, codeViewEntry.Stamp);
// The interpretation os the path in the CodeView needs to be platform agnostic,
// so that PDBs built on Windows work on Unix-like systems and vice versa.
// System.IO.Path.GetFileName() on Unix-like systems doesn't treat '\' as a file name separator,
// so we need a custom implementation. Also avoid throwing an exception if the path contains invalid characters,
// they might not be invalid on the other platform. It's up to the FS APIs to deal with that when opening the stream.
string collocatedPdbPath = PathUtilities.CombinePathWithRelativePath(peImageDirectory, PathUtilities.GetFileName(data.Path));
if (TryOpenPortablePdbFile(collocatedPdbPath, id, pdbFileStreamProvider, out provider, ref errorToReport))
{
pdbPath = collocatedPdbPath;
return true;
}
return false;
}
private static bool TryOpenPortablePdbFile(string path, BlobContentId id, Func<string, Stream?> pdbFileStreamProvider, out MetadataReaderProvider? provider, ref Exception? errorToReport)
{
provider = null;
MetadataReaderProvider? candidate = null;
try
{
Stream? pdbStream;
try
{
pdbStream = pdbFileStreamProvider(path);
}
catch (FileNotFoundException)
{
// Not an unexpected IO exception, continue witout reporting the error.
pdbStream = null;
}
if (pdbStream == null)
{
return false;
}
if (!pdbStream.CanRead || !pdbStream.CanSeek)
{
throw new InvalidOperationException(SR.StreamMustSupportReadAndSeek);
}
candidate = MetadataReaderProvider.FromPortablePdbStream(pdbStream);
// Validate that the PDB matches the assembly version
if (new BlobContentId(candidate.GetMetadataReader().DebugMetadataHeader!.Id) != id)
{
return false;
}
provider = candidate;
return true;
}
catch (Exception e) when (e is BadImageFormatException || e is IOException)
{
errorToReport ??= e;
return false;
}
finally
{
if (provider == null)
{
candidate?.Dispose();
}
}
}
partial void TryOpenEmbeddedPortablePdb(DebugDirectoryEntry embeddedPdbEntry, ref bool openedEmbeddedPdb, ref MetadataReaderProvider? provider, ref Exception? errorToReport);
}
}
|