Skip to content

[cDAC] Implement IXCLRDataProcess.EnumMethodInstancesByAddress #115131

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c6bf4e3
wip
Apr 17, 2025
e71f95e
wip
Apr 21, 2025
e21bd2e
wip
Apr 23, 2025
24c5549
wip
Apr 24, 2025
7aeaa51
implement IXCLRDataMethodInstance.GetRepresentativeEntryAddress and I…
Apr 24, 2025
c778639
wip
Apr 24, 2025
e73b92e
add missing data descriptor
Apr 25, 2025
af6fd4d
implement IntroducedMethodIterator
Apr 25, 2025
1f4ad2f
fix bugs in Getting methoddesc from slot
Apr 28, 2025
77a5ed0
add todo
Apr 28, 2025
42eb4f9
fix bug in GetInstantiatedMethods
Apr 28, 2025
1dadeb3
uncomment changes
Apr 28, 2025
55fbb58
try manually marshalling IXCLRDataMoldue
Apr 30, 2025
c8e1f00
fix manual com marshalling
Apr 30, 2025
d37b68e
fix
Apr 30, 2025
6e7368b
implement FCall lookup
May 1, 2025
cccf552
test and fix FCall lookup
May 2, 2025
555ad9c
add docs for and improve DacEnumerableHash table
May 5, 2025
e996b25
add docs related to ECall
May 5, 2025
6a1760c
fix loader.GetModules call
May 5, 2025
0385ade
add docs for RuntimeTypeSystem changes
May 5, 2025
e9c3195
MethodDesc::s_ClassificationSizeTable to heapdumps
May 6, 2025
ba1cadd
Revert "MethodDesc::s_ClassificationSizeTable to heapdumps"
May 6, 2025
ae58823
fix tests
May 6, 2025
33631e7
fix async v2 flags
May 15, 2025
00ac37e
Merge remote-tracking branch 'origin/main' into cdac-symbol-reading-3
max-charlamb Jun 23, 2025
c039dd6
add docs for MethodDescFlags.HasAsyncMethodData
max-charlamb Jun 23, 2025
9f677af
update to use ClrDataAddress
max-charlamb Jun 23, 2025
5048231
change todo message
max-charlamb Jun 23, 2025
d9bc9c4
Merge branch 'main' into cdac-symbol-reading-3
max-charlamb Jul 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/design/datacontracts/ECall.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Contract ECall

This contract is for fetching information related to native calls into the runtime.

## APIs of contract

``` csharp
// Given an FCall entrypoint returns the corresponding MethodDesc.
// If the address does not correspond to an FCall, returns TargetPointer.Null.
TargetPointer MapTargetBackToMethodDesc(TargetCodePointer address);
```

## Version 1

Global variables used
| Global Name | Type | Purpose |
| --- | --- | --- |
| FCallMethods | ECHash[] | Hash table containing ECHash structures |
| FCallHashSize | uint | Number of buckets in the hash table |


Data descriptors used:
| Data Descriptor Name | Field | Meaning |
| --- | --- | --- |
| `ECHash` | `Next` | Pointer to the next ECHash in the chain |
| `ECHash` | `Implementation` | FCall's Entrypoint address |
| `ECHash` | `MethodDesc` | Pointer to the FCall's method desc |


``` csharp
TargetPointer IECall.MapTargetBackToMethodDesc(TargetCodePointer codePointer)
```

To map an FCall entrypoint back to a MethodDesc, we read the global `FCallMethods` hash table. This is a array of pointers to `ECHash` objects. The length of this array is defined by the global `FCallHashSize` where each element is an `ECHash` which can form a chain. It uses a simple hash function: `<hash> = codePointer % FCallHashSize` to map code entry points to buckets. To map a `codePointer` back to a MethodDesc pointer:

