Skip to content

Commit 129f8a3

Browse files
Use Virtual IPs in R2R format (#128423)
## Summary This PR converts the WASM ReadyToRun format to use virtual IPs for addressing instead of direct function table indices, and updates R2RDump to correctly handle the new format. ## Changes ### RUNTIME_FUNCTION format changes - **BeginAddress** now encodes a virtual IP (bits 30:0) and an **IsFunclet** flag (bit 31) - A sentinel entry (0xFFFFFFFF) followed by a 4-byte **min function table index** is appended after the RUNTIME_FUNCTION table entries - **UnwindInfo** now encodes frame size (ULEB128) and virtual IP count / 2 (ULEB128) ### R2RDump fixes - Mask bit 31 (funclet flag) when parsing WASM RUNTIME_FUNCTION entries - WASM-specific display showing VirtualIP, IsFunclet flag, and size in virtual IPs - Dump the min function table index after the RuntimeFunctions table sentinel - Fix WASM disassembly function resolution: virtual IP → table index (via minFunctionTableIndex + runtimeFunctionId) → global function index (via elem section) → code section index (subtract imported function count) - Handle both active (flags==0) and passive (flags==1) WASM elem segments ### ReadyToRun format documentation - Added WASM-specific RUNTIME_FUNCTION section to readytorun-format.md - Documented UnwindInfo encoding, sentinel entry, and min function table index trailer ## Known Issues - **GCDecoder** is not yet handled correctly for the new WASM format. This will be addressed in a follow-up PR. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 518c8cb commit 129f8a3

35 files changed

Lines changed: 1147 additions & 129 deletions

docs/design/coreclr/botr/readytorun-format.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,36 @@ which encodes an extra 4-byte representing the end RVA of the unwind info blob.
413413
| 4 | 4 | Unwind info end RVA (1 plus RVA of last byte)
414414
| 8 | 4 | GC info start RVA
415415

416+
### RUNTIME_FUNCTION (wasm, size = 8 bytes)
417+
418+
On WebAssembly, the `RUNTIME_FUNCTION` uses a virtual IP as the `BeginAddress` rather than an RVA
419+
into the image. The high bit of the `BeginAddress` field indicates whether the entry represents a
420+
funclet (1) or a main method body (0). The remaining 31 bits encode the virtual IP of the start of
421+
the function or funclet.
422+
423+
| Offset | Size | Value
424+
|-------:|-----:|:-----
425+
| 0 | 4 | Virtual IP (bits 30:0) &#124; IsFunclet flag (bit 31)
426+
| 4 | 4 | UnwindData RVA (GC info follows immediately after the unwind blob)
427+
428+
The table is terminated by a sentinel entry with all bits set (`0xFFFFFFFF`), followed by a 4-byte
429+
value containing the minimum WebAssembly function table index for the image.
430+
431+
### UnwindInfo (wasm)
432+
433+
On WebAssembly, the unwind info blob associated with each `RUNTIME_FUNCTION` entry is encoded as
434+
two consecutive ULEB128 values:
435+
436+
| Order | Encoding | Value
437+
|------:|:---------|:-----
438+
| 1 | ULEB128 | Frame size in bytes (the number of bytes to unwind from the stack)
439+
| 2 | ULEB128 | Virtual IP count divided by 2 (the number of virtual IPs logically present in the function, halved)
440+
441+
The virtual IP count (after multiplying by 2) gives the span of virtual IPs covered by this
442+
function or funclet. All virtual IPs are forced to even numbers so that the runtime can force all virtual
443+
ips to have odd numbers and fit into the address space in a manner which cannot conflict with either interpreter
444+
IPs or PortableEntryPoint structures.
445+
416446
## ReadyToRunSectionType.MethodDefEntryPoints
417447

418448
This section contains a native format sparse array (see 4 Native Format) that maps methoddef rows to

src/coreclr/clrdefinitions.cmake

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ add_definitions(-DFEATURE_READYTORUN)
163163

164164
set(FEATURE_READYTORUN 1)
165165

166+
if(NOT CLR_CMAKE_TARGET_ARCH_WASM)
167+
add_compile_definitions(FEATURE_COLD_R2R_CODE)
168+
endif()
169+
166170
if(FEATURE_REJIT)
167171
add_compile_definitions(FEATURE_REJIT)
168172
endif()

src/coreclr/inc/clrnt.h

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ RtlVirtualUnwind(
521521
#define UNW_FLAG_EHANDLER 0x1 /* filter handler */
522522
#define UNW_FLAG_UHANDLER 0x2 /* unwind handler */
523523

524-
inline PEXCEPTION_ROUTINE
524+
PEXCEPTION_ROUTINE
525525
RtlVirtualUnwind (
526526
_In_ DWORD HandlerType,
527527
_In_ DWORD ImageBase,
@@ -531,24 +531,18 @@ RtlVirtualUnwind (
531531
_Out_ PVOID *HandlerData,
532532
_Out_ PDWORD EstablisherFrame,
533533
__inout_opt PT_KNONVOLATILE_CONTEXT_POINTERS ContextPointers
534-
)
535-
{
536-
PORTABILITY_ASSERT("The function RtlVirtualUnwind is not implemented on wasm");
537-
return nullptr;
538-
}
534+
);
535+
536+
UINT32 DecodeULEB128AsU32(PTR_BYTE* ppData);
539537

540-
FORCEINLINE
541538
ULONG
542539
RtlpGetFunctionEndAddress (
543540
_In_ PT_RUNTIME_FUNCTION FunctionEntry,
544541
_In_ TADDR ImageBase
545-
)
546-
{
547-
PORTABILITY_ASSERT("The function RtlpGetFunctionEndAddress is not implemented on wasm");
548-
return 0;
549-
}
542+
);
550543

551-
#define RUNTIME_FUNCTION__BeginAddress(FunctionEntry) ((FunctionEntry)->BeginAddress)
544+
#define RUNTIME_FUNCTION__IsFunclet(FunctionEntry) ((FunctionEntry)->BeginAddress & 0x80000000)
545+
#define RUNTIME_FUNCTION__BeginAddress(FunctionEntry) ((FunctionEntry)->BeginAddress & 0x7FFFFFFF)
552546
#define RUNTIME_FUNCTION__SetBeginAddress(FunctionEntry,address) ((FunctionEntry)->BeginAddress = (address))
553547

554548
#define RUNTIME_FUNCTION__EndAddress(FunctionEntry, ImageBase) (RtlpGetFunctionEndAddress(FunctionEntry, (ULONG64)(ImageBase)))

src/coreclr/tools/Common/Compiler/ObjectWriter/Dwarf/DwarfHelper.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@ public static void WritePaddedSLEB128(Span<byte> bytes, long value)
103103
}
104104
}
105105

