File: PEWriter\PeWriter.cs
Web Access
Project: src\src\Compilers\Core\Portable\Microsoft.CodeAnalysis.csproj (Microsoft.CodeAnalysis)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Text;
using Microsoft.DiaSymReader;
using static Microsoft.CodeAnalysis.SigningUtilities;
using EmitContext = Microsoft.CodeAnalysis.Emit.EmitContext;
 
namespace Microsoft.Cci
{
    internal sealed class PeWritingException : Exception
    {
        public PeWritingException(Exception inner)
            : base(inner.Message, inner)
        { }
    }
 
    internal static class PeWriter
    {
        internal struct EmitBuilders
        {
            internal BlobBuilder IlBlobBuilder;
            internal PooledBlobBuilder? MappedFieldDataBlobBuilder;
            internal PooledBlobBuilder? ManagedResourceBlobBuilder;
            internal PooledBlobBuilder? PortableExecutableBlobBuilder;
            internal PooledBlobBuilder? PortablePdbBlobBuilder;
 
            public EmitBuilders()
            {
                IlBlobBuilder = new BlobBuilder(32 * 1024);
                MappedFieldDataBlobBuilder = null;
                ManagedResourceBlobBuilder = null;
                PortableExecutableBlobBuilder = null;
                PortablePdbBlobBuilder = null;
            }
 
            internal void Free()
            {
                // There is a bug in LinkSuffix / LinkPrefix which causes the ownership to not
                // transfer when these have Count of 0. To avoid this problem we should not be
                // creating these builders unless we will actually put content into them.
                //
                // https://github.com/dotnet/runtime/issues/99266
                Debug.Assert(ManagedResourceBlobBuilder == null || ManagedResourceBlobBuilder.Count > 0);
                Debug.Assert(MappedFieldDataBlobBuilder == null || MappedFieldDataBlobBuilder.Count > 0);
 
                if (PortableExecutableBlobBuilder is null)
                {
                    MappedFieldDataBlobBuilder?.Free();
                    ManagedResourceBlobBuilder?.Free();
                }
                else
                {
                    // Once PortableExecutableBuilder is created it becomes the owner of the 
                    // MappedFieldDataBuilder and ManagedResourceBuilder instances. Freeing 
                    // it is sufficient to free both of them.
                    PortableExecutableBlobBuilder.Free();
                }
 
                PortablePdbBlobBuilder?.Free();
            }
        }
 