1. Calculate the `<hash>` corresponding to the given `codePointer`.
2. Take the `<hash>` offset into the `FCallMethods` array.
3. Now that we have the correct `ECHash` chain, iterate the chain using the `ECHash.Next` pointer until we find an `ECHash` where the `Implementation` field matches the `codePointer`. If found, return the `MethodDesc` field.
4. If no `ECHash` matches return `TargetPointer.Null` to indicate a MethodDesc was not found.
107 changes: 107 additions & 0 deletions docs/design/datacontracts/Loader.md
Original file line number Diff line number Diff line change
@@ -59,6 +59,9 @@ TargetPointer GetAssembly(ModuleHandle handle);
TargetPointer GetPEAssembly(ModuleHandle handle);
bool TryGetLoadedImageContents(ModuleHandle handle, out TargetPointer baseAddress, out uint size, out uint imageFlags);
bool TryGetSymbolStream(ModuleHandle handle, out TargetPointer buffer, out uint size);
IEnumerable<TargetPointer> GetAvailableTypeParams(ModuleHandle handle);
IEnumerable<TargetPointer> GetInstantiatedMethods(ModuleHandle handle);

bool IsProbeExtensionResultValid(ModuleHandle handle);
ModuleFlags GetFlags(ModuleHandle handle);
string GetPath(ModuleHandle handle);
@@ -86,6 +89,8 @@ bool IsAssemblyLoaded(ModuleHandle handle);
| `Module` | `Path` | Path of the Module (UTF-16, null-terminated) |
| `Module` | `FileName` | File name of the Module (UTF-16, null-terminated) |
| `Module` | `GrowableSymbolStream` | Pointer to the in memory symbol stream |
| `Module` | `AvailableTypeParams` | Pointer to an EETypeHashTable |
| `Module` | `InstMethodHashTable` | Pointer to an InstMethodHashTable |
| `Module` | `FieldDefToDescMap` | Mapping table |
| `Module` | `ManifestModuleReferencesMap` | Mapping table |
| `Module` | `MemberRefToDescMap` | Mapping table |
@@ -118,6 +123,14 @@ bool IsAssemblyLoaded(ModuleHandle handle);
| `ArrayListBlock` | `Next` | Next ArrayListBlock in chain |
| `ArrayListBlock` | `Size` | Size of data section in block |
| `ArrayListBlock` | `ArrayStart` | Start of data section in block |
| `EETypeHashTable` | `Buckets` | Pointer to hash table buckets |
| `EETypeHashTable` | `Count` | Count of elements in the hash table |
| `EETypeHashTable` | `VolatileEntryValue` | The data stored in the hash table entry |
| `EETypeHashTable` | `VolatileEntryNextEntry` | Next pointer in the hash table entry |
| `InstMethodHashTable` | `Buckets` | Pointer to hash table buckets |
| `InstMethodHashTable` | `Count` | Count of elements in the hash table |
| `InstMethodHashTable` | `VolatileEntryValue` | The data stored in the hash table entry |
| `InstMethodHashTable` | `VolatileEntryNextEntry` | Next pointer in the hash table entry |


### Global variables used:
@@ -298,6 +311,30 @@ bool TryGetSymbolStream(ModuleHandle handle, out TargetPointer buffer, out uint
return true;
}

IEnumerable<TargetPointer> GetAvailableTypeParams(ModuleHandle handle)
{
TargetPointer availableTypeParams = target.ReadPointer(handle.Address + /* Module::AvailableTypeParams offset */);

if (availableTypeParams == TargetPointer.Null) return [];

// EETypeHashTable is read as a DacEnumerableHash table.
// For more information on how this is read, see section below.
EETypeHashTable typeHashTable = // read EETypeHashTable at availableTypeParams
return typeHashTable.Entries.Select(entry => entry.TypeHandle);
}

IEnumerable<TargetPointer> GetInstantiatedMethods(ModuleHandle handle)
{
TargetPointer instMethodHashTable = target.ReadPointer(handle.Address + /* Module::InstMethodHashTable offset */);

if (instMethodHashTable == TargetPointer.Null) return [];

// InstMethodHashTable is read as a DacEnumerableHash table.
// For more information on how this is read, see section below.
InstMethodHashTable methodHashTable = // read InstMethodHashTable at instMethodHashTable
return methodHashTable.Entries.Select(entry => entry.MethodDesc);
}