106-
public static ulong ReadULEB128(ReadOnlySpan<byte> buffer)
106+
public static ulong ReadULEB128(ReadOnlySpan<byte> buffer) => ReadULEB128(buffer, out _);
107+
108+
public static ulong ReadULEB128(ReadOnlySpan<byte> buffer, out int bytesRead)
107109
{
108110
ulong value = 0;
109111
byte @byte;
@@ -116,10 +118,13 @@ public static ulong ReadULEB128(ReadOnlySpan<byte> buffer)
116118
shift += 7;
117119
} while ((@byte & 0x80) != 0);
118120

121+
bytesRead = pos;
119122
return value;
120123
}
121124

122-
public static long ReadSLEB128(ReadOnlySpan<byte> buffer)
125+
public static long ReadSLEB128(ReadOnlySpan<byte> buffer) => ReadSLEB128(buffer, out _);
126+
127+
public static long ReadSLEB128(ReadOnlySpan<byte> buffer, out int bytesRead)
123128
{
124129
ulong value = 0;
125130
byte @byte;
@@ -135,6 +140,7 @@ public static long ReadSLEB128(ReadOnlySpan<byte> buffer)
135140
if (((ulong)shift < (8 * sizeof(ulong))) && ((@byte & 0x40) != 0))
136141
value |= unchecked((ulong)(long)-1) << shift;
137142

143+
bytesRead = pos;
138144
return unchecked((long)value);
139145
}
140146
}

src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -928,31 +928,27 @@ private unsafe void ResolveRelocations(int sectionIndex, MemoryStream sectionStr
928928
break;
929929
}
930930