        internal static bool WritePeToStream(
            EmitContext context,
            CommonMessageProvider messageProvider,
            Func<Stream?> getPeStream,
            Func<Stream?>? getPortablePdbStreamOpt,
            PdbWriter? nativePdbWriterOpt,
            string? pdbPathOpt,
            bool metadataOnly,
            bool isDeterministic,
            bool emitTestCoverageData,
            RSAParameters? privateKeyOpt,
            CancellationToken cancellationToken)
        {
            // If PDB writer is given, we have to have PDB path.
            Debug.Assert(nativePdbWriterOpt == null || pdbPathOpt != null);
 
            var mdWriter = FullMetadataWriter.Create(context, messageProvider, metadataOnly, isDeterministic,
                emitTestCoverageData, getPortablePdbStreamOpt != null, cancellationToken);
 
            var properties = context.Module.SerializationProperties;
 
            context.Module.TestData?.SetMetadataWriter(mdWriter);
            nativePdbWriterOpt?.SetMetadataEmitter(mdWriter);
 
            // Since we are producing a full assembly, we should not have a module version ID
            // imposed ahead-of time. Instead we will compute a deterministic module version ID
            // based on the contents of the generated stream.
            Debug.Assert(properties.PersistentIdentifier == default(Guid));
 
            var emitBuilders = new EmitBuilders();
            Blob mvidFixup, mvidStringFixup;
            mdWriter.BuildMetadataAndIL(
                nativePdbWriterOpt,
                emitBuilders.IlBlobBuilder,
                out emitBuilders.MappedFieldDataBlobBuilder,
                out emitBuilders.ManagedResourceBlobBuilder,
                out mvidFixup,
                out mvidStringFixup);
 
            MethodDefinitionHandle entryPointHandle;
            MethodDefinitionHandle debugEntryPointHandle;
            mdWriter.GetEntryPoints(out entryPointHandle, out debugEntryPointHandle);
 
            if (!debugEntryPointHandle.IsNil)
            {
                nativePdbWriterOpt?.SetEntryPoint(MetadataTokens.GetToken(debugEntryPointHandle));
            }
 
            if (nativePdbWriterOpt != null)
            {
                if (context.Module.SourceLinkStreamOpt != null)
                {
                    nativePdbWriterOpt.EmbedSourceLink(context.Module.SourceLinkStreamOpt);
                }
 
                if (mdWriter.Module.OutputKind == OutputKind.WindowsRuntimeMetadata)
                {
                    // Dev12: If compiling to winmdobj, we need to add to PDB source spans of
                    //        all types and members for better error reporting by WinMDExp.
                    nativePdbWriterOpt.WriteDefinitionLocations(mdWriter.Module.GetSymbolToLocationMap());
                }
                else
                {
#if DEBUG
                    // validate that all definitions are writable
                    // if same scenario would happen in a winmdobj project
                    nativePdbWriterOpt.AssertAllDefinitionsHaveTokens(mdWriter.Module.GetSymbolToLocationMap());
#endif
                }
 
                nativePdbWriterOpt.WriteRemainingDebugDocuments(mdWriter.Module.DebugDocumentsBuilder.DebugDocuments);
 
                nativePdbWriterOpt.WriteCompilerVersion(context.Module.CommonCompilation.Language);
            }
 
            Stream? peStream = getPeStream();
            if (peStream == null)
            {
                emitBuilders.Free();
                return false;
            }
 
            BlobContentId pdbContentId = nativePdbWriterOpt?.GetContentId() ?? default;
 
            // the writer shall not be used after this point for writing:
            nativePdbWriterOpt = null;
 
            ushort portablePdbVersion = 0;
            var metadataRootBuilder = mdWriter.GetRootBuilder();
 
            var peHeaderBuilder = new PEHeaderBuilder(
                machine: properties.Machine,
                sectionAlignment: properties.SectionAlignment,
                fileAlignment: properties.FileAlignment,
                imageBase: properties.BaseAddress,
                majorLinkerVersion: properties.LinkerMajorVersion,
                minorLinkerVersion: properties.LinkerMinorVersion,
                majorOperatingSystemVersion: 4,
                minorOperatingSystemVersion: 0,
                majorImageVersion: 0,
                minorImageVersion: 0,
                majorSubsystemVersion: properties.MajorSubsystemVersion,
                minorSubsystemVersion: properties.MinorSubsystemVersion,
                subsystem: properties.Subsystem,
                dllCharacteristics: properties.DllCharacteristics,
                imageCharacteristics: properties.ImageCharacteristics,
                sizeOfStackReserve: properties.SizeOfStackReserve,
                sizeOfStackCommit: properties.SizeOfStackCommit,
                sizeOfHeapReserve: properties.SizeOfHeapReserve,
                sizeOfHeapCommit: properties.SizeOfHeapCommit);
 
            var peIdProvider = isDeterministic ?
                new Func<IEnumerable<Blob>, BlobContentId>(content => BlobContentId.FromHash(CryptographicHashProvider.ComputeSourceHash(content))) :
                null;
 
            // We need to calculate the PDB checksum, so we may as well use the calculated hash for PDB ID regardless of whether deterministic build is requested.
            var portablePdbContentHash = default(ImmutableArray<byte>);
 
            PooledBlobBuilder? portablePdbToEmbed = null;
            if (mdWriter.EmitPortableDebugMetadata)
            {
                mdWriter.AddRemainingDebugDocuments(mdWriter.Module.DebugDocumentsBuilder.DebugDocuments);
 
                // The algorithm must be specified for deterministic builds (checked earlier).
                Debug.Assert(!isDeterministic || context.Module.PdbChecksumAlgorithm.Name != null);
 
                var portablePdbIdProvider = (context.Module.PdbChecksumAlgorithm.Name != null) ?
                    new Func<IEnumerable<Blob>, BlobContentId>(content => BlobContentId.FromHash(portablePdbContentHash = CryptographicHashProvider.ComputeHash(context.Module.PdbChecksumAlgorithm, content))) :
                    null;
 
                emitBuilders.PortablePdbBlobBuilder = PooledBlobBuilder.GetInstance(zero: true);
                var portablePdbBuilder = mdWriter.GetPortablePdbBuilder(metadataRootBuilder.Sizes.RowCounts, debugEntryPointHandle, portablePdbIdProvider);
                pdbContentId = portablePdbBuilder.Serialize(emitBuilders.PortablePdbBlobBuilder);
                portablePdbVersion = portablePdbBuilder.FormatVersion;
 
                if (getPortablePdbStreamOpt == null)
                {
                    // embed to debug directory:
                    portablePdbToEmbed = emitBuilders.PortablePdbBlobBuilder;
                }
                else
                {
                    // write to Portable PDB stream:
                    Stream? portablePdbStream = getPortablePdbStreamOpt();
                    if (portablePdbStream != null)
                    {
                        try
                        {
                            emitBuilders.PortablePdbBlobBuilder.WriteContentTo(portablePdbStream);
                        }
                        catch (Exception e) when (!(e is OperationCanceledException))
                        {
                            throw new SymUnmanagedWriterException(e.Message, e);
                        }
                    }
                }
            }
 
            DebugDirectoryBuilder? debugDirectoryBuilder;
            if (pdbPathOpt != null || isDeterministic || portablePdbToEmbed != null)
            {
                debugDirectoryBuilder = new DebugDirectoryBuilder();
                if (pdbPathOpt != null)
                {
                    string paddedPath = isDeterministic ? pdbPathOpt : PadPdbPath(pdbPathOpt);
                    debugDirectoryBuilder.AddCodeViewEntry(paddedPath, pdbContentId, portablePdbVersion);
 
                    if (!portablePdbContentHash.IsDefault)
                    {
                        // Emit PDB Checksum entry for Portable and Embedded PDBs. The checksum is not as useful when the PDB is embedded, 
                        // however it allows the client to efficiently validate a standalone Portable PDB that 
                        // has been extracted from Embedded PDB and placed next to the PE file.
                        debugDirectoryBuilder.AddPdbChecksumEntry(context.Module.PdbChecksumAlgorithm.Name!, portablePdbContentHash);
                    }
                }
 
                if (isDeterministic)
                {
                    debugDirectoryBuilder.AddReproducibleEntry();
                }
 
                if (portablePdbToEmbed != null)
                {
                    debugDirectoryBuilder.AddEmbeddedPortablePdbEntry(portablePdbToEmbed, portablePdbVersion);
                }
            }
            else
            {
                debugDirectoryBuilder = null;
            }
 
            var strongNameProvider = context.Module.CommonCompilation.Options.StrongNameProvider;
            var corFlags = properties.CorFlags;
 
            var peBuilder = new ExtendedPEBuilder(
                peHeaderBuilder,
                metadataRootBuilder,
                emitBuilders.IlBlobBuilder,
                emitBuilders.MappedFieldDataBlobBuilder,
                emitBuilders.ManagedResourceBlobBuilder,
                CreateNativeResourceSectionSerializer(context.Module),
                debugDirectoryBuilder,
                CalculateStrongNameSignatureSize(context.Module, privateKeyOpt),
                entryPointHandle,
                corFlags,
                peIdProvider,
                metadataOnly && !context.IncludePrivateMembers);
 
            // This needs to force the backing builder to zero due to the issue writing COFF
            // headers. Can remove once this issue is fixed and we've moved to SRM with the 
            // fix
            // https://github.com/dotnet/runtime/issues/99244
            emitBuilders.PortableExecutableBlobBuilder = PooledBlobBuilder.GetInstance(zero: true);
            var peContentId = peBuilder.Serialize(emitBuilders.PortableExecutableBlobBuilder, out Blob mvidSectionFixup);
 
            PatchModuleVersionIds(mvidFixup, mvidSectionFixup, mvidStringFixup, peContentId.Guid);
 
            if (privateKeyOpt != null && corFlags.HasFlag(CorFlags.StrongNameSigned))
            {
                Debug.Assert(strongNameProvider != null);
                strongNameProvider.SignBuilder(peBuilder, emitBuilders.PortableExecutableBlobBuilder, privateKeyOpt.Value);
            }
 
            try
            {
                emitBuilders.PortableExecutableBlobBuilder.WriteContentTo(peStream);
            }
            catch (Exception e) when (!(e is OperationCanceledException))
            {
                throw new PeWritingException(e);
            }
 
            emitBuilders.Free();
            return true;
        }
 