bool IsProbeExtensionResultValid(ModuleHandle handle)
{
TargetPointer peAssembly = target.ReadPointer(handle.Address + /* Module::PEAssembly offset */);
@@ -415,3 +452,73 @@ bool ILoader.IsAssemblyLoaded(ModuleHandle handle)
return assembly.Level >= ASSEMBLY_LEVEL_LOADED;
}
```

### DacEnumerableHash (EETypeHashTable and InstMethodHashTable)

Both `EETypeHashTable` and `InstMethodHashTable` are based on the templated `DacEnumerableHash`. Because the base class is templated on the derived type, offsets may be different in derived types.

The base implementation of `DacEnumerableHash` uses four datadescriptors:
| Datadescriptor | Purpose |
| --- | --- |
| `Buckets` | Pointer to the bucket array |
| `Count` | Number of elements in the hash table |
| `VolatileEntryValue` | The data held by an entry, defined by the derived class |
| `VolatileEntryNextEntry` | The next pointer on an hash table entry |

The hash table is laid out as an array of `VolatileEntry` pointers's (buckets), each possibly forming a chain for values that hash into that bucket. The first three buckets are special and reserved for metadata. Instead of containing a `VolatileEntry`, these pointers are read as values with the following meanings.

| Reserved Bucket offset | Purpose |
| --- | --- |
| `0` | Length of the Bucket array, this value does not include the first 3 slots which are special |
| `1` | Pointer to the next bucket array, not currently used in the cDAC |
| `2` | End sentinel for the current bucket array, not currently used in the cDAC |

The current cDAC implementation does not use the 'hash' part of the table at all. Instead it iterates all elements in the table. Following the existing iteration logic in the runtime (and DAC), resizing the table while iterating is not supported. Given this constraint, the pointer to the next bucket array (resized data table) and the current end sentinel are not required to iterate all entries.

To read all entries in the hash table:
1. Read the length bucket to find the number of chains `n`.
2. Initialize a list of elements `entries = []`.
3. For each chain, (buckets with offsets `3..n + 3`):
1. Read the pointer in the bucket as `volatileEntryPtr`.
2. If `volatileEntryPtr & 0x1 == 0x1`, this is an end sentinel and we stop reading this chain.
3. Otherwise, add `volatileEntryPtr + /* VolatileEntryValue offset */` to entries. This points to the derived class defined data type.
4. Set `volatileEntryPtr` to the value of the pointer located at `volatileEntryPtr + /* VolatileEntryNextEntry offset */` and go to step 3.2.
4. Return `entries` to be further parsed by derived classes.

While both EETypeHashTable and InstMethodHashTable store pointer sized data types, they both use the LSBs as special flags.

#### EETypeHashTable
EETypeHashTable uses the LSB to indicate if the TypeHandle is a hot entry. The cDAC implementation separates each value `value` in the table into two parts. The actual TypeHandle pointer and the associated flags.

```csharp
class EETypeHashTable
{
private const ulong FLAG_MASK = 0x1ul;

public IReadOnlyList<Entry> Entires { get; }

public readonly struct Entry(TargetPointer value)
{
public TargetPointer TypeHandle { get; } = value & ~FLAG_MASK;
public uint Flags { get; } = (uint)(value.Value & FLAG_MASK);
}
}
```

#### InstMethodHashTable
InstMethodHashTable uses the 2 LSBs as flags for the MethodDesc. The cDAC implementation separates each value `value` in the table into two parts. The actual MethodDesc pointer and the associated flags.

```csharp
class InstMethodHashTable
{
private const ulong FLAG_MASK = 0x3ul;

public IReadOnlyList<Entry> Entires { get; }

public readonly struct Entry(TargetPointer value)
{
public TargetPointer MethodDesc { get; } = value & ~FLAG_MASK;
public uint Flags { get; } = (uint)(value.Value & FLAG_MASK);
}
}
```
113 changes: 113 additions & 0 deletions docs/design/datacontracts/RuntimeTypeSystem.md
Original file line number Diff line number Diff line change
@@ -41,6 +41,8 @@ partial interface IRuntimeTypeSystem : IContract
public virtual TargetPointer GetCanonicalMethodTable(TypeHandle typeHandle);
public virtual TargetPointer GetParentMethodTable(TypeHandle typeHandle);

public virtual TargetPointer GetMethodDescForSlot(TypeHandle typeHandle, ushort slot);

public virtual uint GetBaseSize(TypeHandle typeHandle);
// The component size is only available for strings and arrays. It is the size of the element type of the array, or the size of an ECMA 335 character (2 bytes)
public virtual uint GetComponentSize(TypeHandle typeHandle);
@@ -345,6 +347,7 @@ The contract additionally depends on these data descriptors
| `MethodTable` | `PerInstInfo` | Either the array element type, or pointer to generic information for `MethodTable` |
| `EEClass` | `InternalCorElementType` | An InternalCorElementType uses the enum values of a CorElementType to indicate some of the information about the type of the type which uses the EEClass In particular, all reference types are CorElementType.Class, Enums are the element type of their underlying type and ValueTypes which can exactly be represented as an element type are represented as such, all other values types are represented as CorElementType.ValueType. |
| `EEClass` | `MethodTable` | Pointer to the canonical MethodTable of this type |
| `EEClass` | `MethodDescChunk` | Pointer to the first MethodDescChunk of the EEClass |
| `EEClass` | `NumMethods` | Count of methods attached to the EEClass |
| `EEClass` | `NumNonVirtualSlots` | Count of non-virtual slots for the EEClass |
| `EEClass` | `CorTypeAttr` | Various flags |
@@ -629,6 +632,8 @@ The version 1 `MethodDesc` APIs depend on the following globals:
| --- | --- |
| `MethodDescAlignment` | `MethodDescChunk` trailing data is allocated in multiples of this constant. The size (in bytes) of each `MethodDesc` (or subclass) instance is a multiple of this constant. |
| `MethodDescTokenRemainderBitCount` | Number of bits in the token remainder in `MethodDesc` |
| `MethodDescSizeTable` | A pointer to the MethodDesc size table. The MethodDesc flags are used as an offset into this table to lookup the MethodDesc size. |


In the runtime a `MethodDesc` implicitly belongs to a single `MethodDescChunk` and some common data is shared between method descriptors that belong to the same chunk. A single method table
will typically have multiple chunks. There are subkinds of MethodDescs at runtime of varying sizes (but the sizes must be mutliples of `MethodDescAlignment`) and each chunk contains method descriptors of the same size.
@@ -668,6 +673,9 @@ The contract depends on the following other contracts
| Loader |
| PlatformMetadata |
| ReJIT |
| ExecutionManager |
| PrecodeStubs |
| ECall |

And the following enumeration definitions

@@ -694,6 +702,7 @@ And the following enumeration definitions
HasNonVtableSlot = 0x0008,
HasMethodImpl = 0x0010,
HasNativeCodeSlot = 0x0020,
HasAsyncMethodData = 0x0040,
// Mask for the above flags
MethodDescAdditionalPointersMask = 0x0038,
#endredion Additional pointers
@@ -871,6 +880,23 @@ And the various apis are implemented with the following algorithms
return 0x06000000 | tokenRange | tokenRemainder;
}

public uint GetMethodDescSize(MethodDescHandle methodDescHandle)
{
MethodDesc methodDesc = _methodDescs[methodDescHandle.Address];

// the runtime generates a table to lookup the size of a MethodDesc based on the flags
// read the location of the table and index into it using certain bits of MethodDesc.Flags
TargetPointer methodDescSizeTable = target.ReadGlobalPointer(Constants.Globals.MethodDescSizeTable);

ushort arrayOffset = (ushort)(methodDesc.Flags & (ushort)(
MethodDescFlags.ClassificationMask |
MethodDescFlags.HasNonVtableSlot |
MethodDescFlags.HasMethodImpl |
MethodDescFlags.HasNativeCodeSlot |
MethodDescFlags.HasAsyncMethodData));
return target.Read<byte>(methodDescSizeTable + arrayOffset);
}

public bool IsArrayMethod(MethodDescHandle methodDescHandle, out ArrayFunctionType functionType)
{
MethodDesc methodDesc = _methodDescs[methodDescHandle.Address];
@@ -1161,3 +1187,90 @@ Getting the native code pointer for methods with a NativeCodeSlot or a stable en
return GetStableEntryPoint(methodDescHandle.Address, md);
}
```