931-
// TODO-Wasm: None of the IMAGE_REL type relocs should occur in Wasm
932-
// code, and we should add asserts for this once we've updated the necessary
933-
// dependency nodes to emit the proper reloc type on Wasm.
934931
case RelocType.IMAGE_REL_BASED_ABSOLUTE:
935932
// No action required
936933
break;
937934

938935
case RelocType.IMAGE_REL_BASED_DIR64:
939936
case RelocType.IMAGE_REL_BASED_HIGHLOW:
940-
// Debug.Assert(betweenWebcilSections);
941937
// This is an ImageBase-relative value in PE, but our image base
942938
// for Webcil is virtual address 0
939+
Debug.Assert(symbolWebcilSection != null);
943940
Relocation.WriteValue(reloc.Type, pData, virtualSymbolImageOffset + 0 + addend);
944941
break;
945942
case RelocType.IMAGE_REL_BASED_ADDR32NB:
946-
// Debug.Assert(betweenWebcilSections);
943+
Debug.Assert(symbolWebcilSection != null);
947944
Relocation.WriteValue(reloc.Type, pData, virtualSymbolImageOffset + addend);
948945
break;
949946
case RelocType.IMAGE_REL_BASED_REL32:
950947
case RelocType.IMAGE_REL_BASED_RELPTR32:
951-
// Debug.Assert(betweenWebcilSections);
948+
Debug.Assert(symbolWebcilSection != null);
952949
Relocation.WriteValue(reloc.Type, pData, virtualSymbolImageOffset - (virtualRelocOffset + relocLength) + addend);
953950
break;
954951
case RelocType.IMAGE_REL_FILE_ABSOLUTE:
955-
// Debug.Assert(betweenWebcilSections && symbolWebcilSection != null);
956952
Debug.Assert(symbolWebcilSection != null);
957953
long fileOffset = symbolWebcilSection.Header.PointerToRawData + definedSymbol.Value;
958954
Relocation.WriteValue(reloc.Type, pData, fileOffset + addend);

src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ExceptionInfoLookupTableNode.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,20 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
118118
// Add the symbol representing this object node
119119
exceptionInfoLookupBuilder.AddSymbol(this);
120120

121+
bool isWasm = factory.Target.Architecture == TargetArchitecture.Wasm32;
122+
121123
// First, emit the actual EH records in sequence and store map from methods to the EH record symbols
122124
for (int index = 0; index < _methodNodes.Count; index++)
123125
{
124-
exceptionInfoLookupBuilder.EmitReloc(_methodNodes[index], RelocType.IMAGE_REL_BASED_ADDR32NB, -factory.Target.CodeDelta);
126+
if (isWasm)
127+
{
128+
// For WASM, we use a virtual IP in the table here instead of a method pointer, so that the runtime can perform a binary search based on the virtual IP. The virtual IP is emitted as a plain u32 (not a reloc).
129+
exceptionInfoLookupBuilder.EmitUInt(factory.RuntimeFunctionsTable.GetWasmVirtualIP(_methodNodes[index], 0));
130+
}
131+
else
132+
{
133+
exceptionInfoLookupBuilder.EmitReloc(_methodNodes[index], RelocType.IMAGE_REL_BASED_ADDR32NB, -factory.Target.CodeDelta);
134+
}
125135
exceptionInfoLookupBuilder.EmitReloc(_ehInfoNode, RelocType.IMAGE_REL_BASED_ADDR32NB, _ehInfoOffsets[index]);
126136
}
127137

src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ReadyToRunHeaderNode.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,8 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
192192
builder.EmitReloc(item.StartSymbol, RelocType.IMAGE_REL_BASED_ADDR32NB, rvaDelta);
193193

194194
// The header entry for the runtime functions table should not include the 4 byte 0xffffffff sentinel
195-
// value in the covered range.
196-
int delta = item.Id == ReadyToRunSectionType.RuntimeFunctions ? RuntimeFunctionsTableNode.SentinelSizeAdjustment : 0;
195+
// value in the covered range. For WASM, also excludes the trailing min table index.
196+
int delta = item.Id == ReadyToRunSectionType.RuntimeFunctions ? factory.RuntimeFunctionsTable.SentinelSizeAdjustment : 0;
197197
builder.EmitReloc(item.StartSymbol, RelocType.IMAGE_REL_SYMBOL_SIZE, delta);
198198