        private static MethodInfo? s_calculateChecksumMethod;
 
        // internal for testing
        internal static uint CalculateChecksum(BlobBuilder peBlob, Blob checksumBlob)
        {
            if (s_calculateChecksumMethod == null)
            {
                s_calculateChecksumMethod = typeof(PEBuilder).GetRuntimeMethods()
                    .Where(m => m.Name == "CalculateChecksum" && m.GetParameters().Length == 2)
                    .Single();
            }
 
            return (uint)s_calculateChecksumMethod.Invoke(null, new object[]
            {
                peBlob,
                checksumBlob,
            })!;
        }
 
        private static void PatchModuleVersionIds(Blob guidFixup, Blob guidSectionFixup, Blob stringFixup, Guid mvid)
        {
            if (!guidFixup.IsDefault)
            {
                var writer = new BlobWriter(guidFixup);
                writer.WriteGuid(mvid);
                Debug.Assert(writer.RemainingBytes == 0);
            }
 
            if (!guidSectionFixup.IsDefault)
            {
                var writer = new BlobWriter(guidSectionFixup);
                writer.WriteGuid(mvid);
                Debug.Assert(writer.RemainingBytes == 0);
            }
 
            if (!stringFixup.IsDefault)
            {
                var writer = new BlobWriter(stringFixup);
                writer.WriteUserString(mvid.ToString());
                Debug.Assert(writer.RemainingBytes == 0);
            }
        }
 
