diff --git a/src/coreclr/tools/Common/Compiler/DependencyAnalysis/Relocation.cs b/src/coreclr/tools/Common/Compiler/DependencyAnalysis/Relocation.cs index 1b4515c4d210ea..8da0718d9d0164 100644 --- a/src/coreclr/tools/Common/Compiler/DependencyAnalysis/Relocation.cs +++ b/src/coreclr/tools/Common/Compiler/DependencyAnalysis/Relocation.cs @@ -696,6 +696,31 @@ public static unsafe void WriteValue(RelocType relocType, void* location, long v } } + public static unsafe int WriteVariableLengthValue(RelocType relocType, byte* location, long value) + { + Debug.Assert(IsVariableLength(relocType)); + switch (relocType) + { + case RelocType.WASM_TYPE_INDEX_LEB: + case RelocType.WASM_GLOBAL_INDEX_LEB: + case RelocType.WASM_FUNCTION_INDEX_LEB: + case RelocType.WASM_MEMORY_ADDR_LEB: + case RelocType.WASM_MEMORY_ADDR_REL_LEB: + case RelocType.WASM_CLR_RESTORE_CONTEXT_EXCEPTION_TAG_LEB: + DwarfHelper.WriteULEB128(new Span((byte*)location, WASM_PADDED_RELOC_SIZE_32), checked((ulong)value)); + return (int)DwarfHelper.SizeOfULEB128((ulong)value); + + case RelocType.WASM_TABLE_INDEX_SLEB: + case RelocType.WASM_MEMORY_ADDR_SLEB: + case RelocType.WASM_MEMORY_ADDR_REL_SLEB: + DwarfHelper.WriteSLEB128(new Span((byte*)location, WASM_PADDED_RELOC_SIZE_32), value); + return (int)DwarfHelper.SizeOfSLEB128(value); + default: + Debug.Fail("Invalid variable-length RelocType: " + relocType); + return 0; + } + } + public static readonly int MaxSize = 8; // Note: Please update the above field if the max size // changes when adding a new case to this method. @@ -742,6 +767,45 @@ public static int GetSize(RelocType relocType) }; } + public static bool IsVariableLength(RelocType relocType) + { + return relocType switch + { + RelocType.WASM_FUNCTION_INDEX_LEB or + RelocType.WASM_TABLE_INDEX_SLEB or + RelocType.WASM_TYPE_INDEX_LEB or + RelocType.WASM_GLOBAL_INDEX_LEB or + RelocType.WASM_MEMORY_ADDR_LEB or + RelocType.WASM_MEMORY_ADDR_SLEB or + RelocType.WASM_MEMORY_ADDR_REL_LEB or + RelocType.WASM_MEMORY_ADDR_REL_SLEB or + RelocType.WASM_CLR_RESTORE_CONTEXT_EXCEPTION_TAG_LEB => true, + _ => false, + }; + } + + public static int ActualSize(RelocType relocType, long resolvedValue) + { + Debug.Assert(IsVariableLength(relocType)); + switch (relocType) + { + case RelocType.WASM_FUNCTION_INDEX_LEB: + case RelocType.WASM_TYPE_INDEX_LEB: + case RelocType.WASM_GLOBAL_INDEX_LEB: + case RelocType.WASM_MEMORY_ADDR_LEB: + case RelocType.WASM_MEMORY_ADDR_REL_LEB: + case RelocType.WASM_CLR_RESTORE_CONTEXT_EXCEPTION_TAG_LEB: + return (int)DwarfHelper.SizeOfULEB128((ulong)resolvedValue); + case RelocType.WASM_TABLE_INDEX_SLEB: + case RelocType.WASM_MEMORY_ADDR_SLEB: + case RelocType.WASM_MEMORY_ADDR_REL_SLEB: + return (int)DwarfHelper.SizeOfSLEB128(resolvedValue); + default: + Debug.Fail("Invalid reloc type"); + return 0; + } + } + public static unsafe long ReadValue(RelocType relocType, void* location) { switch (relocType) diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/Dwarf/DwarfHelper.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/Dwarf/DwarfHelper.cs index 781388243dbbab..7942c68526562c 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/Dwarf/DwarfHelper.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/Dwarf/DwarfHelper.cs @@ -3,6 +3,8 @@ using System; using System.Buffers; +using System.Diagnostics; +using System.IO; using System.Numerics; namespace ILCompiler.ObjectWriter @@ -122,6 +124,40 @@ public static ulong ReadULEB128(ReadOnlySpan buffer, out int bytesRead) return value; } + internal static ulong? ReadULEB128(Stream source, out int bytesRead) + { + Debug.Assert(source.CanSeek); + Debug.Assert(source.Length >= 0); + + ulong value = 0; + int shift = 0; + bytesRead = 0; + + while (true) + { + int b = source.ReadByte(); + if (b < 0) + { + if (bytesRead == 0) + { + return null; + } + + throw new InvalidDataException("Unexpected end of stream while reading a ULEB128 value."); + } + + byte @byte = (byte)b; + bytesRead++; + value |= ((ulong)@byte & 0x7f) << shift; + if ((@byte & 0x80) == 0) + { + return value; + } + + shift += 7; + } + } + public static long ReadSLEB128(ReadOnlySpan buffer) => ReadSLEB128(buffer, out _); public static long ReadSLEB128(ReadOnlySpan buffer, out int bytesRead) diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs index a5c0d9b2006f85..3504e7d37fc192 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs @@ -140,7 +140,7 @@ private void RecordFunclets(INodeWithFunclets nodeWithFunclets) for (int i = 0; i < funcletKinds.Length; i++) { - WasmFuncType funcletSignature = GetFuncletType(funcletKinds[i], pointerType); + WasmFuncType funcletSignature = GetFuncletType(funcletKinds[i], pointerType); RegisterFunctionSymbol(new Utf8String($"{mangledNodeName}_funclet_{i}")); RegisterStubIndexAndSignature(funcletSignature); } @@ -697,11 +697,10 @@ private protected override void EmitObjectFile(Stream outputFileStream) { using (Stream originalStream = section.Stream) { - MemoryStream stream = new MemoryStream((int)originalStream.Length); + MemoryStream destStream = new MemoryStream((int)originalStream.Length); originalStream.Position = 0; - originalStream.CopyTo(stream); - ResolveRelocations(index, stream, relocations, sectionStart: 0); - section.Stream = stream; + ResolveRelocations(index, originalStream, destStream, relocations, sectionStart: 0, shrink: true); + section.Stream = destStream; // originalStream may be disposed, section.Stream now points to resolved stream } } @@ -728,17 +727,21 @@ private protected override void EmitObjectFile(Stream outputFileStream) // Move stream position forward to account for inter-section padding (precalculated in BuildWebcilDataSegment()) webcilStream.Position = section.Header.PointerToRawData; section.Stream.Position = 0; - section.Stream.CopyTo(webcilStream); - long bytesWritten = (long)webcilStream.Position - (long)section.Header.PointerToRawData; - Debug.Assert(section.Header.SizeOfRawData - bytesWritten == section.Padding, $"Unexpected padding: {section.Header.SizeOfRawData - bytesWritten} != {section.Padding}"); if (_resolvableRelocations.TryGetValue(section.Index, out List relocations)) { - // We emit all Webcil sections into one stream, and resolve relocations directly into this combined stream. - // As a result, the section-relative offsets that relocs in our list have need to be calculated based on the section's + // We emit all Webcil sections into one stream, and copy data / resolve relocations directly into this combined stream. + // As a result, the real offsets that relocs in our list have need to be calculated based on the section's // position within the Webcil segment - ResolveRelocations(section.Index, webcilStream, relocations, sectionStart: (long)section.Header.PointerToRawData); + ResolveRelocations(section.Index, section.Stream, webcilStream, relocations, sectionStart: (long)section.Header.PointerToRawData, shrink: false); + } + else + { + section.Stream.CopyTo(webcilStream); } + + long bytesWritten = (long)webcilStream.Position - (long)section.Header.PointerToRawData; + Debug.Assert(section.Header.SizeOfRawData - bytesWritten == section.Padding, $"Unexpected padding: {section.Header.SizeOfRawData - bytesWritten} != {section.Padding}"); } if (_webcilSegment.Sections.Length > 0) @@ -858,13 +861,150 @@ private bool IsWithinSection(long rva, WebcilSection section) return rva >= section.Header.VirtualAddress && rva < section.Header.VirtualAddress + section.Header.VirtualSize; } - // TODO-WASM: Currently, all Wasm relocs are resolved to 5 byte values unconditionally (the same size as the original placeholder padding), which is wasteful. - // We should remove the padding and shrink the resolved values to their minimal size so we don't bloat the binary size. #nullable enable - private unsafe void ResolveRelocations(int sectionIndex, MemoryStream sectionStream, List relocs, long sectionStart = 0) + static void CopyOnly(MemoryStream src, long srcPos, MemoryStream dest, long destPos, long count) + { + ArgumentOutOfRangeException.ThrowIfNegative(count); + src.GetBuffer().AsSpan((int)srcPos, (int)count).CopyTo(dest.GetBuffer().AsSpan((int)destPos, (int)count)); + } + + private readonly record struct CodeBlob(long Size, long Start, long End); + + private List ParseCodeBlobs(Stream sectionStream) + { + List blobs = new(); + while (true) + { + ulong? decoded = DwarfHelper.ReadULEB128(sectionStream, out _); + if (decoded is null) break; // end of stream + + Debug.Assert(sectionStream.Position + (long)decoded <= sectionStream.Length); + blobs.Add(new CodeBlob((long)decoded, sectionStream.Position, sectionStream.Position + (long)decoded)); + sectionStream.Position += (long)decoded; + } + + return blobs; + } + + /// + /// Resolve relocations in the code section, shrinking the size of all ULEB relocations to their minimal size. + /// This requires code blobs to be pre-split so that we can shrink the size of relocs in each blob independently, and then re-encode the blob with its new size. + /// + // We use an in-place copying strategy here with a read cursor (sectionStream.Position) and a separate write cursor where (write <= read), + // since the resolved blobs will always be equal to or smaller in size than the original blobs. + // Within the blobs, we split on relocations and copy the data between them, resolving each relocation to its minimal size. + private void ResolveCodeRelocations(int sectionIndex, MemoryStream sectionStream, List blobs, List relocs, bool shrink = false) { + if (blobs.Count == 0 && relocs.Count > 0) + { + throw new InvalidDataException(); + } + + long maxBlobSize = blobs.Max(blob => blob.End - blob.Start); + MemoryStream tempStream = new MemoryStream((int)maxBlobSize); byte[] relocScratchBuffer = new byte[Relocation.MaxSize]; + int[] blobShrink = new int[blobs.Count]; + + blobs.Sort((a, b) => a.Start.CompareTo(b.Start)); + relocs.Sort((a, b) => a.Offset.CompareTo(b.Offset)); + + long writeCursor = 0; + int relocCursor = 0; + + byte[] countBuffer = new byte[5]; + // Invariant: writeCursor is where we are writing to in the sectionStream. Further, writeCursor is always less than or equal to the start of the current blob we are processing. + for (int b = 0; b < blobs.Count; b++) + { + CodeBlob blob = blobs[b]; + Debug.Assert(writeCursor <= blobs[b].Start, $"Write cursor {writeCursor} is beyond the start of blob {blobs[b].Start}"); + + bool hasRelocs = relocCursor < relocs.Count && relocs[relocCursor].Offset >= blob.Start && relocs[relocCursor].Offset < blob.End; + if (hasRelocs) + { + tempStream.Position = 0; + tempStream.SetLength(blob.Size); + sectionStream.Position = blob.Start; // sectionStream.Position is now our read cursor + SymbolicRelocation firstReloc = relocs[relocCursor]; + + if (firstReloc.Offset > 0) + { + // Copy the initial data in the blob before the first relocation + int initialSize = (int)firstReloc.Offset - (int)blob.Start; + CopyOnly(sectionStream, sectionStream.Position, tempStream, tempStream.Position, initialSize); + sectionStream.Position += initialSize; + tempStream.Position += initialSize; + } + Debug.Assert(sectionStream.Position == firstReloc.Offset, $"Section stream position sectionStream.Position does not match first reloc offset {firstReloc.Offset}"); + + while (relocCursor < relocs.Count && relocs[relocCursor].Offset < blob.End) + { + SymbolicRelocation curReloc = relocs[relocCursor]; + SymbolicRelocation? nextReloc = null; + // look ahead to the next relocation, if any, to determine how much data is between this relocation and the next one + if (relocCursor + 1 < relocs.Count && relocs[relocCursor + 1].Offset < blob.End) + { + nextReloc = relocs[relocCursor + 1]; + } + + int size = ResolveReloc(sectionIndex, sectionStream, curReloc.Offset, tempStream, tempStream.Position, curReloc, relocScratchBuffer, shrink: shrink); + blobShrink[b] += (int)Relocation.GetSize(curReloc.Type) - size; + + long nextStart = curReloc.Offset + Relocation.GetSize(curReloc.Type); + long nextEnd = nextReloc is not null ? nextReloc.Offset : blob.End; + long betweenSize = nextEnd - nextStart; + + Debug.Assert(nextStart == sectionStream.Position); + CopyOnly(sectionStream, sectionStream.Position, tempStream, tempStream.Position, (int)betweenSize); + sectionStream.Position += betweenSize; + tempStream.Position += betweenSize; + relocCursor++; + } + + Debug.Assert(tempStream.Position <= blob.Size && blob.Size <= tempStream.Length, $"Temp stream position {tempStream.Position} exceeds blob size {blob.Size}"); + + tempStream.SetLength(tempStream.Position); + + // Write the temp stream back into the original stream with a NEW length prefix, starting at writeCursor + DwarfHelper.WriteULEB128(countBuffer, (ulong)tempStream.Length); + sectionStream.Position = writeCursor; + sectionStream.Write(countBuffer, 0, (int)DwarfHelper.SizeOfULEB128((ulong)tempStream.Length)); + writeCursor = sectionStream.Position; // set writeCursor to the position after the length prefix we just wrote + + tempStream.Position = 0; + tempStream.CopyTo(sectionStream); + + writeCursor += tempStream.Length; + } + else + { + // No relocations in this blob. Copy the blob as-is but shrink the length prefix if possible. + DwarfHelper.WriteULEB128(countBuffer, (ulong)blob.Size); + sectionStream.Position = writeCursor; + sectionStream.Write(countBuffer, 0, (int)DwarfHelper.SizeOfULEB128((ulong)blob.Size)); + writeCursor = sectionStream.Position; + + CopyOnly(src: sectionStream, srcPos: blob.Start, dest: sectionStream, destPos: writeCursor, count: blob.Size); + writeCursor += blob.Size; + } + } + sectionStream.SetLength(writeCursor); + + sectionStream.Position = 0; + +#if DEBUG + // The number of code blobs should not have changed. + List newBlobs = ParseCodeBlobs(sectionStream); + Debug.Assert(newBlobs.Count == blobs.Count); + for (int i = 0; i < newBlobs.Count; i++) + { + Debug.Assert(newBlobs[i].Size + blobShrink[i] == blobs[i].Size); + } +#endif + } + + private unsafe int ResolveReloc(int sectionIndex, MemoryStream sourceStream, long srcPos, MemoryStream destStream, long destPos, SymbolicRelocation reloc, byte[] relocScratchBuffer, bool shrink = false) + { WebcilSection? curSectionAsWebcil = null; uint webcilVirtualStart = 0; if (_sections[sectionIndex] is WebcilSection curSection) @@ -873,159 +1013,218 @@ private unsafe void ResolveRelocations(int sectionIndex, MemoryStream sectionStr webcilVirtualStart = curSection.Header.VirtualAddress; } - // If we have a webcil section, we expect it to have a nonzero section start. This is because for webcil, - // we should have written the webcil header and each of the section headers (always non-zero size) before any - // section contents - Debug.Assert(curSectionAsWebcil is null || sectionStart != 0); - - foreach (SymbolicRelocation reloc in relocs) + int size = Relocation.GetSize(reloc.Type); + if (size > relocScratchBuffer.Length) { - int size = Relocation.GetSize(reloc.Type); - if (size > relocScratchBuffer.Length) - { - throw new InvalidOperationException($"Unsupported relocation size for relocation: {reloc.Type}"); - } + throw new InvalidOperationException($"Unsupported relocation size for relocation: {reloc.Type}"); + } - SymbolDefinition definedSymbol = _definedSymbols[reloc.SymbolName]; + SymbolDefinition definedSymbol = _definedSymbols[reloc.SymbolName]; - // The virtual address of the relocation we are resolving - uint virtualRelocOffset = 0; - if (curSectionAsWebcil is not null) - { - virtualRelocOffset = webcilVirtualStart + (uint)reloc.Offset; - Debug.Assert(IsWithinSection(virtualRelocOffset, curSectionAsWebcil)); - } + // The virtual address of the relocation we are resolving + uint virtualRelocOffset = 0; + if (curSectionAsWebcil is not null) + { + virtualRelocOffset = webcilVirtualStart + (uint)reloc.Offset; + Debug.Assert(IsWithinSection(virtualRelocOffset, curSectionAsWebcil)); + } - // The virtual address of the symbol this relocation refers to - uint virtualSymbolImageOffset = 0; - WebcilSection? symbolWebcilSection = null; + // The virtual address of the symbol this relocation refers to + uint virtualSymbolImageOffset = 0; + WebcilSection? symbolWebcilSection = null; - // TODO-Wasm: Enforce the below boolean as an assert once we are emitting proper Wasm code - // relocs for all code containing nodes - // ---> bool betweenWebcilSections = false; - if (_sections[definedSymbol.SectionIndex] is WebcilSection targetSection) - { - symbolWebcilSection = targetSection; - virtualSymbolImageOffset = symbolWebcilSection.Header.VirtualAddress + (uint)definedSymbol.Value; - Debug.Assert(IsWithinSection(virtualSymbolImageOffset, symbolWebcilSection)); - } + if (_sections[definedSymbol.SectionIndex] is WebcilSection targetSection) + { + symbolWebcilSection = targetSection; + virtualSymbolImageOffset = symbolWebcilSection.Header.VirtualAddress + (uint)definedSymbol.Value; + Debug.Assert(IsWithinSection(virtualSymbolImageOffset, symbolWebcilSection)); + } - // We need a pinned raw pointer here for manipulation with Relocation.WriteValue - fixed (byte* pData = ReadRelocToDataSpan(reloc, relocScratchBuffer, sectionStart)) - { - long addend = Relocation.ReadValue(reloc.Type, pData); - int relocLength = Relocation.GetSize(reloc.Type); + // We need a pinned raw pointer here for manipulation with Relocation.WriteValue + fixed (byte* pData = ReadRelocToDataSpan(reloc, relocScratchBuffer)) + { + long addend = Relocation.ReadValue(reloc.Type, pData); + int relocLength = Relocation.GetSize(reloc.Type); + int? actualLength = null; - switch (reloc.Type) + switch (reloc.Type) + { + case RelocType.WASM_TYPE_INDEX_LEB: + case RelocType.WASM_GLOBAL_INDEX_LEB: + case RelocType.WASM_TABLE_INDEX_I32: + case RelocType.WASM_TABLE_INDEX_I64: + case RelocType.WASM_TABLE_INDEX_SLEB: + case RelocType.WASM_TABLE_INDEX_REL_I32: + case RelocType.WASM_FUNCTION_INDEX_LEB: { - case RelocType.WASM_TYPE_INDEX_LEB: - case RelocType.WASM_GLOBAL_INDEX_LEB: - case RelocType.WASM_TABLE_INDEX_I32: - case RelocType.WASM_TABLE_INDEX_I64: - case RelocType.WASM_TABLE_INDEX_SLEB: - case RelocType.WASM_TABLE_INDEX_REL_I32: - case RelocType.WASM_FUNCTION_INDEX_LEB: + // These relocations reference a wasm structural index (function, type, + // table entry, or well-known global). For R2R we self-resolve them here to + // the index assigned when the symbol was registered into its index space. + if (!_wasmSymbolManager.TryGetSymbol(reloc.SymbolName, out WasmSymbol symbol)) + { + throw new InvalidOperationException($"Symbol '{reloc.SymbolName}' was not registered. Relocation type {reloc.Type}."); + } + + if (shrink && Relocation.IsVariableLength(reloc.Type)) + { + actualLength = Relocation.WriteVariableLengthValue(reloc.Type, pData, symbol.Index + addend); + } + else { - // These relocations reference a wasm structural index (function, type, - // table entry, or well-known global). For R2R we self-resolve them here to - // the index assigned when the symbol was registered into its index space. - if (!_wasmSymbolManager.TryGetSymbol(reloc.SymbolName, out WasmSymbol symbol)) - { - throw new InvalidOperationException($"Symbol '{reloc.SymbolName}' was not registered. Relocation type {reloc.Type}."); - } Relocation.WriteValue(reloc.Type, pData, symbol.Index + addend); - break; } + break; + } - case RelocType.IMAGE_REL_BASED_ABSOLUTE: - // No action required - break; - - case RelocType.IMAGE_REL_BASED_DIR64: - case RelocType.IMAGE_REL_BASED_HIGHLOW: - // This is an ImageBase-relative value in PE, but our image base - // for Webcil is virtual address 0 - Debug.Assert(symbolWebcilSection != null); - Relocation.WriteValue(reloc.Type, pData, virtualSymbolImageOffset + 0 + addend); - break; - case RelocType.IMAGE_REL_BASED_ADDR32NB: - Debug.Assert(symbolWebcilSection != null); - Relocation.WriteValue(reloc.Type, pData, virtualSymbolImageOffset + addend); - break; - case RelocType.IMAGE_REL_BASED_REL32: - case RelocType.IMAGE_REL_BASED_RELPTR32: - Debug.Assert(symbolWebcilSection != null); - Relocation.WriteValue(reloc.Type, pData, virtualSymbolImageOffset - (virtualRelocOffset + relocLength) + addend); - break; - case RelocType.IMAGE_REL_FILE_ABSOLUTE: - Debug.Assert(symbolWebcilSection != null); - long fileOffset = symbolWebcilSection.Header.PointerToRawData + definedSymbol.Value; - Relocation.WriteValue(reloc.Type, pData, fileOffset + addend); - break; - case RelocType.WASM_MEMORY_ADDR_REL_SLEB: + case RelocType.IMAGE_REL_BASED_ABSOLUTE: + // No action required + break; + + case RelocType.IMAGE_REL_BASED_DIR64: + case RelocType.IMAGE_REL_BASED_HIGHLOW: + // This is an ImageBase-relative value in PE, but our image base + // for Webcil is virtual address 0 + Debug.Assert(symbolWebcilSection != null); + Relocation.WriteValue(reloc.Type, pData, virtualSymbolImageOffset + 0 + addend); + break; + case RelocType.IMAGE_REL_BASED_ADDR32NB: + Debug.Assert(symbolWebcilSection != null); + Relocation.WriteValue(reloc.Type, pData, virtualSymbolImageOffset + addend); + break; + case RelocType.IMAGE_REL_BASED_REL32: + case RelocType.IMAGE_REL_BASED_RELPTR32: + Debug.Assert(symbolWebcilSection != null); + Relocation.WriteValue(reloc.Type, pData, virtualSymbolImageOffset - (virtualRelocOffset + relocLength) + addend); + break; + case RelocType.IMAGE_REL_FILE_ABSOLUTE: + Debug.Assert(symbolWebcilSection != null); + long fileOffset = symbolWebcilSection.Header.PointerToRawData + definedSymbol.Value; + Relocation.WriteValue(reloc.Type, pData, fileOffset + addend); + break; + case RelocType.WASM_MEMORY_ADDR_REL_SLEB: + { + // These relocs should be for cases of the form: + // global.get $imageBase + // i32.const + // i32.add + // i32.load 0 + // So, the relocated address value should always represent an offset relative to image base. + // This offset should ALWAYS be equal to the actual offset from image base at runtime, due to Webcil's + // flag mapping + if (symbolWebcilSection is null) { - // These relocs should be for cases of the form: - // global.get $imageBase - // i32.const - // i32.add - // i32.load 0 - // So, the relocated address value should always represent an offset relative to image base. - // This offset should ALWAYS be equal to the actual offset from image base at runtime, due to Webcil's - // flag mapping - if (symbolWebcilSection is null) - { - throw new InvalidDataException($"WASM_MEMORY_ADDR_REL_SLEB: symbol '{reloc.SymbolName}' (sectionIndex {definedSymbol.SectionIndex}, section type {_sections[definedSymbol.SectionIndex]?.GetType().Name}) is not in a WebcilSection. Reloc in section {sectionIndex} ({_sections[sectionIndex]?.GetType().Name}), offset {reloc.Offset:X}."); - } + throw new InvalidDataException($"WASM_MEMORY_ADDR_REL_SLEB: symbol '{reloc.SymbolName}' (sectionIndex {definedSymbol.SectionIndex}, section type {_sections[definedSymbol.SectionIndex]?.GetType().Name}) is not in a WebcilSection. Reloc in section {sectionIndex} ({_sections[sectionIndex]?.GetType().Name}), offset {reloc.Offset:X}."); + } + if (shrink) + { + actualLength = Relocation.WriteVariableLengthValue(reloc.Type, pData, virtualSymbolImageOffset + addend); + } + else + { Relocation.WriteValue(reloc.Type, pData, virtualSymbolImageOffset + addend); - break; } - case RelocType.WASM_MEMORY_ADDR_REL_LEB: + + break; + } + case RelocType.WASM_MEMORY_ADDR_REL_LEB: + { + // These relocs should be for cases of the form: + // global.get $imageBase + // i32.load + // So, the relocated address value should always represent an offset relative to image base. + // This offset should ALWAYS be equal to the actual offset from image base at runtime, due to Webcil's + // flag mapping + if (symbolWebcilSection is null) { - // These relocs should be for cases of the form: - // global.get $imageBase - // i32.load - // So, the relocated address value should always represent an offset relative to image base. - // This offset should ALWAYS be equal to the actual offset from image base at runtime, due to Webcil's - // flag mapping - if (symbolWebcilSection is null) - { - throw new InvalidDataException($"WASM_MEMORY_ADDR_REL_LEB: symbol '{reloc.SymbolName}' (sectionIndex {definedSymbol.SectionIndex}, section type {_sections[definedSymbol.SectionIndex]?.GetType().Name}) is not in a WebcilSection. Reloc in section {sectionIndex} ({_sections[sectionIndex]?.GetType().Name}), offset {reloc.Offset:X}."); - } + throw new InvalidDataException($"WASM_MEMORY_ADDR_REL_LEB: symbol '{reloc.SymbolName}' (sectionIndex {definedSymbol.SectionIndex}, section type {_sections[definedSymbol.SectionIndex]?.GetType().Name}) is not in a WebcilSection. Reloc in section {sectionIndex} ({_sections[sectionIndex]?.GetType().Name}), offset {reloc.Offset:X}."); + } + if (shrink) + { + actualLength = Relocation.WriteVariableLengthValue(reloc.Type, pData, virtualSymbolImageOffset + addend); + } + else + { Relocation.WriteValue(reloc.Type, pData, virtualSymbolImageOffset + addend); - break; } - case RelocType.WASM_CLR_RESTORE_CONTEXT_EXCEPTION_TAG_LEB: + + break; + } + case RelocType.WASM_CLR_RESTORE_CONTEXT_EXCEPTION_TAG_LEB: + { + WasmSymbol symbol = _wasmSymbolManager.GetSymbol(RtlRestoreContextTagName); + Debug.Assert(symbol.IndexSpace == WasmIndexSpace.Tag); + if (shrink) + { + actualLength = Relocation.WriteVariableLengthValue(reloc.Type, pData, symbol.Index + addend); + } + else { - WasmSymbol symbol = _wasmSymbolManager.GetSymbol(RtlRestoreContextTagName); - Debug.Assert(symbol.IndexSpace == WasmIndexSpace.Tag); Relocation.WriteValue(reloc.Type, pData, symbol.Index + addend); - break; } - default: - // TODO-WASM: add other cases as needed; - // ignoring other reloc types for now - throw new NotSupportedException($"Relocation type {reloc.Type} not yet implemented"); + break; } - - WriteRelocFromDataSpan(reloc, pData, sectionStart); + default: + // TODO-WASM: add other cases as needed; + // ignoring other reloc types for now + throw new NotSupportedException($"Relocation type {reloc.Type} not yet implemented"); } + + return WriteRelocFromDataSpan(reloc, pData, actualLength ?? relocLength); } - Span ReadRelocToDataSpan(SymbolicRelocation reloc, byte[] buffer, long sectionStart) + Span ReadRelocToDataSpan(SymbolicRelocation reloc, byte[] buffer) { Span relocContents = buffer.AsSpan(0, Relocation.GetSize(reloc.Type)); - sectionStream.Position = reloc.Offset + sectionStart; - sectionStream.ReadExactly(relocContents); + sourceStream.Position = srcPos; + sourceStream.ReadExactly(relocContents); return relocContents; } - void WriteRelocFromDataSpan(SymbolicRelocation reloc, byte* pData, long sectionStart) + int WriteRelocFromDataSpan(SymbolicRelocation reloc, byte* pData, int length) + { + destStream.Position = destPos; + destStream.Write(new Span(pData, length)); + return length; + } + } + + private void ResolveRelocations(int sectionIndex, Stream sectionStream, MemoryStream dstStream, List relocs, long sectionStart = 0, bool shrink = false) + { + Debug.Assert(sectionStream.CanSeek); + Debug.Assert(sectionStream.Length >= 0); + + if (relocs.Count == 0) + { + sectionStream.CopyTo(dstStream); + return; + } + + if (shrink && _sections[sectionIndex] is WasmSection { Type: WasmSectionType.Code }) + { + sectionStream.Position = 0; + sectionStream.CopyTo(dstStream); + + dstStream.Position = 0; + List blobs = ParseCodeBlobs(dstStream); + + dstStream.Position = 0; + ResolveCodeRelocations(sectionIndex, dstStream, blobs, relocs, shrink); + return; + } + + byte[] relocScratchBuffer = new byte[Relocation.MaxSize]; + + // Otherwise, we can resolve relocations on top of the copied in section stream, since the size and layout of the stream won't be changing. + long startPos = dstStream.Position; + sectionStream.CopyTo(dstStream); + for (int i = 0; i < relocs.Count; i++) { - sectionStream.Position = reloc.Offset + sectionStart; - sectionStream.Write(new Span(pData, Relocation.GetSize(reloc.Type))); + SymbolicRelocation reloc = relocs[i]; + ResolveReloc(sectionIndex, dstStream, srcPos: sectionStart + reloc.Offset, dstStream, destPos: sectionStart + reloc.Offset, reloc, relocScratchBuffer); } + dstStream.Position = sectionStream.Length + startPos; } #nullable disable diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 98aa4d34c2661b..61e842aa92698f 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -111,13 +111,14 @@ static void Validate(ReadyToRunReader reader) Assert.True(WasmR2RAssert.WasmIndexSpacesHaveExpectedEntries(webcilReader, out string indexDiagnostic), indexDiagnostic); // The wasm JIT references the ABI well-known globals via maximally padded WASM_GLOBAL_INDEX_LEB - // relocations that the R2R object writer must self-resolve back to the fixed global - // indices. Verify the emitted code contains a correctly self-resolved 'global.get' for the + // relocations that the R2R object writer must self-resolve to the fixed global + // indices and shrink down to their minimal size. Verify the emitted code contains a correctly self-resolved 'global.get' for the // image base (1, materialized by static-data reads in SumStaticData) and the table base // (2, materialized by the try/finally funclet path in SumWithFinally). Each pattern encodes - // the exact resolved index, so a regression in self-resolution changes it (or makes - // crossgen2 throw while emitting the method). The stack-pointer well-known global is passed to - // managed methods as a parameter in R2R, so it is not referenced via 'global.get' here. + // the exact resolved index in its minimal form, so a regression in self-resolution changes + // it (or makes crossgen2 throw while emitting the method). The stack-pointer well-known global + // is passed to managed methods as a parameter in R2R, so it is not referenced via + // 'global.get' here. const int ImageBaseGlobal = 1; const int TableBaseGlobal = 2; Assert.True(WasmR2RAssert.WasmImageContainsWellKnownGlobalGet(webcilReader, ImageBaseGlobal), diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/WasmR2RAssert.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/WasmR2RAssert.cs index ce08f92db0a3e2..acf4a493508750 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/WasmR2RAssert.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/WasmR2RAssert.cs @@ -14,36 +14,26 @@ internal static class WasmR2RAssert { /// /// Returns true if any WASM function body in the image contains a global.get of the - /// given ABI well-known-global index, emitted as a maximally padded 5-byte - /// WASM_GLOBAL_INDEX_LEB reference (the global.get opcode 0x23 followed - /// by the 5-byte padded ULEB128 of the index). + /// given ABI well-known-global index (the global.get opcode 0x23 followed + /// by the minimally encoded ULEB128 index). /// /// /// The wasm JIT references only the three ABI well-known globals (0 = stack pointer, 1 = image base, - /// 2 = table base) in this padded form; ordinary global.get instructions use the minimal - /// LEB128 encoding. The R2R object writer self-resolves the relocation in place, so after - /// compilation the padded slot holds the fixed index, e.g. image base -> - /// 23 81 80 80 80 00 and table base -> 23 82 80 80 80 00. This is a regression - /// smoke check for that self-resolution: it scans raw instruction bytes and does not decode - /// wasm instruction boundaries. + /// 2 = table base) through padded relocations. The R2R object writer self-resolves and shrinks those + /// relocations, so after compilation the instruction contains the fixed index in its minimal form, + /// e.g. image base -> 23 01 and table base -> 23 02. This is a regression smoke + /// check for that self-resolution: it scans raw instruction bytes and does not decode wasm + /// instruction boundaries. /// public static bool WasmImageContainsWellKnownGlobalGet(WebcilImageReader reader, int wellKnownGlobalIndex) { - // The well-known globals are 0/1/2, which all fit in a single ULEB128 payload byte. The padded - // encoding below only writes that single payload byte, so it is correct for indices <= 0x7F. + // The well-known globals are 0/1/2, which all fit in a single ULEB128 byte. Debug.Assert((uint)wellKnownGlobalIndex <= 0x7F, $"Only single-byte well-known-global indices are supported; got {wellKnownGlobalIndex}."); - // global.get (0x23) followed by the 5-byte padded ULEB128 of wellKnownGlobalIndex. Padding sets - // the continuation bit on the first four bytes and clears the last, so a small index N - // encodes as (N | 0x80), 0x80, 0x80, 0x80, 0x00. - Span pattern = stackalloc byte[6]; + Span pattern = stackalloc byte[2]; pattern[0] = 0x23; - pattern[1] = (byte)((wellKnownGlobalIndex & 0x7F) | 0x80); - pattern[2] = 0x80; - pattern[3] = 0x80; - pattern[4] = 0x80; - pattern[5] = 0x00; + pattern[1] = (byte)wellKnownGlobalIndex; for (int functionIndex = 0; ; functionIndex++) {