Getting a MethodDesc for a certain slot in a MethodTable
```csharp
// Based on MethodTable::IntroducedMethodIterator
private IEnumerable<MethodDescHandle> GetIntroducedMethods(TypeHandle typeHandle)
{
// typeHandle must represent a MethodTable

EEClass eeClass = GetClassData(typeHandle);

// pointer to the first MethodDescChunk
TargetPointer chunkAddr = eeClass.MethodDescChunk;
while (chunkAddr != TargetPointer.Null)
{
MethodDescChunk chunk = // read Data.MethodDescChunk data from chunkAddr
TargetPointer methodDescPtr = chunk.FirstMethodDesc;

// chunk.Count is the number of MethodDescs in the chunk - 1
// add 1 to get the actual number of MethodDescs within the chunk
for (int i = 0; i < chunk.Count + 1; i++)
{
MethodDescHandle methodDescHandle = GetMethodDescHandle(methodDescPtr);

// increment pointer to the beginning of the next MethodDesc
methodDescPtr += GetMethodDescSize(methodDescHandle);
yield return methodDescHandle;
}

// go to the next chunk
chunkAddr = chunk.Next;
}
}

private readonly TargetPointer GetMethodDescForEntrypoint(TargetCodePointer pCode)
{
// Standard path, ask ExecutionManager for the MethodDesc
IExecutionManager executionManager = _target.Contracts.ExecutionManager;
if (executionManager.GetCodeBlockHandle(pCode) is CodeBlockHandle cbh)
{
TargetPointer methodDescPtr = executionManager.GetMethodDesc(cbh);
return methodDescPtr;
}

// FCall path, look up address in the FCall table using the ECall contract
{
TargetPointer methodDescPtr = _target.Contracts.ECall.MapTargetBackToMethodDesc(pCode);
if (methodDescPtr != TargetPointer.Null)
{
return methodDescPtr;
}
}

// Stub path, read address as a Precode and get the MethodDesc from it
{
TargetPointer methodDescPtr = _target.Contracts.PrecodeStubs.GetMethodDescFromStubAddress(pCode);
return methodDescPtr;
}
}

public TargetPointer GetMethodDescForSlot(TypeHandle methodTable, ushort slot)
{
if (!typeHandle.IsMethodTable())
throw new ArgumentException($"{nameof(typeHandle)} is not a MethodTable");

TargetPointer cannonMTPTr = GetCanonicalMethodTable(typeHandle);
TypeHandle canonMT = GetTypeHandle(cannonMTPTr);
TargetPointer slotPtr = GetAddressOfSlot(canonMT, slot);
TargetCodePointer pCode = _target.ReadCodePointer(slotPtr);

if (pCode == TargetCodePointer.Null)
{
// if pCode is null, we iterate through the method descs in the MT.
foreach (MethodDescHandle mdh in GetIntroducedMethods(typeHandle))
{
MethodDesc md = _methodDescs[mdh.Address];

// if a MethodDesc matches the slot, return that MethodDesc
if (md.Slot == slot)
{
return mdh.Address;
}
}
}

return GetMethodDescForEntrypoint(pCode);
}
```
1 change: 1 addition & 0 deletions src/coreclr/debug/runtimeinfo/contractpointerdata.cpp
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@
#include "cdacplatformmetadata.hpp"
#include "threads.h"
#include "vars.hpp"
#include "ecall.h"

extern "C"
{
1 change: 1 addition & 0 deletions src/coreclr/debug/runtimeinfo/contracts.jsonc
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@
{
"CodeVersions": 1,
"DacStreams": 1,
"ECall": 1,
"EcmaMetadata" : 1,
"Exception": 1,
"ExecutionManager": 2,
1 change: 1 addition & 0 deletions src/coreclr/debug/runtimeinfo/datadescriptor.cpp
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@
#include "configure.h"

#include "../debug/ee/debugger.h"
#include "ecall.h"

#ifdef HAVE_GCCOVER
#include "gccover.h"
Loading
Oops, something went wrong.
Loading
Oops, something went wrong.