199199
count++;

src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/RuntimeFunctionsTableNode.cs

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66

7+
using ILCompiler.ObjectWriter;
78
using Internal.Text;
89
using Internal.TypeSystem;
910

@@ -15,6 +16,7 @@ public class RuntimeFunctionsTableNode : HeaderTableNode
1516
{
1617
private List<MethodWithGCInfo> _methodNodes;
1718
private Dictionary<MethodWithGCInfo, int> _insertedMethodNodes;
19+
private Dictionary<(MethodWithGCInfo method, int funcletIdx), uint> _methodToVirtualIP;
1820
private readonly NodeFactory _nodeFactory;
1921
private int _tableSize = -1;
2022

@@ -61,6 +63,58 @@ private void LayoutRuntimeFunctions()
6163
}
6264
}
6365

66+
public uint GetWasmVirtualIP(MethodWithGCInfo lookupMethod, int lookupFrameIndex)
67+
{
68+
bool isWasm = _nodeFactory.Target.Architecture == TargetArchitecture.Wasm32;
69+
70+
if (!isWasm)
71+
{
72+
// For non-WASM targets, virtual IPs are not used, so throw
73+
throw new InvalidOperationException("Virtual IPs are not used on non-WASM targets.");
74+
}
75+
76+
if (_methodToVirtualIP != null)
77+
{
78+
return _methodToVirtualIP[(lookupMethod, lookupFrameIndex)];
79+
}
80+
81+
if (_methodNodes == null)
82+
{
83+
LayoutRuntimeFunctions();
84+
}
85+
86+
Dictionary<(MethodWithGCInfo method, int funcletIdx), uint> methodToVirtualIP = new ();
87+
88+
// For WASM, track virtual IP index for each RUNTIME_FUNCTION entry
89+
uint currentVirtualIP = 0;
90+
91+
foreach (MethodWithGCInfo method in _methodNodes)
92+
{
93+
for (int frameIndex = 0; frameIndex < method.FrameInfos.Length; frameIndex++)
94+
{
95+
FrameInfo frameInfo = method.FrameInfos[frameIndex];
96+
methodToVirtualIP.Add((method, frameIndex), currentVirtualIP);
97+
98+
// Advance the virtual IP by the number of virtual IPs for this frame
99+
uint virtualIPCount = GetFunctionLocalVirtualIPCountFromUnwindBlob(frameInfo.BlobData);
100+
currentVirtualIP += virtualIPCount;
101+
}
102+
}
103+
104+
_methodToVirtualIP = methodToVirtualIP;
105+
return _methodToVirtualIP[(lookupMethod, lookupFrameIndex)];
106+
}
107+
108+
/// <summary>
109+
/// Read the virtual IP count from a WASM unwind blob.
110+
/// The blob format is: ULEB128(frameSize) followed by ULEB128(virtualIPCount).
111+
/// </summary>
112+
private static uint GetFunctionLocalVirtualIPCountFromUnwindBlob(byte[] blobData)
113+
{
114+
DwarfHelper.ReadULEB128(blobData.AsSpan(), out int offset); // skip frame size
115+
return (uint)DwarfHelper.ReadULEB128(blobData.AsSpan(offset), out _) * 2; // Multiply by 2 to force all virtual IPs to be an even number.
116+
}
117+
64118
public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
65119
{
66120
if (relocsOnly)
@@ -78,6 +132,8 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
78132
uint runtimeFunctionIndex = 0;
79133
List<uint> mapping = new List<uint>();
80134

135+
bool isWasm = _nodeFactory.Target.Architecture == TargetArchitecture.Wasm32;
136+
81137
for (int cold = 0; cold < 2; cold++)
82138
{
83139
foreach (MethodWithGCInfo method in _methodNodes)
@@ -124,9 +180,10 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
124180
symbol = method;
125181
}
126182

127-
if (_nodeFactory.Target.Architecture == TargetArchitecture.Wasm32)
183+
if (isWasm)
128184
{
129-
runtimeFunctionsBuilder.EmitReloc(symbol, RelocType.WASM_TABLE_INDEX_I32, frameIndex);
185+
// Set high bit for frame indices greater than 0 to indicate that the RUNTIME_FUNCTION is a funclet
186+
runtimeFunctionsBuilder.EmitUInt(GetWasmVirtualIP(method, frameIndex) | (frameIndex != 0 ? 0x80000000 : 0));
130187
}
131188
else
132189
{
@@ -157,14 +214,29 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
157214
// Emit sentinel entry
158215
runtimeFunctionsBuilder.EmitUInt(~0u);
159216

217+
if (isWasm)
218+
{
219+
// After sentinel, emit a WASM_TABLE_INDEX_I32 reloc pointing to the first
220+
// compiled method. This records the min function table index in the file.
221+
if (_methodNodes.Count > 0)
222+
{
223+
runtimeFunctionsBuilder.EmitReloc(_methodNodes[0], RelocType.WASM_TABLE_INDEX_I32, delta: 0);
224+
}
225+
else
226+
{
227+
// If there are no compiled methods, emit a 0 for the min table index.
228+
runtimeFunctionsBuilder.EmitUInt(0);
229+
}
230+
}
231+
160232
_tableSize = runtimeFunctionsBuilder.CountBytes;
161233
return runtimeFunctionsBuilder.ToObjectData();
162234
}
163235

164236
/// <summary>
165237
/// Returns the runtime functions table size and excludes the 4 byte sentinel entry at the end (used by
166238
/// the runtime in NativeUnwindInfoLookupTable::LookupUnwindInfoForMethod) so that it's not treated as
167-
/// part of the table itself.
239+
/// part of the table itself. For WASM, also excludes the trailing 4-byte min table index.
168240
/// </summary>
169241
public int TableSizeExcludingSentinel
170242
{
@@ -179,6 +251,7 @@ public int TableSizeExcludingSentinel
179251

180252
public override int ClassCode => (int)ObjectNodeOrder.RuntimeFunctionsTableNode;
181253

182-
internal const int SentinelSizeAdjustment = -4;
254+
internal int SentinelSizeAdjustment =>
255+
_nodeFactory.Target.Architecture == TargetArchitecture.Wasm32 ? -8 : -4;
183256
}
184257
}

