// dnlib: See LICENSE.txt for more info using System; using System.Collections.Generic; using System.IO; using dnlib.IO; using dnlib.DotNet.Pdb; using dnlib.PE; using dnlib.W32Resources; using dnlib.DotNet.MD; using System.Diagnostics; using dnlib.DotNet.Pdb.WindowsPdb; using System.Text; using System.IO.Compression; using System.Runtime.InteropServices; namespace dnlib.DotNet.Writer { /// /// Module writer event args /// public readonly struct ModuleWriterEventArgs { /// /// Gets the writer ( or ) /// public ModuleWriterBase Writer { get; } /// /// Gets the event /// public ModuleWriterEvent Event { get; } /// /// Constructor /// /// Writer /// Event public ModuleWriterEventArgs(ModuleWriterBase writer, ModuleWriterEvent @event) { Writer = writer ?? throw new ArgumentNullException(nameof(writer)); Event = @event; } } /// /// Module writer progress event args /// public readonly struct ModuleWriterProgressEventArgs { /// /// Gets the writer ( or ) /// public ModuleWriterBase Writer { get; } /// /// Gets the progress, 0.0 - 1.0 /// public double Progress { get; } /// /// Constructor /// /// Writer /// Progress, 0.0 - 1.0 public ModuleWriterProgressEventArgs(ModuleWriterBase writer, double progress) { if (progress < 0 || progress > 1) throw new ArgumentOutOfRangeException(nameof(progress)); Writer = writer ?? throw new ArgumentNullException(nameof(writer)); Progress = progress; } } /// /// Content ID /// public readonly struct ContentId { /// /// Gets the GUID /// public readonly Guid Guid; /// /// Gets the timestamp /// public readonly uint Timestamp; /// /// Constructor /// /// Guid /// Timestamp public ContentId(Guid guid, uint timestamp) { Guid = guid; Timestamp = timestamp; } } /// /// Event handler /// /// Event args type /// Sender /// Event args public delegate void EventHandler2(object sender, TEventArgs e); /// /// PDB writer options /// [Flags] public enum PdbWriterOptions { /// /// No bit is set /// None = 0, /// /// Don't use Microsoft.DiaSymReader.Native. This is a NuGet package with an updated Windows PDB reader/writer implementation, /// and if it's available at runtime, dnlib will try to use it. If this option is set, dnlib won't use it. /// You have to add a reference to the NuGet package if you want to use it, dnlib has no reference to the NuGet package. /// /// This is only used if it's a Windows PDB file. /// NoDiaSymReader = 0x00000001, /// /// Don't use diasymreader.dll's PDB writer that is shipped with .NET Framework. /// /// This is only used if it's a Windows PDB file. /// NoOldDiaSymReader = 0x00000002, /// /// Create a deterministic PDB file and add a debug directory entry to the PE file. /// /// It's ignored if the PDB writer doesn't support it. /// Deterministic = 0x00000004, /// /// Hash the PDB file and add a PDB checksum debug directory entry to the PE file. /// /// It's ignored if the PDB writer doesn't support it. /// PdbChecksum = 0x00000008, } /// /// Common module writer options base class /// public class ModuleWriterOptionsBase { PEHeadersOptions peHeadersOptions; Cor20HeaderOptions cor20HeaderOptions; MetadataOptions metadataOptions; ILogger logger; ILogger metadataLogger; bool noWin32Resources; Win32Resources win32Resources; StrongNameKey strongNameKey; StrongNamePublicKey strongNamePublicKey; bool delaySign; /// /// Raised at various times when writing the file. The listener has a chance to modify /// the file, eg. add extra metadata, encrypt methods, etc. /// public event EventHandler2 WriterEvent; internal void RaiseEvent(object sender, ModuleWriterEventArgs e) => WriterEvent?.Invoke(sender, e); /// /// Raised when the progress is updated /// public event EventHandler2 ProgressUpdated; internal void RaiseEvent(object sender, ModuleWriterProgressEventArgs e) => ProgressUpdated?.Invoke(sender, e); /// /// Gets/sets the logger. If this is null, any errors result in a /// being thrown. To disable this behavior, either /// create your own logger or use . /// public ILogger Logger { get => logger; set => logger = value; } /// /// Gets/sets the writer logger. If this is null, use /// . /// public ILogger MetadataLogger { get => metadataLogger; set => metadataLogger = value; } /// /// Gets/sets the options. This is never null. /// public PEHeadersOptions PEHeadersOptions { get => peHeadersOptions ??= new PEHeadersOptions(); set => peHeadersOptions = value; } /// /// Gets/sets the options. This is never null. /// public Cor20HeaderOptions Cor20HeaderOptions { get => cor20HeaderOptions ??= new Cor20HeaderOptions(); set => cor20HeaderOptions = value; } /// /// Gets/sets the options. This is never null. /// public MetadataOptions MetadataOptions { get => metadataOptions ??= new MetadataOptions(); set => metadataOptions = value; } /// /// If true, Win32 resources aren't written to the output /// public bool NoWin32Resources { get => noWin32Resources; set => noWin32Resources = value; } /// /// Gets/sets the Win32 resources. If this is null, use the module's /// Win32 resources if any. /// public Win32Resources Win32Resources { get => win32Resources; set => win32Resources = value; } /// /// true to delay sign the assembly. Initialize to the /// public key to use, and don't initialize . To generate the /// public key from your strong name key file, execute sn -p mykey.snk mypublickey.snk /// public bool DelaySign { get => delaySign; set => delaySign = value; } /// /// Gets/sets the strong name key. When you enhance strong name sign an assembly, /// this instance's HashAlgorithm must be initialized to its public key's HashAlgorithm. /// You should call /// to initialize this property if you use normal strong name signing. /// You should call /// or /// to initialize this property if you use enhanced strong name signing. /// public StrongNameKey StrongNameKey { get => strongNameKey; set => strongNameKey = value; } /// /// Gets/sets the new public key that should be used. If this is null, use /// the public key generated from . If it is also null, /// use the module's Assembly's public key. /// You should call /// or /// to initialize this property if you use enhanced strong name signing. /// public StrongNamePublicKey StrongNamePublicKey { get => strongNamePublicKey; set => strongNamePublicKey = value; } /// /// true if method bodies can be shared (two or more method bodies can share the /// same RVA), false if method bodies can't be shared. Don't enable it if there /// must be a 1:1 relationship with method bodies and their RVAs. /// This is enabled by default and results in smaller files. /// public bool ShareMethodBodies { get; set; } /// /// true if the PE header CheckSum field should be updated, false if the /// CheckSum field isn't updated. /// public bool AddCheckSum { get; set; } /// /// true if it's a 64-bit module, false if it's a 32-bit or AnyCPU module. /// public bool Is64Bit { get { if (!PEHeadersOptions.Machine.HasValue) return false; return PEHeadersOptions.Machine.Value.Is64Bit(); } } /// /// Gets/sets the module kind /// public ModuleKind ModuleKind { get; set; } /// /// true if it should be written as an EXE file, false if it should be /// written as a DLL file. /// public bool IsExeFile => ModuleKind != ModuleKind.Dll && ModuleKind != ModuleKind.NetModule; /// /// Set it to true to enable writing a PDB file. Default is false (a PDB file /// won't be written to disk). /// public bool WritePdb { get; set; } /// /// PDB writer options. This property is ignored if is false. /// public PdbWriterOptions PdbOptions { get; set; } /// /// PDB file name. If it's null a PDB file with the same name as the output assembly /// will be created but with a PDB extension. must be true or /// this property is ignored. /// public string PdbFileName { get; set; } /// /// PDB file name stored in the debug directory, or null to use /// public string PdbFileNameInDebugDirectory { get; set; } /// /// PDB stream. If this is initialized, then you should also set /// to the name of the PDB file since the file name must be written to the PE debug directory. /// must be true or this property is ignored. /// public Stream PdbStream { get; set; } /// /// Gets the PDB content id (portable PDBs). The argument is the PDB stream with the PDB ID zeroed out, /// and the 2nd argument is the default timestamp. /// This property is ignored if a deterministic PDB file is created or if the PDB checksum is calculated. /// public Func GetPdbContentId { get; set; } const ChecksumAlgorithm DefaultPdbChecksumAlgorithm = ChecksumAlgorithm.SHA256; /// /// PDB checksum algorithm /// public ChecksumAlgorithm PdbChecksumAlgorithm { get; set; } = DefaultPdbChecksumAlgorithm; /// /// true if an .mvid section should be added to the assembly. Not used by native module writer. /// public bool AddMvidSection { get; set; } /// /// Constructor /// /// The module protected ModuleWriterOptionsBase(ModuleDef module) { ShareMethodBodies = true; MetadataOptions.MetadataHeaderOptions.VersionString = module.RuntimeVersion; ModuleKind = module.Kind; PEHeadersOptions.Machine = module.Machine; PEHeadersOptions.Characteristics = module.Characteristics; PEHeadersOptions.DllCharacteristics = module.DllCharacteristics; if (module.Kind == ModuleKind.Windows) PEHeadersOptions.Subsystem = Subsystem.WindowsGui; else PEHeadersOptions.Subsystem = Subsystem.WindowsCui; PEHeadersOptions.NumberOfRvaAndSizes = 0x10; Cor20HeaderOptions.Flags = module.Cor20HeaderFlags; if (module.Assembly is not null && !PublicKeyBase.IsNullOrEmpty2(module.Assembly.PublicKey)) Cor20HeaderOptions.Flags |= ComImageFlags.StrongNameSigned; if (module.Cor20HeaderRuntimeVersion is not null) { Cor20HeaderOptions.MajorRuntimeVersion = (ushort)(module.Cor20HeaderRuntimeVersion.Value >> 16); Cor20HeaderOptions.MinorRuntimeVersion = (ushort)module.Cor20HeaderRuntimeVersion.Value; } else if (module.IsClr1x) { Cor20HeaderOptions.MajorRuntimeVersion = 2; Cor20HeaderOptions.MinorRuntimeVersion = 0; } else { Cor20HeaderOptions.MajorRuntimeVersion = 2; Cor20HeaderOptions.MinorRuntimeVersion = 5; } if (module.TablesHeaderVersion is not null) { MetadataOptions.TablesHeapOptions.MajorVersion = (byte)(module.TablesHeaderVersion.Value >> 8); MetadataOptions.TablesHeapOptions.MinorVersion = (byte)module.TablesHeaderVersion.Value; } else if (module.IsClr1x) { // Generics aren't supported MetadataOptions.TablesHeapOptions.MajorVersion = 1; MetadataOptions.TablesHeapOptions.MinorVersion = 0; } else { // Generics are supported MetadataOptions.TablesHeapOptions.MajorVersion = 2; MetadataOptions.TablesHeapOptions.MinorVersion = 0; } // Some tools crash if #GUID is missing so always create it by default MetadataOptions.Flags |= MetadataFlags.AlwaysCreateGuidHeap; var modDefMD = module as ModuleDefMD; if (modDefMD is not null) { var ntHeaders = modDefMD.Metadata.PEImage.ImageNTHeaders; PEHeadersOptions.TimeDateStamp = ntHeaders.FileHeader.TimeDateStamp; PEHeadersOptions.MajorLinkerVersion = ntHeaders.OptionalHeader.MajorLinkerVersion; PEHeadersOptions.MinorLinkerVersion = ntHeaders.OptionalHeader.MinorLinkerVersion; PEHeadersOptions.ImageBase = ntHeaders.OptionalHeader.ImageBase; PEHeadersOptions.MajorOperatingSystemVersion = ntHeaders.OptionalHeader.MajorOperatingSystemVersion; PEHeadersOptions.MinorOperatingSystemVersion = ntHeaders.OptionalHeader.MinorOperatingSystemVersion; PEHeadersOptions.MajorImageVersion = ntHeaders.OptionalHeader.MajorImageVersion; PEHeadersOptions.MinorImageVersion = ntHeaders.OptionalHeader.MinorImageVersion; PEHeadersOptions.MajorSubsystemVersion = ntHeaders.OptionalHeader.MajorSubsystemVersion; PEHeadersOptions.MinorSubsystemVersion = ntHeaders.OptionalHeader.MinorSubsystemVersion; PEHeadersOptions.Win32VersionValue = ntHeaders.OptionalHeader.Win32VersionValue; AddCheckSum = ntHeaders.OptionalHeader.CheckSum != 0; AddMvidSection = HasMvidSection(modDefMD.Metadata.PEImage.ImageSectionHeaders); if (HasDebugDirectoryEntry(modDefMD.Metadata.PEImage.ImageDebugDirectories, ImageDebugType.Reproducible)) PdbOptions |= PdbWriterOptions.Deterministic; if (HasDebugDirectoryEntry(modDefMD.Metadata.PEImage.ImageDebugDirectories, ImageDebugType.PdbChecksum)) PdbOptions |= PdbWriterOptions.PdbChecksum; if (TryGetPdbChecksumAlgorithm(modDefMD.Metadata.PEImage, modDefMD.Metadata.PEImage.ImageDebugDirectories, out var pdbChecksumAlgorithm)) PdbChecksumAlgorithm = pdbChecksumAlgorithm; MetadataOptions.TablesHeapOptions.Log2Rid = modDefMD.TablesStream.Log2Rid; } if (Is64Bit) { PEHeadersOptions.Characteristics &= ~Characteristics.Bit32Machine; PEHeadersOptions.Characteristics |= Characteristics.LargeAddressAware; } else if (modDefMD is null) PEHeadersOptions.Characteristics |= Characteristics.Bit32Machine; } static bool HasMvidSection(IList sections) { int count = sections.Count; for (int i = 0; i < count; i++) { var section = sections[i]; if (section.VirtualSize != 16) continue; var name = section.Name; // Roslyn ignores the last 2 bytes if (name[0] == '.' && name[1] == 'm' && name[2] == 'v' && name[3] == 'i' && name[4] == 'd' && name[5] == 0) return true; } return false; } static bool HasDebugDirectoryEntry(IList debugDirs, ImageDebugType type) { int count = debugDirs.Count; for (int i = 0; i < count; i++) { if (debugDirs[i].Type == type) return true; } return false; } static bool TryGetPdbChecksumAlgorithm(IPEImage peImage, IList debugDirs, out ChecksumAlgorithm pdbChecksumAlgorithm) { int count = debugDirs.Count; for (int i = 0; i < count; i++) { var dir = debugDirs[i]; if (dir.Type == ImageDebugType.PdbChecksum) { var reader = peImage.CreateReader(dir.AddressOfRawData, dir.SizeOfData); if (TryGetPdbChecksumAlgorithm(ref reader, out pdbChecksumAlgorithm)) return true; } } pdbChecksumAlgorithm = DefaultPdbChecksumAlgorithm; return false; } static bool TryGetPdbChecksumAlgorithm(ref DataReader reader, out ChecksumAlgorithm pdbChecksumAlgorithm) { try { var checksumName = reader.TryReadZeroTerminatedUtf8String(); if (Hasher.TryGetChecksumAlgorithm(checksumName, out pdbChecksumAlgorithm, out int checksumSize) && (uint)checksumSize == reader.BytesLeft) return true; } catch (IOException) { } catch (ArgumentException) { } pdbChecksumAlgorithm = DefaultPdbChecksumAlgorithm; return false; } /// /// Initializes and /// for normal strong name signing. /// /// Module /// Signature strong name key pair public void InitializeStrongNameSigning(ModuleDef module, StrongNameKey signatureKey) { StrongNameKey = signatureKey; StrongNamePublicKey = null; if (module.Assembly is not null) module.Assembly.CustomAttributes.RemoveAll("System.Reflection.AssemblySignatureKeyAttribute"); } /// /// Initializes and /// for enhanced strong name signing (without key migration). See /// http://msdn.microsoft.com/en-us/library/hh415055.aspx /// /// Module /// Signature strong name key pair /// Signature public key public void InitializeEnhancedStrongNameSigning(ModuleDef module, StrongNameKey signatureKey, StrongNamePublicKey signaturePubKey) { InitializeStrongNameSigning(module, signatureKey); StrongNameKey = StrongNameKey.WithHashAlgorithm(signaturePubKey.HashAlgorithm); } /// /// Initializes and /// for enhanced strong name signing (with key migration). See /// http://msdn.microsoft.com/en-us/library/hh415055.aspx /// /// Module /// Signature strong name key pair /// Signature public key /// Identity strong name key pair /// Identity public key public void InitializeEnhancedStrongNameSigning(ModuleDef module, StrongNameKey signatureKey, StrongNamePublicKey signaturePubKey, StrongNameKey identityKey, StrongNamePublicKey identityPubKey) { StrongNameKey = signatureKey.WithHashAlgorithm(signaturePubKey.HashAlgorithm); StrongNamePublicKey = identityPubKey; if (module.Assembly is not null) module.Assembly.UpdateOrCreateAssemblySignatureKeyAttribute(identityPubKey, identityKey, signaturePubKey); } } /// /// Module writer base class /// public abstract class ModuleWriterBase : ILogger { /// Default alignment of all constants protected internal const uint DEFAULT_CONSTANTS_ALIGNMENT = 8; /// Default alignment of all method bodies protected const uint DEFAULT_METHODBODIES_ALIGNMENT = 4; /// Default alignment of all .NET resources protected const uint DEFAULT_NETRESOURCES_ALIGNMENT = 4; /// Default alignment of the .NET metadata protected const uint DEFAULT_METADATA_ALIGNMENT = 4; /// Default Win32 resources alignment protected internal const uint DEFAULT_WIN32_RESOURCES_ALIGNMENT = 8; /// Default strong name signature alignment protected const uint DEFAULT_STRONGNAMESIG_ALIGNMENT = 4; /// Default COR20 header alignment protected const uint DEFAULT_COR20HEADER_ALIGNMENT = 4; /// See protected Stream destStream; /// See protected UniqueChunkList constants; /// See protected MethodBodyChunks methodBodies; /// See protected NetResources netResources; /// See protected Metadata metadata; /// See protected Win32ResourcesChunk win32Resources; /// Offset where the module is written. Usually 0. protected long destStreamBaseOffset; /// Debug directory protected DebugDirectory debugDirectory; string createdPdbFileName; /// /// Strong name signature /// protected StrongNameSignature strongNameSignature; /// /// Returns the module writer options /// public abstract ModuleWriterOptionsBase TheOptions { get; } /// /// Gets the destination stream /// public Stream DestinationStream => destStream; /// /// Gets the constants /// public UniqueChunkList Constants => constants; /// /// Gets the method bodies /// public MethodBodyChunks MethodBodies => methodBodies; /// /// Gets the .NET resources /// public NetResources NetResources => netResources; /// /// Gets the .NET metadata /// public Metadata Metadata => metadata; /// /// Gets the Win32 resources or null if there's none /// public Win32ResourcesChunk Win32Resources => win32Resources; /// /// Gets the strong name signature or null if there's none /// public StrongNameSignature StrongNameSignature => strongNameSignature; /// /// Gets all s. The reloc section must be the last section, so use if you need to append a section /// public abstract List Sections { get; } /// /// Adds to the sections list, but before the reloc section which must be last /// /// New section to add to the list public virtual void AddSection(PESection section) => Sections.Add(section); /// /// Gets the .text section /// public abstract PESection TextSection { get; } /// /// Gets the .rsrc section or null if there's none /// public abstract PESection RsrcSection { get; } /// /// Gets the debug directory or null if there's none /// public DebugDirectory DebugDirectory => debugDirectory; /// /// true if this is a , false if /// this is a . /// public bool IsNativeWriter => this is NativeModuleWriter; /// /// null if we're not writing a PDB /// PdbState pdbState; /// /// Writes the module to a file /// /// File name. The file will be truncated if it exists. public void Write(string fileName) { using (var dest = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite)) { dest.SetLength(0); try { Write(dest); } catch { // Writing failed. Delete the file since it's useless. dest.Close(); DeleteFileNoThrow(fileName); throw; } } } static void DeleteFileNoThrow(string fileName) { if (string.IsNullOrEmpty(fileName)) return; try { File.Delete(fileName); } catch { } } /// /// Writes the module to a /// /// Destination stream public void Write(Stream dest) { pdbState = TheOptions.WritePdb && Module.PdbState is not null ? Module.PdbState : null; if (TheOptions.DelaySign) { Debug.Assert(TheOptions.StrongNamePublicKey is not null, "Options.StrongNamePublicKey must be initialized when delay signing the assembly"); Debug.Assert(TheOptions.StrongNameKey is null, "Options.StrongNameKey must be null when delay signing the assembly"); TheOptions.Cor20HeaderOptions.Flags &= ~ComImageFlags.StrongNameSigned; } else if (TheOptions.StrongNameKey is not null || TheOptions.StrongNamePublicKey is not null) TheOptions.Cor20HeaderOptions.Flags |= ComImageFlags.StrongNameSigned; destStream = dest; destStreamBaseOffset = destStream.Position; OnWriterEvent(ModuleWriterEvent.Begin); var imageLength = WriteImpl(); destStream.Position = destStreamBaseOffset + imageLength; OnWriterEvent(ModuleWriterEvent.End); } /// /// Returns the module that is written /// public abstract ModuleDef Module { get; } /// /// Writes the module to . Event listeners and /// have been initialized when this method is called. /// /// Number of bytes written protected abstract long WriteImpl(); /// /// Creates the strong name signature if the module has one of the strong name flags /// set or wants to sign the assembly. /// protected void CreateStrongNameSignature() { if (TheOptions.DelaySign && TheOptions.StrongNamePublicKey is not null) { int len = TheOptions.StrongNamePublicKey.CreatePublicKey().Length - 0x20; strongNameSignature = new StrongNameSignature(len > 0 ? len : 0x80); } else if (TheOptions.StrongNameKey is not null) strongNameSignature = new StrongNameSignature(TheOptions.StrongNameKey.SignatureSize); else if (Module.Assembly is not null && !PublicKeyBase.IsNullOrEmpty2(Module.Assembly.PublicKey)) { int len = Module.Assembly.PublicKey.Data.Length - 0x20; strongNameSignature = new StrongNameSignature(len > 0 ? len : 0x80); } else if (((TheOptions.Cor20HeaderOptions.Flags ?? Module.Cor20HeaderFlags) & ComImageFlags.StrongNameSigned) != 0) strongNameSignature = new StrongNameSignature(0x80); } /// /// Creates the .NET metadata chunks (constants, method bodies, .NET resources, /// the metadata, and Win32 resources) /// /// protected void CreateMetadataChunks(ModuleDef module) { constants = new UniqueChunkList(); methodBodies = new MethodBodyChunks(TheOptions.ShareMethodBodies); netResources = new NetResources(DEFAULT_NETRESOURCES_ALIGNMENT); DebugMetadataKind debugKind; if (pdbState is not null && (pdbState.PdbFileKind == PdbFileKind.PortablePDB || pdbState.PdbFileKind == PdbFileKind.EmbeddedPortablePDB)) debugKind = DebugMetadataKind.Standalone; else debugKind = DebugMetadataKind.None; metadata = Metadata.Create(module, constants, methodBodies, netResources, TheOptions.MetadataOptions, debugKind); metadata.Logger = TheOptions.MetadataLogger ?? this; metadata.MetadataEvent += Metadata_MetadataEvent; metadata.ProgressUpdated += Metadata_ProgressUpdated; // StrongNamePublicKey is used if the user wants to override the assembly's // public key or when enhanced strong naming the assembly. var pk = TheOptions.StrongNamePublicKey; if (pk is not null) metadata.AssemblyPublicKey = pk.CreatePublicKey(); else if (TheOptions.StrongNameKey is not null) metadata.AssemblyPublicKey = TheOptions.StrongNameKey.PublicKey; var w32Resources = GetWin32Resources(); if (w32Resources is not null) win32Resources = new Win32ResourcesChunk(w32Resources); } /// /// Gets the Win32 resources that should be written to the new image or null if none /// protected abstract Win32Resources GetWin32Resources(); /// /// Calculates and of all s /// /// All chunks /// Starting file offset /// Starting RVA /// File alignment /// Section alignment protected void CalculateRvasAndFileOffsets(List chunks, FileOffset offset, RVA rva, uint fileAlignment, uint sectionAlignment) { int count = chunks.Count; var maxAlignment = Math.Min(fileAlignment, sectionAlignment); for (int i = 0; i < count; i++) { var chunk = chunks[i]; // We don't need to align to result of CalculateAlignment as all the chunks in `chunks` either // don't need a specific alignment or align themselves. var alignment = chunk.CalculateAlignment(); if (alignment > maxAlignment) Error("Chunk alignment is too big. Chunk: {0}, alignment: {1:X4}", chunk, alignment); chunk.SetOffset(offset, rva); // If it has zero size, it's not present in the file (eg. a section that wasn't needed) if (chunk.GetVirtualSize() != 0) { offset += chunk.GetFileLength(); rva += chunk.GetVirtualSize(); offset = offset.AlignUp(fileAlignment); rva = rva.AlignUp(sectionAlignment); } } } /// /// Writes all chunks to /// /// The writer /// All chunks /// File offset of first chunk /// File alignment protected void WriteChunks(DataWriter writer, List chunks, FileOffset offset, uint fileAlignment) { int count = chunks.Count; for (int i = 0; i < count; i++) { var chunk = chunks[i]; chunk.VerifyWriteTo(writer); // If it has zero size, it's not present in the file (eg. a section that wasn't needed) if (chunk.GetVirtualSize() != 0) { offset += chunk.GetFileLength(); var newOffset = offset.AlignUp(fileAlignment); writer.WriteZeroes((int)(newOffset - offset)); offset = newOffset; } } } /// /// Strong name sign the assembly /// /// Strong name signature offset protected void StrongNameSign(long snSigOffset) { var snSigner = new StrongNameSigner(destStream, destStreamBaseOffset); snSigner.WriteSignature(TheOptions.StrongNameKey, snSigOffset); } bool CanWritePdb() => pdbState is not null; /// /// Creates the debug directory if a PDB file should be written /// protected void CreateDebugDirectory() { if (CanWritePdb()) debugDirectory = new DebugDirectory(); } /// /// Write the PDB file. The caller should send the PDB events before and after calling this /// method. /// protected void WritePdbFile() { if (!CanWritePdb()) return; if (debugDirectory is null) throw new InvalidOperationException("debugDirectory is null but WritePdb is true"); if (pdbState is null) { Error("TheOptions.WritePdb is true but module has no PdbState"); return; } try { switch (pdbState.PdbFileKind) { case PdbFileKind.WindowsPDB: WriteWindowsPdb(pdbState); break; case PdbFileKind.PortablePDB: WritePortablePdb(pdbState, false); break; case PdbFileKind.EmbeddedPortablePDB: WritePortablePdb(pdbState, true); break; default: Error("Invalid PDB file kind {0}", pdbState.PdbFileKind); break; } } catch { DeleteFileNoThrow(createdPdbFileName); throw; } } void AddReproduciblePdbDebugDirectoryEntry() => debugDirectory.Add(Array2.Empty(), type: ImageDebugType.Reproducible, majorVersion: 0, minorVersion: 0, timeDateStamp: 0); void AddPdbChecksumDebugDirectoryEntry(byte[] checksumBytes, ChecksumAlgorithm checksumAlgorithm) { var stream = new MemoryStream(); var writer = new DataWriter(stream); var checksumName = Hasher.GetChecksumName(checksumAlgorithm); writer.WriteBytes(Encoding.UTF8.GetBytes(checksumName)); writer.WriteByte(0); writer.WriteBytes(checksumBytes); var blob = stream.ToArray(); debugDirectory.Add(blob, ImageDebugType.PdbChecksum, majorVersion: 1, minorVersion: 0, timeDateStamp: 0); } const uint PdbAge = 1; void WriteWindowsPdb(PdbState pdbState) { bool addPdbChecksumDebugDirectoryEntry = (TheOptions.PdbOptions & PdbWriterOptions.PdbChecksum) != 0; addPdbChecksumDebugDirectoryEntry = false;//TODO: If this is true, get the checksum from the PDB writer var symWriter = GetWindowsPdbSymbolWriter(TheOptions.PdbOptions, out var pdbFilename); if (symWriter is null) { Error("Could not create a PDB symbol writer. A Windows OS might be required."); return; } using (var pdbWriter = new WindowsPdbWriter(symWriter, pdbState, metadata)) { pdbWriter.Logger = TheOptions.Logger; pdbWriter.Write(); var pdbAge = PdbAge; bool hasContentId = pdbWriter.GetDebugInfo(TheOptions.PdbChecksumAlgorithm, ref pdbAge, out var pdbGuid, out uint stamp, out var idd, out var codeViewData); if (hasContentId) { debugDirectory.Add(GetCodeViewData(pdbGuid, pdbAge, TheOptions.PdbFileNameInDebugDirectory ?? pdbFilename), type: ImageDebugType.CodeView, majorVersion: 0, minorVersion: 0, timeDateStamp: stamp); } else { Debug.Fail("Failed to get the PDB content ID"); if (codeViewData is null) throw new InvalidOperationException(); var entry = debugDirectory.Add(codeViewData); entry.DebugDirectory = idd; entry.DebugDirectory.TimeDateStamp = GetTimeDateStamp(); } //TODO: Only do this if symWriter supports PDB checksums if (addPdbChecksumDebugDirectoryEntry) {}//TODO: AddPdbChecksumDebugDirectoryEntry(checksumBytes, TheOptions.PdbChecksumAlgorithm);, and verify that the order of the debug dir entries is the same as Roslyn created binaries if (symWriter.IsDeterministic) AddReproduciblePdbDebugDirectoryEntry(); } } /// /// Gets the timestamp stored in the PE header /// /// protected uint GetTimeDateStamp() { var td = TheOptions.PEHeadersOptions.TimeDateStamp; if (td.HasValue) return (uint)td; TheOptions.PEHeadersOptions.TimeDateStamp = PEHeadersOptions.CreateNewTimeDateStamp(); return (uint)TheOptions.PEHeadersOptions.TimeDateStamp; } SymbolWriter GetWindowsPdbSymbolWriter(PdbWriterOptions options, out string pdbFilename) { #if NETSTANDARD || NETCOREAPP if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { pdbFilename = null; return null; } #endif if (TheOptions.PdbStream is not null) { return Pdb.Dss.SymbolReaderWriterFactory.Create(options, TheOptions.PdbStream, pdbFilename = TheOptions.PdbFileName ?? GetStreamName(TheOptions.PdbStream) ?? GetDefaultPdbFileName()); } if (!string.IsNullOrEmpty(TheOptions.PdbFileName)) { createdPdbFileName = pdbFilename = TheOptions.PdbFileName; return Pdb.Dss.SymbolReaderWriterFactory.Create(options, createdPdbFileName); } createdPdbFileName = pdbFilename = GetDefaultPdbFileName(); if (createdPdbFileName is null) return null; return Pdb.Dss.SymbolReaderWriterFactory.Create(options, createdPdbFileName); } static string GetStreamName(Stream stream) => (stream as FileStream)?.Name; static string GetModuleName(ModuleDef module) { var name = module.Name ?? string.Empty; if (string.IsNullOrEmpty(name)) return null; if (name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) || name.EndsWith(".netmodule", StringComparison.OrdinalIgnoreCase)) return name; return name + ".pdb"; } string GetDefaultPdbFileName() { var destFileName = GetStreamName(destStream) ?? GetModuleName(Module); if (string.IsNullOrEmpty(destFileName)) { Error("TheOptions.WritePdb is true but it's not possible to guess the default PDB file name. Set PdbFileName to the name of the PDB file."); return null; } return Path.ChangeExtension(destFileName, "pdb"); } void WritePortablePdb(PdbState pdbState, bool isEmbeddedPortablePdb) { bool ownsStream = false; Stream pdbStream = null; try { MemoryStream embeddedMemoryStream = null; if (isEmbeddedPortablePdb) { pdbStream = embeddedMemoryStream = new MemoryStream(); ownsStream = true; } else pdbStream = GetStandalonePortablePdbStream(out ownsStream); if (pdbStream is null) throw new ModuleWriterException("Couldn't create a PDB stream"); var pdbFilename = TheOptions.PdbFileName ?? GetStreamName(pdbStream) ?? GetDefaultPdbFileName(); if (isEmbeddedPortablePdb) pdbFilename = Path.GetFileName(pdbFilename); uint entryPointToken; if (pdbState.UserEntryPoint is null) entryPointToken = 0; else entryPointToken = new MDToken(Table.Method, metadata.GetRid(pdbState.UserEntryPoint)).Raw; metadata.WritePortablePdb(pdbStream, entryPointToken, out var pdbIdOffset); Guid pdbGuid; var pdbId = new byte[20]; var pdbIdWriter = new ArrayWriter(pdbId); uint codeViewTimestamp; byte[] checksumBytes; if ((TheOptions.PdbOptions & PdbWriterOptions.Deterministic) != 0 || (TheOptions.PdbOptions & PdbWriterOptions.PdbChecksum) != 0 || TheOptions.GetPdbContentId is null) { pdbStream.Position = 0; checksumBytes = Hasher.Hash(TheOptions.PdbChecksumAlgorithm, pdbStream, pdbStream.Length); if (checksumBytes.Length < 20) throw new ModuleWriterException("Checksum bytes length < 20"); RoslynContentIdProvider.GetContentId(checksumBytes, out pdbGuid, out codeViewTimestamp); } else { var contentId = TheOptions.GetPdbContentId(pdbStream, GetTimeDateStamp()); codeViewTimestamp = contentId.Timestamp; pdbGuid = contentId.Guid; checksumBytes = null; } pdbIdWriter.WriteBytes(pdbGuid.ToByteArray()); pdbIdWriter.WriteUInt32(codeViewTimestamp); Debug.Assert(pdbIdWriter.Position == pdbId.Length); pdbStream.Position = pdbIdOffset; pdbStream.Write(pdbId, 0, pdbId.Length); // NOTE: We add these directory entries in the same order as Roslyn seems to do: // - CodeView // - PdbChecksum // - Reproducible // - EmbeddedPortablePdb debugDirectory.Add(GetCodeViewData(pdbGuid, PdbAge, TheOptions.PdbFileNameInDebugDirectory ?? pdbFilename), type: ImageDebugType.CodeView, majorVersion: PortablePdbConstants.FormatVersion, minorVersion: PortablePdbConstants.PortableCodeViewVersionMagic, timeDateStamp: codeViewTimestamp); if (checksumBytes is not null) AddPdbChecksumDebugDirectoryEntry(checksumBytes, TheOptions.PdbChecksumAlgorithm); if ((TheOptions.PdbOptions & PdbWriterOptions.Deterministic) != 0) AddReproduciblePdbDebugDirectoryEntry(); if (isEmbeddedPortablePdb) { Debug.Assert(embeddedMemoryStream is not null); debugDirectory.Add(CreateEmbeddedPortablePdbBlob(embeddedMemoryStream), type: ImageDebugType.EmbeddedPortablePdb, majorVersion: PortablePdbConstants.FormatVersion, minorVersion: PortablePdbConstants.EmbeddedVersion, timeDateStamp: 0); } } finally { if (ownsStream && pdbStream is not null) pdbStream.Dispose(); } } static byte[] CreateEmbeddedPortablePdbBlob(MemoryStream portablePdbStream) { var compressedData = Compress(portablePdbStream); var data = new byte[4 + 4 + compressedData.Length]; var stream = new MemoryStream(data); var writer = new DataWriter(stream); writer.WriteInt32(0x4244504D);//"MPDB" writer.WriteUInt32((uint)portablePdbStream.Length); writer.WriteBytes(compressedData); Debug.Assert(stream.Position == data.Length); return data; } static byte[] Compress(MemoryStream sourceStream) { sourceStream.Position = 0; var destStream = new MemoryStream(); using (var deflate = new DeflateStream(destStream, CompressionMode.Compress)) { var source = sourceStream.ToArray(); deflate.Write(source, 0, source.Length); } return destStream.ToArray(); } static byte[] GetCodeViewData(Guid guid, uint age, string filename) { var stream = new MemoryStream(); var writer = new DataWriter(stream); writer.WriteInt32(0x53445352); writer.WriteBytes(guid.ToByteArray()); writer.WriteUInt32(age); writer.WriteBytes(Encoding.UTF8.GetBytes(filename)); writer.WriteByte(0); return stream.ToArray(); } Stream GetStandalonePortablePdbStream(out bool ownsStream) { if (TheOptions.PdbStream is not null) { ownsStream = false; return TheOptions.PdbStream; } if (!string.IsNullOrEmpty(TheOptions.PdbFileName)) createdPdbFileName = TheOptions.PdbFileName; else createdPdbFileName = GetDefaultPdbFileName(); if (createdPdbFileName is null) { ownsStream = false; return null; } ownsStream = true; return File.Create(createdPdbFileName); } void Metadata_MetadataEvent(object sender, MetadataWriterEventArgs e) { switch (e.Event) { case MetadataEvent.BeginCreateTables: OnWriterEvent(ModuleWriterEvent.MDBeginCreateTables); break; case MetadataEvent.AllocateTypeDefRids: OnWriterEvent(ModuleWriterEvent.MDAllocateTypeDefRids); break; case MetadataEvent.AllocateMemberDefRids: OnWriterEvent(ModuleWriterEvent.MDAllocateMemberDefRids); break; case MetadataEvent.MemberDefRidsAllocated: OnWriterEvent(ModuleWriterEvent.MDMemberDefRidsAllocated); break; case MetadataEvent.MemberDefsInitialized: OnWriterEvent(ModuleWriterEvent.MDMemberDefsInitialized); break; case MetadataEvent.BeforeSortTables: OnWriterEvent(ModuleWriterEvent.MDBeforeSortTables); break; case MetadataEvent.MostTablesSorted: OnWriterEvent(ModuleWriterEvent.MDMostTablesSorted); break; case MetadataEvent.MemberDefCustomAttributesWritten: OnWriterEvent(ModuleWriterEvent.MDMemberDefCustomAttributesWritten); break; case MetadataEvent.BeginAddResources: OnWriterEvent(ModuleWriterEvent.MDBeginAddResources); break; case MetadataEvent.EndAddResources: OnWriterEvent(ModuleWriterEvent.MDEndAddResources); break; case MetadataEvent.BeginWriteMethodBodies: OnWriterEvent(ModuleWriterEvent.MDBeginWriteMethodBodies); break; case MetadataEvent.EndWriteMethodBodies: OnWriterEvent(ModuleWriterEvent.MDEndWriteMethodBodies); break; case MetadataEvent.OnAllTablesSorted: OnWriterEvent(ModuleWriterEvent.MDOnAllTablesSorted); break; case MetadataEvent.EndCreateTables: OnWriterEvent(ModuleWriterEvent.MDEndCreateTables); break; default: Debug.Fail($"Unknown MD event: {e.Event}"); break; } } void Metadata_ProgressUpdated(object sender, MetadataProgressEventArgs e) => RaiseProgress(ModuleWriterEvent.MDBeginCreateTables, ModuleWriterEvent.MDEndCreateTables + 1, e.Progress); /// /// Raises a writer event /// /// Event protected void OnWriterEvent(ModuleWriterEvent evt) { RaiseProgress(evt, 0); TheOptions.RaiseEvent(this, new ModuleWriterEventArgs(this, evt)); } static readonly double[] eventToProgress = new double[(int)ModuleWriterEvent.End - (int)ModuleWriterEvent.Begin + 1 + 1] { 0, // Begin 0.00128048488389907,// PESectionsCreated 0.0524625293056615, // ChunksCreated 0.0531036610555682, // ChunksAddedToSections 0.0535679983835939, // MDBeginCreateTables 0.0547784058004697, // MDAllocateTypeDefRids 0.0558606342971218, // MDAllocateMemberDefRids 0.120553993799033, // MDMemberDefRidsAllocated 0.226210300699921, // MDMemberDefsInitialized 0.236002648477671, // MDBeforeSortTables 0.291089703426468, // MDMostTablesSorted 0.449919748849947, // MDMemberDefCustomAttributesWritten 0.449919985998736, // MDBeginAddResources 0.452716444513587, // MDEndAddResources 0.452716681662375, // MDBeginWriteMethodBodies 0.924922132195272, // MDEndWriteMethodBodies 0.931410404476231, // MDOnAllTablesSorted 0.931425463424305, // MDEndCreateTables 0.932072998191503, // BeginWritePdb 0.932175327893773, // EndWritePdb 0.932175446468167, // BeginCalculateRvasAndFileOffsets 0.954646479929387, // EndCalculateRvasAndFileOffsets 0.95492263969368, // BeginWriteChunks 0.980563166714175, // EndWriteChunks 0.980563403862964, // BeginStrongNameSign 0.980563403862964, // EndStrongNameSign 0.980563522437358, // BeginWritePEChecksum 0.999975573674777, // EndWritePEChecksum 1, // End 1,// An extra one so we can get the next base progress without checking the index }; void RaiseProgress(ModuleWriterEvent evt, double subProgress) => RaiseProgress(evt, evt + 1, subProgress); void RaiseProgress(ModuleWriterEvent evt, ModuleWriterEvent nextEvt, double subProgress) { subProgress = Math.Min(1, Math.Max(0, subProgress)); var baseProgress = eventToProgress[(int)evt]; var nextProgress = eventToProgress[(int)nextEvt]; var progress = baseProgress + (nextProgress - baseProgress) * subProgress; progress = Math.Min(1, Math.Max(0, progress)); TheOptions.RaiseEvent(this, new ModuleWriterProgressEventArgs(this, progress)); } ILogger GetLogger() => TheOptions.Logger ?? DummyLogger.ThrowModuleWriterExceptionOnErrorInstance; /// void ILogger.Log(object sender, LoggerEvent loggerEvent, string format, params object[] args) => GetLogger().Log(this, loggerEvent, format, args); /// bool ILogger.IgnoresEvent(LoggerEvent loggerEvent) => GetLogger().IgnoresEvent(loggerEvent); /// /// Logs an error message /// /// Format /// Format args protected void Error(string format, params object[] args) => GetLogger().Log(this, LoggerEvent.Error, format, args); /// /// Logs a warning message /// /// Format /// Format args protected void Warning(string format, params object[] args) => GetLogger().Log(this, LoggerEvent.Warning, format, args); } }