From 3392587509bf051141c2cc24202d99daf55aec4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Sat, 29 Aug 2026 18:48:28 -0400 Subject: [PATCH 1/4] Use value type for COFF relocations --- .../Compiler/ObjectWriter/CoffObjectWriter.cs | 58 ++- .../CoffObjectWriterTests.cs | 484 ++++++++++++++++++ .../ILCompiler.Compiler.Tests.csproj | 1 + .../ILCompiler.Compiler.csproj | 2 + 4 files changed, 518 insertions(+), 27 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.Compiler.Tests/CoffObjectWriterTests.cs diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/CoffObjectWriter.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/CoffObjectWriter.cs index 54ff003ae967de..60c949e0a3ed67 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/CoffObjectWriter.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/CoffObjectWriter.cs @@ -291,7 +291,10 @@ private protected override void EmitRelocations(int sectionIndex, List IMAGE_REL_I386_DIR32NB, IMAGE_REL_BASED_ADDR32NB => IMAGE_REL_I386_DIR32NB, @@ -313,19 +315,17 @@ private protected override void EmitRelocations(int sectionIndex, List IMAGE_REL_I386_SECREL, IMAGE_REL_SECTION => IMAGE_REL_I386_SECTION, _ => throw new NotSupportedException($"Unsupported relocation: {relocation.Type}") - }, - }); + })); } break; case Machine.Amd64: foreach (var relocation in relocationList) { - coffRelocations.Add(new CoffRelocation - { - VirtualAddress = (uint)relocation.Offset, - SymbolTableIndex = _symbolNameToIndex[relocation.SymbolName], - Type = relocation.Type switch + coffRelocations.Add(new CoffRelocation( + (uint)relocation.Offset, + _symbolNameToIndex[relocation.SymbolName], + relocation.Type switch { IMAGE_REL_BASED_ABSOLUTE => IMAGE_REL_AMD64_ADDR32NB, IMAGE_REL_BASED_ADDR32NB => IMAGE_REL_AMD64_ADDR32NB, @@ -336,19 +336,17 @@ private protected override void EmitRelocations(int sectionIndex, List IMAGE_REL_AMD64_SECREL, IMAGE_REL_SECTION => IMAGE_REL_AMD64_SECTION, _ => throw new NotSupportedException($"Unsupported relocation: {relocation.Type}") - }, - }); + })); } break; case Machine.Arm64: foreach (var relocation in relocationList) { - coffRelocations.Add(new CoffRelocation - { - VirtualAddress = (uint)relocation.Offset, - SymbolTableIndex = _symbolNameToIndex[relocation.SymbolName], - Type = relocation.Type switch + coffRelocations.Add(new CoffRelocation( + (uint)relocation.Offset, + _symbolNameToIndex[relocation.SymbolName], + relocation.Type switch { IMAGE_REL_BASED_ABSOLUTE => IMAGE_REL_ARM64_ADDR32NB, IMAGE_REL_BASED_ADDR32NB => IMAGE_REL_ARM64_ADDR32NB, @@ -365,8 +363,7 @@ private protected override void EmitRelocations(int sectionIndex, List IMAGE_REL_ARM64_SECREL, IMAGE_REL_SECTION => IMAGE_REL_ARM64_SECTION, _ => throw new NotSupportedException($"Unsupported relocation: {relocation.Type}") - }, - }); + })); } break; @@ -454,7 +451,7 @@ private protected override void EmitObjectFile(Stream outputFileStream) if (section.Relocations.Count > 0) { - foreach (var relocation in section.Relocations) + foreach (ref readonly CoffRelocation relocation in CollectionsMarshal.AsSpan(section.Relocations)) { relocation.Write(outputFileStream); } @@ -700,11 +697,18 @@ internal enum CoffRelocationType IMAGE_REL_ARM64_REL32 = 17, } - protected sealed class CoffRelocation + internal readonly struct CoffRelocation { - public uint VirtualAddress { get; set; } - public uint SymbolTableIndex { get; set; } - public CoffRelocationType Type { get; set; } + public CoffRelocation(uint virtualAddress, uint symbolTableIndex, CoffRelocationType type) + { + VirtualAddress = virtualAddress; + SymbolTableIndex = symbolTableIndex; + Type = type; + } + + public uint VirtualAddress { get; } + public uint SymbolTableIndex { get; } + public CoffRelocationType Type { get; } public const int Size = sizeof(uint) + // VirtualAddress diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/CoffObjectWriterTests.cs b/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/CoffObjectWriterTests.cs new file mode 100644 index 00000000000000..f502cedf00b4df --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/CoffObjectWriterTests.cs @@ -0,0 +1,484 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; + +using ILCompiler.DependencyAnalysis; +using ILCompiler.DependencyAnalysisFramework; +using ILCompiler.ObjectWriter; + +using Internal.IL; +using Internal.Text; +using Internal.TypeSystem; + +using Xunit; + +namespace ILCompiler.Compiler.Tests +{ + public class CoffObjectWriterTests + { + private const int CoffHeaderSize = 20; + private const int CoffRelocationSize = 10; + private const int CoffSectionHeaderSize = 40; + private const int CoffSymbolSize = 18; + private const uint ImageScnLnkNRelocOvfl = 0x01000000; + + private static readonly object s_emitLock = new object(); + private static readonly Dictionary s_nodeFactories = new Dictionary + { + { TargetArchitecture.X86, CreateNodeFactory(TargetArchitecture.X86) }, + { TargetArchitecture.X64, CreateNodeFactory(TargetArchitecture.X64) }, + { TargetArchitecture.ARM64, CreateNodeFactory(TargetArchitecture.ARM64) }, + }; + + [Fact] + public void RelocationUsesCompactValueLayout() + { + Assert.True(typeof(CoffObjectWriter.CoffRelocation).IsValueType); + Assert.Equal(12, Unsafe.SizeOf()); + } + + public static IEnumerable RelocationFieldValues() + { + var seenValues = new HashSet(); + foreach (CoffObjectWriter.CoffRelocationType type in Enum.GetValues()) + { + int typeValue = (int)type; + if (seenValues.Add(typeValue)) + { + yield return new object[] { 0u, 0u, typeValue }; + } + } + + yield return new object[] { uint.MaxValue, uint.MaxValue, ushort.MaxValue }; + } + + [Theory] + [MemberData(nameof(RelocationFieldValues))] + public void RelocationWritesExpectedBytes(uint virtualAddress, uint symbolTableIndex, int typeValue) + { + var relocation = new CoffObjectWriter.CoffRelocation( + virtualAddress, + symbolTableIndex, + (CoffObjectWriter.CoffRelocationType)typeValue); + + using var stream = new MemoryStream(); + relocation.Write(stream); + + byte[] expected = new byte[CoffRelocationSize]; + BinaryPrimitives.WriteUInt32LittleEndian(expected, virtualAddress); + BinaryPrimitives.WriteUInt32LittleEndian(expected.AsSpan(4), symbolTableIndex); + BinaryPrimitives.WriteUInt16LittleEndian(expected.AsSpan(8), (ushort)typeValue); + Assert.Equal(expected, stream.ToArray()); + } + + [Fact] + public void RelocationUsesCoffWireLayout() + { + var relocation = new CoffObjectWriter.CoffRelocation( + 0x11223344, + 0x55667788, + (CoffObjectWriter.CoffRelocationType)0x99AA); + + using var stream = new MemoryStream(); + relocation.Write(stream); + + Assert.Equal( + new byte[] { 0x44, 0x33, 0x22, 0x11, 0x88, 0x77, 0x66, 0x55, 0xAA, 0x99 }, + stream.ToArray()); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(3)] + public void EmitsExpectedRelocationCount(int relocationCount) + { + Relocation[] relocations = CreateRelocations(relocationCount, RelocType.IMAGE_REL_BASED_ADDR32NB); + byte[] objectBytes = EmitObject(new byte[sizeof(uint)], relocations); + CoffSection section = FindSection(objectBytes, ".rdata"); + + Assert.Equal(relocationCount, section.NumberOfRelocations); + + if (relocationCount == 0) + { + Assert.Equal(0u, section.PointerToRelocations); + } + else + { + Assert.NotEqual(0u, section.PointerToRelocations); + } + } + + [Fact] + public void ConvertsRelocationsWithoutChangingOrderOrFields() + { + Relocation[] relocations = + [ + CreateRelocation(0, RelocType.IMAGE_REL_BASED_ABSOLUTE, targetIndex: 0), + CreateRelocation(4, RelocType.IMAGE_REL_BASED_ADDR32NB, targetIndex: 1), + CreateRelocation(8, RelocType.IMAGE_REL_BASED_HIGHLOW, targetIndex: 0), + CreateRelocation(16, RelocType.IMAGE_REL_BASED_DIR64, targetIndex: 1), + CreateRelocation(24, RelocType.IMAGE_REL_BASED_REL32, targetIndex: 0), + CreateRelocation(28, RelocType.IMAGE_REL_BASED_RELPTR32, targetIndex: 1), + CreateRelocation(32, RelocType.IMAGE_REL_SECREL, targetIndex: 0), + CreateRelocation(36, RelocType.IMAGE_REL_SECTION, targetIndex: 1), + ]; + + byte[] first = EmitObject(new byte[40], relocations, targetCount: 2, sourceSectionPrefixSize: 13); + byte[] second = EmitObject(new byte[40], relocations, targetCount: 2, sourceSectionPrefixSize: 13); + Assert.Equal(first, second); + + CoffSection section = FindSection(first, ".rdata"); + Assert.Equal(relocations.Length, section.NumberOfRelocations); + + uint[] expectedAddresses = [16, 20, 24, 32, 40, 44, 48, 52]; + uint[] expectedSymbolIndices = [0, 1, 0, 1, 0, 1, 0, 1]; + ushort[] expectedTypes = [3, 3, 2, 1, 4, 4, 11, 10]; + + for (int i = 0; i < relocations.Length; i++) + { + CoffRelocation relocation = ReadRelocation(first, section, i); + Assert.Equal(expectedAddresses[i], relocation.VirtualAddress); + Assert.Equal(expectedSymbolIndices[i], relocation.SymbolTableIndex); + Assert.Equal(expectedTypes[i], relocation.Type); + } + + Assert.Equal("target0", ReadSymbolName(first, 0)); + Assert.Equal("target1", ReadSymbolName(first, 1)); + Assert.Equal("source", ReadSymbolName(first, 2)); + } + + [Fact] + public void ConvertsArchitectureSpecificRelocationTypes() + { + AssertRelocationTypes( + TargetArchitecture.X86, + [ + RelocType.IMAGE_REL_BASED_ABSOLUTE, + RelocType.IMAGE_REL_BASED_ADDR32NB, + RelocType.IMAGE_REL_BASED_HIGHLOW, + RelocType.IMAGE_REL_BASED_REL32, + RelocType.IMAGE_REL_BASED_RELPTR32, + RelocType.IMAGE_REL_SECREL, + RelocType.IMAGE_REL_SECTION, + ], + [7, 7, 6, 20, 20, 11, 10]); + + AssertRelocationTypes( + TargetArchitecture.X64, + [ + RelocType.IMAGE_REL_BASED_ABSOLUTE, + RelocType.IMAGE_REL_BASED_ADDR32NB, + RelocType.IMAGE_REL_BASED_HIGHLOW, + RelocType.IMAGE_REL_BASED_DIR64, + RelocType.IMAGE_REL_BASED_REL32, + RelocType.IMAGE_REL_BASED_RELPTR32, + RelocType.IMAGE_REL_SECREL, + RelocType.IMAGE_REL_SECTION, + ], + [3, 3, 2, 1, 4, 4, 11, 10]); + + AssertRelocationTypes( + TargetArchitecture.ARM64, + [ + RelocType.IMAGE_REL_BASED_ABSOLUTE, + RelocType.IMAGE_REL_BASED_ADDR32NB, + RelocType.IMAGE_REL_BASED_HIGHLOW, + RelocType.IMAGE_REL_BASED_DIR64, + RelocType.IMAGE_REL_BASED_REL32, + RelocType.IMAGE_REL_BASED_RELPTR32, + RelocType.IMAGE_REL_BASED_ARM64_BRANCH26, + RelocType.IMAGE_REL_BASED_ARM64_PAGEBASE_REL21, + RelocType.IMAGE_REL_BASED_ARM64_PAGEOFFSET_12A, + RelocType.IMAGE_REL_BASED_ARM64_PAGEOFFSET_12L, + RelocType.IMAGE_REL_ARM64_TLS_SECREL_HIGH12A, + RelocType.IMAGE_REL_ARM64_TLS_SECREL_LOW12A, + RelocType.IMAGE_REL_SECREL, + RelocType.IMAGE_REL_SECTION, + ], + [2, 2, 1, 14, 17, 17, 3, 4, 6, 7, 10, 9, 8, 13]); + } + + [Fact] + public void AppliesAddendsBeforeWritingRelocations() + { + byte[] data = new byte[20]; + BinaryPrimitives.WriteInt32LittleEndian(data, 10); + BinaryPrimitives.WriteInt64LittleEndian(data.AsSpan(8), 100); + BinaryPrimitives.WriteInt32LittleEndian(data.AsSpan(16), -2); + + Relocation[] relocations = + [ + CreateRelocation(0, RelocType.IMAGE_REL_BASED_ADDR32NB, targetIndex: 0, addend: 7), + CreateRelocation(8, RelocType.IMAGE_REL_BASED_DIR64, targetIndex: 0, addend: -40), + CreateRelocation(16, RelocType.IMAGE_REL_BASED_RELPTR32, targetIndex: 0, addend: 3), + ]; + + byte[] objectBytes = EmitObject(data, relocations); + CoffSection section = FindSection(objectBytes, ".rdata"); + ReadOnlySpan emittedData = objectBytes.AsSpan((int)section.PointerToRawData, data.Length); + + Assert.Equal(17, BinaryPrimitives.ReadInt32LittleEndian(emittedData)); + Assert.Equal(60, BinaryPrimitives.ReadInt64LittleEndian(emittedData.Slice(8))); + Assert.Equal(5, BinaryPrimitives.ReadInt32LittleEndian(emittedData.Slice(16))); + } + + [Fact] + public void EmitsOverflowRelocationRecord() + { + const int RelocationCount = ushort.MaxValue + 1; + Relocation[] relocations = CreateRelocations(RelocationCount, RelocType.IMAGE_REL_BASED_ADDR32NB); + + byte[] objectBytes = EmitObject(new byte[sizeof(uint)], relocations); + CoffSection section = FindSection(objectBytes, ".rdata"); + + Assert.Equal(ushort.MaxValue, section.NumberOfRelocations); + Assert.NotEqual(0u, section.Characteristics & ImageScnLnkNRelocOvfl); + + CoffRelocation overflow = ReadRelocation(objectBytes, section, 0); + Assert.Equal((uint)RelocationCount + 1, overflow.VirtualAddress); + Assert.Equal(0u, overflow.SymbolTableIndex); + Assert.Equal(0, overflow.Type); + + CoffRelocation first = ReadRelocation(objectBytes, section, 1); + CoffRelocation last = ReadRelocation(objectBytes, section, RelocationCount); + Assert.Equal(0u, first.VirtualAddress); + Assert.Equal(0u, last.VirtualAddress); + Assert.Equal(3, first.Type); + Assert.Equal(3, last.Type); + } + + private static Relocation[] CreateRelocations(int count, RelocType type) + { + Relocation[] relocations = new Relocation[count]; + TestObjectNode target = new TestObjectNode("target0", ObjectNodeSection.DataSection, Array.Empty()); + for (int i = 0; i < relocations.Length; i++) + { + relocations[i] = new Relocation(type, offset: 0, target); + } + return relocations; + } + + private static Relocation CreateRelocation(int offset, RelocType type, int targetIndex, int addend = 0) + { + var target = new TestObjectNode($"target{targetIndex}", ObjectNodeSection.DataSection, Array.Empty(), addend); + return new Relocation(type, offset, target); + } + + private static void AssertRelocationTypes( + TargetArchitecture architecture, + RelocType[] relocationTypes, + ushort[] expectedTypes) + { + var relocations = new Relocation[relocationTypes.Length]; + for (int i = 0; i < relocationTypes.Length; i++) + { + relocations[i] = CreateRelocation(i * 8, relocationTypes[i], targetIndex: 0); + } + + byte[] objectBytes = EmitObject( + new byte[relocationTypes.Length * 8], + relocations, + architecture: architecture); + CoffSection section = FindSection(objectBytes, ".rdata"); + Assert.Equal(relocationTypes.Length, section.NumberOfRelocations); + + for (int i = 0; i < expectedTypes.Length; i++) + { + Assert.Equal(expectedTypes[i], ReadRelocation(objectBytes, section, i).Type); + } + } + + private static byte[] EmitObject( + byte[] data, + Relocation[] relocations, + int targetCount = 1, + TargetArchitecture architecture = TargetArchitecture.X64, + int sourceSectionPrefixSize = 0) + { + int sourceIndex = targetCount; + var nodes = new DependencyNode[targetCount + 1 + (sourceSectionPrefixSize > 0 ? 1 : 0)]; + for (int i = 0; i < targetCount; i++) + { + nodes[i] = new TestObjectNode($"target{i}", ObjectNodeSection.DataSection, [0]); + } + + if (sourceSectionPrefixSize > 0) + { + nodes[sourceIndex++] = new TestDataNode( + ObjectNodeSection.ReadOnlyDataSection, + new byte[sourceSectionPrefixSize]); + } + + int sourceAlignment = architecture == TargetArchitecture.X86 ? 4 : 8; + nodes[sourceIndex] = new TestObjectNode( + "source", + ObjectNodeSection.ReadOnlyDataSection, + data, + alignment: sourceAlignment, + relocations: relocations); + + lock (s_emitLock) + { + var objectWriter = new CoffObjectWriter(s_nodeFactories[architecture], ObjectWritingOptions.None); + using var stream = new MemoryStream(); + objectWriter.EmitObject(stream, nodes, dumper: null, Logger.Null); + return stream.ToArray(); + } + } + + private static NodeFactory CreateNodeFactory(TargetArchitecture architecture) + { + var target = new TargetDetails(architecture, TargetOS.Windows, TargetAbi.NativeAot); + var context = new CompilerTypeSystemContext(target, SharedGenericsMode.CanonicalReferenceTypes, DelegateFeature.All) + { + InputFilePaths = new Dictionary + { + { "Test.CoreLib", @"Test.CoreLib.dll" }, + { "ILCompiler.Compiler.Tests.Assets", @"ILCompiler.Compiler.Tests.Assets.dll" }, + }, + ReferenceFilePaths = new Dictionary(), + }; + + context.SetSystemModule(context.GetModuleForSimpleName("Test.CoreLib")); + IILScanner scanner = new RyuJitCompilationBuilder(context, new SingleFileCompilationModuleGroup()) + .GetILScannerBuilder() + .UseCompilationRoots(Array.Empty()) + .ToILScanner(); + NodeFactory nodeFactory = ((Compilation)scanner).NodeFactory; + nodeFactory.SetMarkingComplete(); + return nodeFactory; + } + + private static CoffSection FindSection(byte[] objectBytes, string name) + { + ushort sectionCount = BinaryPrimitives.ReadUInt16LittleEndian(objectBytes.AsSpan(2)); + for (int i = 0; i < sectionCount; i++) + { + int offset = CoffHeaderSize + i * CoffSectionHeaderSize; + string sectionName = Encoding.ASCII.GetString(objectBytes, offset, 8).TrimEnd('\0'); + if (sectionName == name) + { + return new CoffSection( + BinaryPrimitives.ReadUInt32LittleEndian(objectBytes.AsSpan(offset + 20)), + BinaryPrimitives.ReadUInt32LittleEndian(objectBytes.AsSpan(offset + 24)), + BinaryPrimitives.ReadUInt16LittleEndian(objectBytes.AsSpan(offset + 32)), + BinaryPrimitives.ReadUInt32LittleEndian(objectBytes.AsSpan(offset + 36))); + } + } + + throw new InvalidOperationException($"Section '{name}' was not found."); + } + + private static CoffRelocation ReadRelocation(byte[] objectBytes, CoffSection section, int index) + { + ReadOnlySpan relocation = objectBytes.AsSpan( + checked((int)section.PointerToRelocations + index * CoffRelocationSize), + CoffRelocationSize); + return new CoffRelocation( + BinaryPrimitives.ReadUInt32LittleEndian(relocation), + BinaryPrimitives.ReadUInt32LittleEndian(relocation.Slice(4)), + BinaryPrimitives.ReadUInt16LittleEndian(relocation.Slice(8))); + } + + private static string ReadSymbolName(byte[] objectBytes, int symbolIndex) + { + uint symbolTableOffset = BinaryPrimitives.ReadUInt32LittleEndian(objectBytes.AsSpan(8)); + int offset = checked((int)symbolTableOffset + symbolIndex * CoffSymbolSize); + return Encoding.ASCII.GetString(objectBytes, offset, 8).TrimEnd('\0'); + } + + private readonly struct CoffSection + { + public CoffSection(uint pointerToRawData, uint pointerToRelocations, ushort numberOfRelocations, uint characteristics) + { + PointerToRawData = pointerToRawData; + PointerToRelocations = pointerToRelocations; + NumberOfRelocations = numberOfRelocations; + Characteristics = characteristics; + } + + public uint PointerToRawData { get; } + public uint PointerToRelocations { get; } + public ushort NumberOfRelocations { get; } + public uint Characteristics { get; } + } + + private readonly struct CoffRelocation + { + public CoffRelocation(uint virtualAddress, uint symbolTableIndex, ushort type) + { + VirtualAddress = virtualAddress; + SymbolTableIndex = symbolTableIndex; + Type = type; + } + + public uint VirtualAddress { get; } + public uint SymbolTableIndex { get; } + public ushort Type { get; } + } + + private sealed class TestObjectNode : ObjectNode, ISymbolDefinitionNode + { + private readonly ObjectData _data; + private readonly Utf8String _name; + private readonly int _offset; + private readonly ObjectNodeSection _section; + + public TestObjectNode( + string name, + ObjectNodeSection section, + byte[] data, + int offset = 0, + int alignment = 1, + Relocation[] relocations = null) + { + _name = new Utf8String(name); + _section = section; + _offset = offset; + _data = new ObjectData(data, relocations ?? Array.Empty(), alignment, [this]); + } + + public int Offset => _offset; + public override bool IsShareable => false; + public override int ClassCode => 0x6AEB3B7; + public override bool StaticDependenciesAreComputed => true; + + public void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb) => sb.Append(_name); + + public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) => _data; + + public override ObjectNodeSection GetSection(NodeFactory factory) => _section; + + protected override string GetName(NodeFactory factory) => _name.ToString(); + } + + private sealed class TestDataNode : ObjectNode + { + private readonly ObjectData _data; + private readonly ObjectNodeSection _section; + + public TestDataNode(ObjectNodeSection section, byte[] data) + { + _section = section; + _data = new ObjectData(data, Array.Empty(), alignment: 1, Array.Empty()); + } + + public override bool IsShareable => false; + public override int ClassCode => 0x5C68E93; + public override bool StaticDependenciesAreComputed => true; + + public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) => _data; + + public override ObjectNodeSection GetSection(NodeFactory factory) => _section; + + protected override string GetName(NodeFactory factory) => nameof(TestDataNode); + } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/ILCompiler.Compiler.Tests.csproj b/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/ILCompiler.Compiler.Tests.csproj index 69e9d87637f92d..9697474ae44c72 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/ILCompiler.Compiler.Tests.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/ILCompiler.Compiler.Tests.csproj @@ -40,6 +40,7 @@ + diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj index 0964719be88f3d..a2f3a9ad93dd89 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj @@ -26,6 +26,8 @@ + + all contentfiles From 8eca6f0d855d13d5beb6502dfbdbd5aa0b22234a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Sat, 29 Aug 2026 20:48:49 -0400 Subject: [PATCH 2/4] Remove dedicated COFF writer tests --- .../CoffObjectWriterTests.cs | 484 ------------------ .../ILCompiler.Compiler.Tests.csproj | 1 - .../ILCompiler.Compiler.csproj | 2 - 3 files changed, 487 deletions(-) delete mode 100644 src/coreclr/tools/aot/ILCompiler.Compiler.Tests/CoffObjectWriterTests.cs diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/CoffObjectWriterTests.cs b/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/CoffObjectWriterTests.cs deleted file mode 100644 index f502cedf00b4df..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/CoffObjectWriterTests.cs +++ /dev/null @@ -1,484 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Buffers.Binary; -using System.Collections.Generic; -using System.IO; -using System.Runtime.CompilerServices; -using System.Text; - -using ILCompiler.DependencyAnalysis; -using ILCompiler.DependencyAnalysisFramework; -using ILCompiler.ObjectWriter; - -using Internal.IL; -using Internal.Text; -using Internal.TypeSystem; - -using Xunit; - -namespace ILCompiler.Compiler.Tests -{ - public class CoffObjectWriterTests - { - private const int CoffHeaderSize = 20; - private const int CoffRelocationSize = 10; - private const int CoffSectionHeaderSize = 40; - private const int CoffSymbolSize = 18; - private const uint ImageScnLnkNRelocOvfl = 0x01000000; - - private static readonly object s_emitLock = new object(); - private static readonly Dictionary s_nodeFactories = new Dictionary - { - { TargetArchitecture.X86, CreateNodeFactory(TargetArchitecture.X86) }, - { TargetArchitecture.X64, CreateNodeFactory(TargetArchitecture.X64) }, - { TargetArchitecture.ARM64, CreateNodeFactory(TargetArchitecture.ARM64) }, - }; - - [Fact] - public void RelocationUsesCompactValueLayout() - { - Assert.True(typeof(CoffObjectWriter.CoffRelocation).IsValueType); - Assert.Equal(12, Unsafe.SizeOf()); - } - - public static IEnumerable RelocationFieldValues() - { - var seenValues = new HashSet(); - foreach (CoffObjectWriter.CoffRelocationType type in Enum.GetValues()) - { - int typeValue = (int)type; - if (seenValues.Add(typeValue)) - { - yield return new object[] { 0u, 0u, typeValue }; - } - } - - yield return new object[] { uint.MaxValue, uint.MaxValue, ushort.MaxValue }; - } - - [Theory] - [MemberData(nameof(RelocationFieldValues))] - public void RelocationWritesExpectedBytes(uint virtualAddress, uint symbolTableIndex, int typeValue) - { - var relocation = new CoffObjectWriter.CoffRelocation( - virtualAddress, - symbolTableIndex, - (CoffObjectWriter.CoffRelocationType)typeValue); - - using var stream = new MemoryStream(); - relocation.Write(stream); - - byte[] expected = new byte[CoffRelocationSize]; - BinaryPrimitives.WriteUInt32LittleEndian(expected, virtualAddress); - BinaryPrimitives.WriteUInt32LittleEndian(expected.AsSpan(4), symbolTableIndex); - BinaryPrimitives.WriteUInt16LittleEndian(expected.AsSpan(8), (ushort)typeValue); - Assert.Equal(expected, stream.ToArray()); - } - - [Fact] - public void RelocationUsesCoffWireLayout() - { - var relocation = new CoffObjectWriter.CoffRelocation( - 0x11223344, - 0x55667788, - (CoffObjectWriter.CoffRelocationType)0x99AA); - - using var stream = new MemoryStream(); - relocation.Write(stream); - - Assert.Equal( - new byte[] { 0x44, 0x33, 0x22, 0x11, 0x88, 0x77, 0x66, 0x55, 0xAA, 0x99 }, - stream.ToArray()); - } - - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(3)] - public void EmitsExpectedRelocationCount(int relocationCount) - { - Relocation[] relocations = CreateRelocations(relocationCount, RelocType.IMAGE_REL_BASED_ADDR32NB); - byte[] objectBytes = EmitObject(new byte[sizeof(uint)], relocations); - CoffSection section = FindSection(objectBytes, ".rdata"); - - Assert.Equal(relocationCount, section.NumberOfRelocations); - - if (relocationCount == 0) - { - Assert.Equal(0u, section.PointerToRelocations); - } - else - { - Assert.NotEqual(0u, section.PointerToRelocations); - } - } - - [Fact] - public void ConvertsRelocationsWithoutChangingOrderOrFields() - { - Relocation[] relocations = - [ - CreateRelocation(0, RelocType.IMAGE_REL_BASED_ABSOLUTE, targetIndex: 0), - CreateRelocation(4, RelocType.IMAGE_REL_BASED_ADDR32NB, targetIndex: 1), - CreateRelocation(8, RelocType.IMAGE_REL_BASED_HIGHLOW, targetIndex: 0), - CreateRelocation(16, RelocType.IMAGE_REL_BASED_DIR64, targetIndex: 1), - CreateRelocation(24, RelocType.IMAGE_REL_BASED_REL32, targetIndex: 0), - CreateRelocation(28, RelocType.IMAGE_REL_BASED_RELPTR32, targetIndex: 1), - CreateRelocation(32, RelocType.IMAGE_REL_SECREL, targetIndex: 0), - CreateRelocation(36, RelocType.IMAGE_REL_SECTION, targetIndex: 1), - ]; - - byte[] first = EmitObject(new byte[40], relocations, targetCount: 2, sourceSectionPrefixSize: 13); - byte[] second = EmitObject(new byte[40], relocations, targetCount: 2, sourceSectionPrefixSize: 13); - Assert.Equal(first, second); - - CoffSection section = FindSection(first, ".rdata"); - Assert.Equal(relocations.Length, section.NumberOfRelocations); - - uint[] expectedAddresses = [16, 20, 24, 32, 40, 44, 48, 52]; - uint[] expectedSymbolIndices = [0, 1, 0, 1, 0, 1, 0, 1]; - ushort[] expectedTypes = [3, 3, 2, 1, 4, 4, 11, 10]; - - for (int i = 0; i < relocations.Length; i++) - { - CoffRelocation relocation = ReadRelocation(first, section, i); - Assert.Equal(expectedAddresses[i], relocation.VirtualAddress); - Assert.Equal(expectedSymbolIndices[i], relocation.SymbolTableIndex); - Assert.Equal(expectedTypes[i], relocation.Type); - } - - Assert.Equal("target0", ReadSymbolName(first, 0)); - Assert.Equal("target1", ReadSymbolName(first, 1)); - Assert.Equal("source", ReadSymbolName(first, 2)); - } - - [Fact] - public void ConvertsArchitectureSpecificRelocationTypes() - { - AssertRelocationTypes( - TargetArchitecture.X86, - [ - RelocType.IMAGE_REL_BASED_ABSOLUTE, - RelocType.IMAGE_REL_BASED_ADDR32NB, - RelocType.IMAGE_REL_BASED_HIGHLOW, - RelocType.IMAGE_REL_BASED_REL32, - RelocType.IMAGE_REL_BASED_RELPTR32, - RelocType.IMAGE_REL_SECREL, - RelocType.IMAGE_REL_SECTION, - ], - [7, 7, 6, 20, 20, 11, 10]); - - AssertRelocationTypes( - TargetArchitecture.X64, - [ - RelocType.IMAGE_REL_BASED_ABSOLUTE, - RelocType.IMAGE_REL_BASED_ADDR32NB, - RelocType.IMAGE_REL_BASED_HIGHLOW, - RelocType.IMAGE_REL_BASED_DIR64, - RelocType.IMAGE_REL_BASED_REL32, - RelocType.IMAGE_REL_BASED_RELPTR32, - RelocType.IMAGE_REL_SECREL, - RelocType.IMAGE_REL_SECTION, - ], - [3, 3, 2, 1, 4, 4, 11, 10]); - - AssertRelocationTypes( - TargetArchitecture.ARM64, - [ - RelocType.IMAGE_REL_BASED_ABSOLUTE, - RelocType.IMAGE_REL_BASED_ADDR32NB, - RelocType.IMAGE_REL_BASED_HIGHLOW, - RelocType.IMAGE_REL_BASED_DIR64, - RelocType.IMAGE_REL_BASED_REL32, - RelocType.IMAGE_REL_BASED_RELPTR32, - RelocType.IMAGE_REL_BASED_ARM64_BRANCH26, - RelocType.IMAGE_REL_BASED_ARM64_PAGEBASE_REL21, - RelocType.IMAGE_REL_BASED_ARM64_PAGEOFFSET_12A, - RelocType.IMAGE_REL_BASED_ARM64_PAGEOFFSET_12L, - RelocType.IMAGE_REL_ARM64_TLS_SECREL_HIGH12A, - RelocType.IMAGE_REL_ARM64_TLS_SECREL_LOW12A, - RelocType.IMAGE_REL_SECREL, - RelocType.IMAGE_REL_SECTION, - ], - [2, 2, 1, 14, 17, 17, 3, 4, 6, 7, 10, 9, 8, 13]); - } - - [Fact] - public void AppliesAddendsBeforeWritingRelocations() - { - byte[] data = new byte[20]; - BinaryPrimitives.WriteInt32LittleEndian(data, 10); - BinaryPrimitives.WriteInt64LittleEndian(data.AsSpan(8), 100); - BinaryPrimitives.WriteInt32LittleEndian(data.AsSpan(16), -2); - - Relocation[] relocations = - [ - CreateRelocation(0, RelocType.IMAGE_REL_BASED_ADDR32NB, targetIndex: 0, addend: 7), - CreateRelocation(8, RelocType.IMAGE_REL_BASED_DIR64, targetIndex: 0, addend: -40), - CreateRelocation(16, RelocType.IMAGE_REL_BASED_RELPTR32, targetIndex: 0, addend: 3), - ]; - - byte[] objectBytes = EmitObject(data, relocations); - CoffSection section = FindSection(objectBytes, ".rdata"); - ReadOnlySpan emittedData = objectBytes.AsSpan((int)section.PointerToRawData, data.Length); - - Assert.Equal(17, BinaryPrimitives.ReadInt32LittleEndian(emittedData)); - Assert.Equal(60, BinaryPrimitives.ReadInt64LittleEndian(emittedData.Slice(8))); - Assert.Equal(5, BinaryPrimitives.ReadInt32LittleEndian(emittedData.Slice(16))); - } - - [Fact] - public void EmitsOverflowRelocationRecord() - { - const int RelocationCount = ushort.MaxValue + 1; - Relocation[] relocations = CreateRelocations(RelocationCount, RelocType.IMAGE_REL_BASED_ADDR32NB); - - byte[] objectBytes = EmitObject(new byte[sizeof(uint)], relocations); - CoffSection section = FindSection(objectBytes, ".rdata"); - - Assert.Equal(ushort.MaxValue, section.NumberOfRelocations); - Assert.NotEqual(0u, section.Characteristics & ImageScnLnkNRelocOvfl); - - CoffRelocation overflow = ReadRelocation(objectBytes, section, 0); - Assert.Equal((uint)RelocationCount + 1, overflow.VirtualAddress); - Assert.Equal(0u, overflow.SymbolTableIndex); - Assert.Equal(0, overflow.Type); - - CoffRelocation first = ReadRelocation(objectBytes, section, 1); - CoffRelocation last = ReadRelocation(objectBytes, section, RelocationCount); - Assert.Equal(0u, first.VirtualAddress); - Assert.Equal(0u, last.VirtualAddress); - Assert.Equal(3, first.Type); - Assert.Equal(3, last.Type); - } - - private static Relocation[] CreateRelocations(int count, RelocType type) - { - Relocation[] relocations = new Relocation[count]; - TestObjectNode target = new TestObjectNode("target0", ObjectNodeSection.DataSection, Array.Empty()); - for (int i = 0; i < relocations.Length; i++) - { - relocations[i] = new Relocation(type, offset: 0, target); - } - return relocations; - } - - private static Relocation CreateRelocation(int offset, RelocType type, int targetIndex, int addend = 0) - { - var target = new TestObjectNode($"target{targetIndex}", ObjectNodeSection.DataSection, Array.Empty(), addend); - return new Relocation(type, offset, target); - } - - private static void AssertRelocationTypes( - TargetArchitecture architecture, - RelocType[] relocationTypes, - ushort[] expectedTypes) - { - var relocations = new Relocation[relocationTypes.Length]; - for (int i = 0; i < relocationTypes.Length; i++) - { - relocations[i] = CreateRelocation(i * 8, relocationTypes[i], targetIndex: 0); - } - - byte[] objectBytes = EmitObject( - new byte[relocationTypes.Length * 8], - relocations, - architecture: architecture); - CoffSection section = FindSection(objectBytes, ".rdata"); - Assert.Equal(relocationTypes.Length, section.NumberOfRelocations); - - for (int i = 0; i < expectedTypes.Length; i++) - { - Assert.Equal(expectedTypes[i], ReadRelocation(objectBytes, section, i).Type); - } - } - - private static byte[] EmitObject( - byte[] data, - Relocation[] relocations, - int targetCount = 1, - TargetArchitecture architecture = TargetArchitecture.X64, - int sourceSectionPrefixSize = 0) - { - int sourceIndex = targetCount; - var nodes = new DependencyNode[targetCount + 1 + (sourceSectionPrefixSize > 0 ? 1 : 0)]; - for (int i = 0; i < targetCount; i++) - { - nodes[i] = new TestObjectNode($"target{i}", ObjectNodeSection.DataSection, [0]); - } - - if (sourceSectionPrefixSize > 0) - { - nodes[sourceIndex++] = new TestDataNode( - ObjectNodeSection.ReadOnlyDataSection, - new byte[sourceSectionPrefixSize]); - } - - int sourceAlignment = architecture == TargetArchitecture.X86 ? 4 : 8; - nodes[sourceIndex] = new TestObjectNode( - "source", - ObjectNodeSection.ReadOnlyDataSection, - data, - alignment: sourceAlignment, - relocations: relocations); - - lock (s_emitLock) - { - var objectWriter = new CoffObjectWriter(s_nodeFactories[architecture], ObjectWritingOptions.None); - using var stream = new MemoryStream(); - objectWriter.EmitObject(stream, nodes, dumper: null, Logger.Null); - return stream.ToArray(); - } - } - - private static NodeFactory CreateNodeFactory(TargetArchitecture architecture) - { - var target = new TargetDetails(architecture, TargetOS.Windows, TargetAbi.NativeAot); - var context = new CompilerTypeSystemContext(target, SharedGenericsMode.CanonicalReferenceTypes, DelegateFeature.All) - { - InputFilePaths = new Dictionary - { - { "Test.CoreLib", @"Test.CoreLib.dll" }, - { "ILCompiler.Compiler.Tests.Assets", @"ILCompiler.Compiler.Tests.Assets.dll" }, - }, - ReferenceFilePaths = new Dictionary(), - }; - - context.SetSystemModule(context.GetModuleForSimpleName("Test.CoreLib")); - IILScanner scanner = new RyuJitCompilationBuilder(context, new SingleFileCompilationModuleGroup()) - .GetILScannerBuilder() - .UseCompilationRoots(Array.Empty()) - .ToILScanner(); - NodeFactory nodeFactory = ((Compilation)scanner).NodeFactory; - nodeFactory.SetMarkingComplete(); - return nodeFactory; - } - - private static CoffSection FindSection(byte[] objectBytes, string name) - { - ushort sectionCount = BinaryPrimitives.ReadUInt16LittleEndian(objectBytes.AsSpan(2)); - for (int i = 0; i < sectionCount; i++) - { - int offset = CoffHeaderSize + i * CoffSectionHeaderSize; - string sectionName = Encoding.ASCII.GetString(objectBytes, offset, 8).TrimEnd('\0'); - if (sectionName == name) - { - return new CoffSection( - BinaryPrimitives.ReadUInt32LittleEndian(objectBytes.AsSpan(offset + 20)), - BinaryPrimitives.ReadUInt32LittleEndian(objectBytes.AsSpan(offset + 24)), - BinaryPrimitives.ReadUInt16LittleEndian(objectBytes.AsSpan(offset + 32)), - BinaryPrimitives.ReadUInt32LittleEndian(objectBytes.AsSpan(offset + 36))); - } - } - - throw new InvalidOperationException($"Section '{name}' was not found."); - } - - private static CoffRelocation ReadRelocation(byte[] objectBytes, CoffSection section, int index) - { - ReadOnlySpan relocation = objectBytes.AsSpan( - checked((int)section.PointerToRelocations + index * CoffRelocationSize), - CoffRelocationSize); - return new CoffRelocation( - BinaryPrimitives.ReadUInt32LittleEndian(relocation), - BinaryPrimitives.ReadUInt32LittleEndian(relocation.Slice(4)), - BinaryPrimitives.ReadUInt16LittleEndian(relocation.Slice(8))); - } - - private static string ReadSymbolName(byte[] objectBytes, int symbolIndex) - { - uint symbolTableOffset = BinaryPrimitives.ReadUInt32LittleEndian(objectBytes.AsSpan(8)); - int offset = checked((int)symbolTableOffset + symbolIndex * CoffSymbolSize); - return Encoding.ASCII.GetString(objectBytes, offset, 8).TrimEnd('\0'); - } - - private readonly struct CoffSection - { - public CoffSection(uint pointerToRawData, uint pointerToRelocations, ushort numberOfRelocations, uint characteristics) - { - PointerToRawData = pointerToRawData; - PointerToRelocations = pointerToRelocations; - NumberOfRelocations = numberOfRelocations; - Characteristics = characteristics; - } - - public uint PointerToRawData { get; } - public uint PointerToRelocations { get; } - public ushort NumberOfRelocations { get; } - public uint Characteristics { get; } - } - - private readonly struct CoffRelocation - { - public CoffRelocation(uint virtualAddress, uint symbolTableIndex, ushort type) - { - VirtualAddress = virtualAddress; - SymbolTableIndex = symbolTableIndex; - Type = type; - } - - public uint VirtualAddress { get; } - public uint SymbolTableIndex { get; } - public ushort Type { get; } - } - - private sealed class TestObjectNode : ObjectNode, ISymbolDefinitionNode - { - private readonly ObjectData _data; - private readonly Utf8String _name; - private readonly int _offset; - private readonly ObjectNodeSection _section; - - public TestObjectNode( - string name, - ObjectNodeSection section, - byte[] data, - int offset = 0, - int alignment = 1, - Relocation[] relocations = null) - { - _name = new Utf8String(name); - _section = section; - _offset = offset; - _data = new ObjectData(data, relocations ?? Array.Empty(), alignment, [this]); - } - - public int Offset => _offset; - public override bool IsShareable => false; - public override int ClassCode => 0x6AEB3B7; - public override bool StaticDependenciesAreComputed => true; - - public void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb) => sb.Append(_name); - - public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) => _data; - - public override ObjectNodeSection GetSection(NodeFactory factory) => _section; - - protected override string GetName(NodeFactory factory) => _name.ToString(); - } - - private sealed class TestDataNode : ObjectNode - { - private readonly ObjectData _data; - private readonly ObjectNodeSection _section; - - public TestDataNode(ObjectNodeSection section, byte[] data) - { - _section = section; - _data = new ObjectData(data, Array.Empty(), alignment: 1, Array.Empty()); - } - - public override bool IsShareable => false; - public override int ClassCode => 0x5C68E93; - public override bool StaticDependenciesAreComputed => true; - - public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) => _data; - - public override ObjectNodeSection GetSection(NodeFactory factory) => _section; - - protected override string GetName(NodeFactory factory) => nameof(TestDataNode); - } - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/ILCompiler.Compiler.Tests.csproj b/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/ILCompiler.Compiler.Tests.csproj index 9697474ae44c72..69e9d87637f92d 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/ILCompiler.Compiler.Tests.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/ILCompiler.Compiler.Tests.csproj @@ -40,7 +40,6 @@ - diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj index a2f3a9ad93dd89..0964719be88f3d 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj @@ -26,8 +26,6 @@ - - all contentfiles From 6ea091b3d6106dce42d7d0e4281cf23f93ba3721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Sat, 29 Aug 2026 20:58:06 -0400 Subject: [PATCH 3/4] Mark COFF relocation write readonly --- .../tools/Common/Compiler/ObjectWriter/CoffObjectWriter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/CoffObjectWriter.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/CoffObjectWriter.cs index 60c949e0a3ed67..2ad1f3bb04262a 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/CoffObjectWriter.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/CoffObjectWriter.cs @@ -715,7 +715,7 @@ public CoffRelocation(uint virtualAddress, uint symbolTableIndex, CoffRelocation sizeof(uint) + // SymbolTableIndex sizeof(ushort); // Type - public void Write(Stream stream) + public readonly void Write(Stream stream) { Span buffer = stackalloc byte[Size]; From cc34c7cca3d550201dd8b4ab3e5bfc864b200193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Sat, 29 Aug 2026 21:32:08 -0400 Subject: [PATCH 4/4] Remove redundant readonly modifier --- .../tools/Common/Compiler/ObjectWriter/CoffObjectWriter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/CoffObjectWriter.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/CoffObjectWriter.cs index 2ad1f3bb04262a..60c949e0a3ed67 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/CoffObjectWriter.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/CoffObjectWriter.cs @@ -715,7 +715,7 @@ public CoffRelocation(uint virtualAddress, uint symbolTableIndex, CoffRelocation sizeof(uint) + // SymbolTableIndex sizeof(ushort); // Type - public readonly void Write(Stream stream) + public void Write(Stream stream) { Span buffer = stackalloc byte[Size];