src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -897,9 +897,12 @@ public void AttachToDependencyGraph(DependencyAnalyzerBase<NodeFactory> graph, I
897897
RuntimeFunctionsGCInfo = new RuntimeFunctionsGCInfoNode();
898898
graph.AddRoot(RuntimeFunctionsGCInfo, "GC info is always generated");
899899

900-
DelayLoadMethodCallThunks = new SymbolNodeRange("DelayLoadMethodCallThunkNodeRange");
901-
graph.AddRoot(DelayLoadMethodCallThunks, "DelayLoadMethodCallThunks header entry is always generated");
902-
Header.Add(Internal.Runtime.ReadyToRunSectionType.DelayLoadMethodCallThunks, DelayLoadMethodCallThunks);
900+
if (!Target.IsWasm)
901+
{
902+
DelayLoadMethodCallThunks = new SymbolNodeRange("DelayLoadMethodCallThunkNodeRange");
903+
graph.AddRoot(DelayLoadMethodCallThunks, "DelayLoadMethodCallThunks header entry is always generated");
904+
Header.Add(Internal.Runtime.ReadyToRunSectionType.DelayLoadMethodCallThunks, DelayLoadMethodCallThunks);
905+
}
903906

904907
ExceptionInfoLookupTableNode exceptionInfoLookupTableNode = new ExceptionInfoLookupTableNode(this);
905908
Header.Add(Internal.Runtime.ReadyToRunSectionType.ExceptionInfo, exceptionInfoLookupTableNode);

src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public static string GetPlatformSpecificRegister(Machine machine, int regnum)
8787
case Machine.RiscV64:
8888
return ((RiscV64.Registers)regnum).ToString();
8989
case WasmMachine.Wasm32:
90-
throw new NotImplementedException($"No implementation for machine type {machine}.");
90+
return $"NYI '{regnum}'"; // WASM-TODO Implement this correctly.
9191
default:
9292
throw new NotImplementedException($"No implementation for machine type {machine}.");
9393
}

0 commit comments

Comments
 (0)