        // Padding: We pad the path to this minimal size to
        // allow some tools to patch the path without the need to rewrite the entire image.
        // This is a workaround put in place until these tools are retired.
        private static string PadPdbPath(string path)
        {
            const int minLength = 260;
            return path + new string('\0', Math.Max(0, minLength - Encoding.UTF8.GetByteCount(path) - 1));
        }
 
        private static ResourceSectionBuilder? CreateNativeResourceSectionSerializer(CommonPEModuleBuilder module)
        {
            // Win32 resources are supplied to the compiler in one of two forms, .RES (the output of the resource compiler),
            // or .OBJ (the output of running cvtres.exe on a .RES file). A .RES file is parsed and processed into
            // a set of objects implementing IWin32Resources. These are then ordered and the final image form is constructed
            // and written to the resource section. Resources in .OBJ form are already very close to their final output
            // form. Rather than reading them and parsing them into a set of objects similar to those produced by
            // processing a .RES file, we process them like the native linker would, copy the relevant sections from
            // the .OBJ into our output and apply some fixups.
 
            var nativeResourceSectionOpt = module.Win32ResourceSection;
            if (nativeResourceSectionOpt != null)
            {
                return new ResourceSectionBuilderFromObj(nativeResourceSectionOpt);
            }
 
            var nativeResourcesOpt = module.Win32Resources;
            if (nativeResourcesOpt?.Any() == true)
            {
                return new ResourceSectionBuilderFromResources(nativeResourcesOpt);
            }
 
            var rawResourcesOpt = module.RawWin32Resources;
            if (rawResourcesOpt != null)
            {
                return new ResourceSectionBuilderFromRaw(rawResourcesOpt);
            }
 
            return null;
        }
 
        private sealed class ResourceSectionBuilderFromObj : ResourceSectionBuilder
        {
            private readonly ResourceSection _resourceSection;
 
            public ResourceSectionBuilderFromObj(ResourceSection resourceSection)
            {
                Debug.Assert(resourceSection != null);
                _resourceSection = resourceSection;
            }
 
            protected override void Serialize(BlobBuilder builder, SectionLocation location)
            {
                NativeResourceWriter.SerializeWin32Resources(builder, _resourceSection, location.RelativeVirtualAddress);
            }
        }
 
        private sealed class ResourceSectionBuilderFromResources : ResourceSectionBuilder
        {
            private readonly IEnumerable<IWin32Resource> _resources;
 
            public ResourceSectionBuilderFromResources(IEnumerable<IWin32Resource> resources)
            {
                Debug.Assert(resources.Any());
                _resources = resources;
            }
 
            protected override void Serialize(BlobBuilder builder, SectionLocation location)
            {
                NativeResourceWriter.SerializeWin32Resources(builder, _resources, location.RelativeVirtualAddress);
            }
        }
 
        private sealed class ResourceSectionBuilderFromRaw : ResourceSectionBuilder
        {
            private readonly Stream _resources;
            public ResourceSectionBuilderFromRaw(Stream resources)
            {
                _resources = resources;
            }
 
            protected override void Serialize(BlobBuilder builder, SectionLocation location)
            {
                int value;
                while ((value = _resources.ReadByte()) >= 0)
                {
                    builder.WriteByte((byte)value);
                }
            }
        }
    }
}