Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
43 changes: 43 additions & 0 deletions docs/design/datacontracts/DebugInfo.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,3 +389,46 @@ Each variable entry in the Vars section is nibble-encoded as follows:
| `VLT_FIXED_VA` | offset (encoded unsigned) |

Signed integers are encoded using the same unsigned scheme, with the sign bit stored in bit 0 (`value = unsigned >> 1`, negate if `unsigned & 1`). On x86, stack offsets are DWORD-aligned and stored divided by `sizeof(DWORD)`.

### Async Suspension Point APIs (Version 2+)

We also support decoding async suspension points (and their captured continuation-object locals) from the `AsyncInfo` chunk of the debug info blob. The chunk is present only for methods that the JIT compiled with runtime-async suspension points; for all other methods, `AsyncInfoSize` is `0` in the FAT header and the API returns an empty list.

Additional types:
```csharp
// A native code location at which an async method may suspend, together with
// the continuation-object locals captured at that point.
public readonly struct AsyncSuspensionInfo
{
public uint NativeOffset { get; init; }
public IReadOnlyList<AsyncLocalInfo> Locals { get; init; }
}

// A single local captured into the continuation object at a suspension point.
public readonly struct AsyncLocalInfo
{
// Offset of the local within the continuation object's data area.
public uint Offset { get; init; }
// IL var number of the local (or a synthetic marker such as MAX_ILNUM-relative values).
public uint ILVarNumber { get; init; }
}
```

```csharp
IReadOnlyList<AsyncSuspensionInfo> GetAsyncSuspensionPoints(TargetCodePointer pCode);
```

### AsyncInfo Data Encoding

Each entry is nibble-encoded as follows:

1. `NumSuspensionPoints` — encoded unsigned 32-bit integer.
2. Total var count across all suspension points — encoded unsigned 32-bit integer. Informational only; the decoder reads it but does not need it, since the per-suspension-point counts read in step 3 already cover the entire flat var list.
3. For each of the `NumSuspensionPoints` suspension points (in order):
* `DiagnosticNativeOffset` — encoded signed delta from the previous suspension point's offset (the first delta is from `0`). Deltas are not required to be monotonic.
* `NumContinuationVars` — encoded unsigned 32-bit integer giving the number of continuation locals captured at this suspension point.
4. For each var (a single flat sequence, partitioned by the `NumContinuationVars` counts from step 3, in suspension-point order):
* `VarNumber - MAX_ILNUM` — encoded unsigned 32-bit integer. The `MAX_ILNUM` bias keeps the synthetic negative IL var numbers (e.g. `VARARGS_HND_ILNUM`, `RETBUF_ILNUM`, `TYPECTXT_ILNUM`) representable as unsigned values; the decoder reverses the bias by adding `MAX_ILNUM` back.
* `Offset` — encoded unsigned 32-bit integer giving the byte offset of the local within the continuation object's data area.

`AsyncSuspensionInfo.Locals[i]` for the `n`-th suspension point therefore corresponds to the `i`-th var in the flat sequence whose flat index is the prefix sum of `NumContinuationVars[0..n-1]` plus `i`.
30 changes: 30 additions & 0 deletions docs/design/datacontracts/Object.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ public readonly record struct DelegateInfo(
TargetCodePointer TargetMethodPtr,
DelegateType DelegateType);

// DiagnosticIP is TargetPointer.Null when the continuation has no ResumeInfo.
public readonly record struct ContinuationInfo(
TargetPointer Next,
TargetPointer DiagnosticIP,
uint State);

// Get the method table address for the object
TargetPointer GetMethodTableAddress(TargetPointer address);

Expand All @@ -37,6 +43,9 @@ int TryGetHashCode(TargetPointer address);
TargetPointer GetSyncBlockAddress(TargetPointer address);

DelegateInfo GetDelegateInfo(TargetPointer address);

// Get the linked-list / diagnostic-IP / state triple for a runtime-async continuation object.
ContinuationInfo GetContinuationInfo(TargetPointer address);
```

## Version 1
Expand All @@ -55,6 +64,10 @@ Data descriptors used:
| `Delegate` | `MethodPtr` | Primary method pointer |
| `Delegate` | `MethodPtrAux` | Auxiliary method pointer |
| `Delegate` | `InvocationCount` | Invocation count (non-zero for multicast/wrapper/unmanaged/special delegates) |
| `ContinuationObject` | `Next` | Pointer to the next continuation in the linked list |
| `ContinuationObject` | `ResumeInfo` | Pointer to the `ResumeInfo` for this suspension point (may be null) |
| `ContinuationObject` | `State` | State index identifying the suspension point within the resumed method |
| `AsyncResumeInfo` | `DiagnosticIP` | Native IP into the resumed method used for diagnostics (may be null) |

Global variables used:
| Global Name | Type | Purpose |
Expand Down Expand Up @@ -210,4 +223,21 @@ DelegateInfo GetDelegateInfo(TargetPointer address)

return new DelegateInfo(targetObject, targetMethodPtr, delegateType);
}

ContinuationInfo GetContinuationInfo(TargetPointer address)
{
TargetPointer next = target.ReadPointer(address + /* ContinuationObject::Next offset */);
TargetPointer resumeInfo = target.ReadPointer(address + /* ContinuationObject::ResumeInfo offset */);
uint state = (uint)target.Read<int>(address + /* ContinuationObject::State offset */);

// ResumeInfo may be null
TargetPointer diagnosticIP = resumeInfo != TargetPointer.Null
? target.ReadPointer(resumeInfo + /* AsyncResumeInfo::DiagnosticIP offset */)
: TargetPointer.Null;

return new ContinuationInfo(
Next: next,
DiagnosticIP: diagnosticIP,
State: state);
}
```
11 changes: 7 additions & 4 deletions src/coreclr/debug/daccess/dacdbiimpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7410,10 +7410,12 @@ static BYTE* DebugInfoStoreNew(void * pData, size_t cBytes)
return new (nothrow) BYTE[cBytes];
}

HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetAsyncLocals(VMPTR_MethodDesc vmMethod, CORDB_ADDRESS codeAddr, UINT32 state, OUT DacDbiArrayList<AsyncLocalData>* pAsyncLocals)
HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::EnumerateAsyncLocals(VMPTR_MethodDesc vmMethod, CORDB_ADDRESS codeAddr, UINT32 state, FP_ASYNC_LOCAL_CALLBACK fpCallback, CALLBACK_DATA pUserData)
{
DD_ENTER_MAY_THROW;

_ASSERTE(fpCallback != NULL);

HRESULT hr = S_OK;
EX_TRY
{
Expand Down Expand Up @@ -7465,13 +7467,14 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetAsyncLocals(VMPTR_MethodDesc v
}

UINT32 varCount = asyncSuspensionPoints[state].NumContinuationVars;
pAsyncLocals->Alloc(varCount);

_ASSERTE(varBeginIndex + varCount <= cAsyncVars);
for (UINT32 i = 0; i < varCount; i++)
{
(*pAsyncLocals)[i].offset = asyncVars[varBeginIndex + i].Offset;
(*pAsyncLocals)[i].ilVarNum = asyncVars[varBeginIndex + i].VarNumber;
AsyncLocalData local;
local.offset = asyncVars[varBeginIndex + i].Offset;
local.ilVarNum = asyncVars[varBeginIndex + i].VarNumber;
fpCallback(&local, pUserData);
}
}
EX_CATCH_HRESULT(hr);
Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/debug/daccess/dacdbiimpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ class DacDbiInterfaceImpl :
OUT PCODE *pDiagnosticIP,
OUT CORDB_ADDRESS *pNextContinuation,
OUT UINT32 *pState);
HRESULT STDMETHODCALLTYPE GetAsyncLocals(VMPTR_MethodDesc vmMethod, CORDB_ADDRESS codeAddr, UINT32 state, OUT DacDbiArrayList<AsyncLocalData>* pAsyncLocals);
HRESULT STDMETHODCALLTYPE EnumerateAsyncLocals(VMPTR_MethodDesc vmMethod, CORDB_ADDRESS codeAddr, UINT32 state, FP_ASYNC_LOCAL_CALLBACK fpCallback, CALLBACK_DATA pUserData);
HRESULT STDMETHODCALLTYPE GetGenericArgTokenIndex(VMPTR_MethodDesc vmMethod, OUT UINT32* pIndex);

private:
Expand Down
3 changes: 1 addition & 2 deletions src/coreclr/debug/di/rspriv.h
Original file line number Diff line number Diff line change
Expand Up @@ -11432,8 +11432,7 @@ class CordbAsyncFrame : public CordbBase, public ICorDebugILFrame, public ICorDe
CORDB_ADDRESS m_diagnosticIP;
CORDB_ADDRESS m_continuationAddress;
UINT32 m_state;
int m_nNumberOfVars;
DacDbiArrayList<AsyncLocalData> m_asyncVars;
CQuickArrayList<AsyncLocalData> m_asyncVars;

Instantiation m_genericArgs; // the generics type arguments
BOOL m_genericArgsLoaded; // whether we have loaded and cached the generics type arguments
Expand Down
23 changes: 18 additions & 5 deletions src/coreclr/debug/di/rsthread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10996,7 +10996,21 @@ HRESULT CordbAsyncFrame::Init()
// LookupOrCreateNativeCode is marked INTERNAL_SYNC_API_ENTRY and requires the StopGoLock.
m_pCode.Assign(pModule->LookupOrCreateNativeCode(m_methodDef, m_vmMethodDesc, m_pCodeStart));
m_pCode->LoadNativeInfo();
IfFailThrow(GetProcess()->GetDAC()->GetAsyncLocals(m_vmMethodDesc, m_pCodeStart, m_state, &m_asyncVars));

{
CallbackAccumulator<AsyncLocalData> acc;
IfFailThrow(GetProcess()->GetDAC()->EnumerateAsyncLocals(
m_vmMethodDesc, m_pCodeStart, m_state,
&CallbackAccumulator<AsyncLocalData>::PushCallback,
&acc));
IfFailThrow(acc.hrError);

SIZE_T cLocals = acc.items.Size();
for (SIZE_T i = 0; i < cLocals; i++)
{
m_asyncVars.Push(acc.items[i]);
}
}

// Initialize function and IL code
m_pFunction.Assign(m_pCode->GetFunction());
Expand Down Expand Up @@ -11038,7 +11052,6 @@ void CordbAsyncFrame::Neuter()
return;

m_pCode.Clear();
m_asyncVars.Dealloc();
m_pAppDomain.Clear();

m_pFunction.Clear();
Expand Down Expand Up @@ -11259,7 +11272,7 @@ HRESULT CordbAsyncFrame::GetArgument(DWORD dwIndex, ICorDebugValue ** ppValue)
CordbType * pType;
LoadGenericArgs();
m_pCode->GetArgumentType(dwIndex, &m_genericArgs, &pType);
for (unsigned int i = 0; i < m_asyncVars.Count(); i++)
for (unsigned int i = 0; i < m_asyncVars.Size(); i++)
{
if (m_asyncVars[i].ilVarNum == dwIndex)
{
Expand Down Expand Up @@ -11438,7 +11451,7 @@ HRESULT CordbAsyncFrame::GetLocalVariableEx(ILCodeKind flags, DWORD dwIndex, ICo
#endif // FEATURE_CODE_VERSIONING

IfFailThrow(pActiveCode->GetLocalVariableType(dwIndex, &m_genericArgs, &pType));
for (unsigned int i = 0 ; i < m_asyncVars.Count(); i++)
for (unsigned int i = 0 ; i < m_asyncVars.Size(); i++)
{
if (m_asyncVars[i].ilVarNum == dwIndex+argCount)
{
Expand Down Expand Up @@ -11533,7 +11546,7 @@ void CordbAsyncFrame::LoadGenericArgs()
CORDB_ADDRESS genericTypeParam = 0;
if (result == S_OK)
{
for (unsigned int i = 0 ; i < m_asyncVars.Count(); i++)
for (unsigned int i = 0 ; i < m_asyncVars.Size(); i++)
{
if (m_asyncVars[i].ilVarNum == genericArgIndex)
{
Expand Down
25 changes: 24 additions & 1 deletion src/coreclr/debug/inc/dacdbiinterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -2265,7 +2265,30 @@ IDacDbiInterface : public IUnknown
OUT CORDB_ADDRESS* pNextContinuation,
OUT UINT32* pState) = 0;

virtual HRESULT STDMETHODCALLTYPE GetAsyncLocals(VMPTR_MethodDesc vmMethod, CORDB_ADDRESS codeAddr, UINT32 state, OUT DacDbiArrayList<AsyncLocalData>* pAsyncLocals) = 0;
// Callback invoked once per async local enumerated by EnumerateAsyncLocals.
// The callback must not throw. Implementations typically push the value into an
// accumulator stashed in pUserData (see CallbackAccumulator<AsyncLocalData>).
typedef void (*FP_ASYNC_LOCAL_CALLBACK)(AsyncLocalData * pLocal, CALLBACK_DATA pUserData);

// Enumerate the async locals captured at a given async suspension point.
//
// Arguments:
// vmMethod - the async method in question
// codeAddr - native code address used to disambiguate code versions; when 0
// the active native code of vmMethod is used
// state - index of the async suspension point whose locals are requested
// fpCallback - callback invoked once per AsyncLocalData; must not be NULL and
// must not throw
// pUserData - opaque user data passed through to the callback
//
// Notes:
// Returns S_OK with no callbacks invoked when:
// - vmMethod refers to an async thunk method
// - codeAddr is non-zero but does not resolve to a valid native code version
// - state is past the number of suspension points reported for the method
// Otherwise returns S_OK after invoking fpCallback for every local captured at
// suspension point `state`.
virtual HRESULT STDMETHODCALLTYPE EnumerateAsyncLocals(VMPTR_MethodDesc vmMethod, CORDB_ADDRESS codeAddr, UINT32 state, FP_ASYNC_LOCAL_CALLBACK fpCallback, CALLBACK_DATA pUserData) = 0;

virtual HRESULT STDMETHODCALLTYPE GetGenericArgTokenIndex(
VMPTR_MethodDesc vmMethod,
Expand Down
8 changes: 5 additions & 3 deletions src/coreclr/inc/dacdbi.idl
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ struct DebuggerIPCE_ObjectData;
struct MonitorLockInfo;
struct DacGcReference;
struct DacSharedReJitInfo;
struct AsyncLocalData;

//
// Types MIDL cannot handle natively. These dummy definitions satisfy the MIDL
Expand All @@ -42,6 +41,9 @@ cpp_quote("#if 0")
// Passed by value in IsMatchingParentFrame, GetFramePointer, and the internal frame callback.
typedef struct { UINT_PTR m_sp; } FramePointer;

// AsyncLocalData - struct passed by pointer to FP_ASYNC_LOCAL_CALLBACK.
typedef struct { UINT_PTR dummy; } AsyncLocalData;

// VMPTR types - opaque pointer-sized wrappers
typedef ULONG64 VMPTR_AppDomain;
typedef ULONG64 VMPTR_OBJECTHANDLE;
Expand Down Expand Up @@ -129,7 +131,6 @@ typedef struct { void *pList; int nEntries; } DacDbiArrayList_CORDB_ADDRESS;
typedef struct { void *pList; int nEntries; } DacDbiArrayList_GUID;
typedef struct { void *pList; int nEntries; } DacDbiArrayList_COR_SEGMENT;
typedef struct { void *pList; int nEntries; } DacDbiArrayList_COR_MEMORY_RANGE;
typedef struct { void *pList; int nEntries; } DacDbiArrayList_AsyncLocalData;

cpp_quote("#endif")

Expand All @@ -142,6 +143,7 @@ typedef BOOL (*FP_INTERNAL_FRAME_ENUMERATION_CALLBACK)(FramePointer fpFrame, CAL
typedef void (*FP_HEAPSEGMENT_CALLBACK)(CORDB_ADDRESS rangeStart, CORDB_ADDRESS rangeEnd, int generation, ULONG heap, CALLBACK_DATA pUserData);
typedef void (*FP_RCW_INTERFACE_CALLBACK)(CORDB_ADDRESS itfPtr, CALLBACK_DATA pUserData);
typedef void (*FP_EXCEPTION_STACK_FRAME_CALLBACK)(VMPTR_AppDomain vmAppDomain, VMPTR_Assembly vmAssembly, CORDB_ADDRESS ip, mdMethodDef methodDef, BOOL isLastForeignExceptionFrame, CALLBACK_DATA pUserData);
typedef void (*FP_ASYNC_LOCAL_CALLBACK)(AsyncLocalData *pLocal, CALLBACK_DATA pUserData);
typedef void (*FP_FIELDDATA_CALLBACK)(struct FieldData * pFieldData, CALLBACK_DATA pUserData);


Expand Down Expand Up @@ -435,7 +437,7 @@ interface IDacDbiInterface : IUnknown
[out] PCODE * pDiagnosticIP,
[out] CORDB_ADDRESS * pNextContinuation,
[out] UINT32 * pState);
HRESULT GetAsyncLocals([in] VMPTR_MethodDesc vmMethod, [in] CORDB_ADDRESS codeAddr, [in] UINT32 state, [out] DacDbiArrayList_AsyncLocalData * pAsyncLocals);
HRESULT EnumerateAsyncLocals([in] VMPTR_MethodDesc vmMethod, [in] CORDB_ADDRESS codeAddr, [in] UINT32 state, [in] FP_ASYNC_LOCAL_CALLBACK fpCallback, [in] CALLBACK_DATA pUserData);

// Generic Arg Token
HRESULT GetGenericArgTokenIndex([in] VMPTR_MethodDesc vmMethod, [out] UINT32 * pTokenIndex);
Expand Down
8 changes: 8 additions & 0 deletions src/coreclr/vm/datadescriptor/datadescriptor.inc
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,16 @@ CDAC_TYPE_END(String)

CDAC_TYPE_BEGIN(ContinuationObject)
CDAC_TYPE_SIZE(sizeof(ContinuationObject))
CDAC_TYPE_FIELD(ContinuationObject, T_POINTER, Next, cdac_data<ContinuationObject>::Next)
CDAC_TYPE_FIELD(ContinuationObject, T_POINTER, ResumeInfo, cdac_data<ContinuationObject>::ResumeInfo)
CDAC_TYPE_FIELD(ContinuationObject, T_INT32, State, cdac_data<ContinuationObject>::State)
CDAC_TYPE_END(ContinuationObject)

CDAC_TYPE_BEGIN(AsyncResumeInfo)
CDAC_TYPE_INDETERMINATE(AsyncResumeInfo)
CDAC_TYPE_FIELD(AsyncResumeInfo, T_POINTER, DiagnosticIP, offsetof(CORINFO_AsyncResumeInfo, DiagnosticIP))
CDAC_TYPE_END(AsyncResumeInfo)

CDAC_TYPE_BEGIN(Array)
CDAC_TYPE_SIZE(sizeof(ArrayBase))
CDAC_TYPE_FIELD(Array, T_UINT32, m_NumComponents, cdac_data<ArrayBase>::m_NumComponents)
Expand Down
9 changes: 9 additions & 0 deletions src/coreclr/vm/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -2122,6 +2122,7 @@ class GenericCacheStruct
class ContinuationObject : public Object
{
friend class CoreLibBinder;
friend struct ::cdac_data<ContinuationObject>;

public:
CorInfoContinuationFlags GetFlags() const
Expand Down Expand Up @@ -2234,6 +2235,14 @@ class ContinuationObject : public Object
int32_t State;
};

template<>
struct cdac_data<ContinuationObject>
{
static constexpr size_t Next = offsetof(ContinuationObject, Next);
static constexpr size_t ResumeInfo = offsetof(ContinuationObject, ResumeInfo);
static constexpr size_t State = offsetof(ContinuationObject, State);
};

// This class corresponds to Exception on the managed side.
typedef DPTR(class ExceptionObject) PTR_ExceptionObject;
#include "pshpack4.h"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,29 @@ public readonly struct DebugVarInfo
public int StackOffset2 { get; init; }
}

/// <summary>
/// A native code location at which an async method may suspend, together with the
/// continuation-object locals captured at that point.
/// </summary>
public readonly struct AsyncSuspensionInfo
{
/// <summary>The native code offset of the suspension point.</summary>
public uint NativeOffset { get; init; }
/// <summary>The continuation-object locals live at this suspension point.</summary>
public IReadOnlyList<AsyncLocalInfo> Locals { get; init; }
}
Comment thread
rcj1 marked this conversation as resolved.

/// <summary>
/// A single local captured into the continuation object at a suspension point.
/// </summary>
public readonly struct AsyncLocalInfo
{
/// <summary>Offset of the local within the continuation object's data area.</summary>
public uint Offset { get; init; }
/// <summary>IL var number of the local (or a synthetic marker such as MAX_ILNUM-relative values).</summary>
public uint ILVarNumber { get; init; }
}

public interface IDebugInfo : IContract
{
static string IContract.Name { get; } = nameof(DebugInfo);
Expand All @@ -91,6 +114,13 @@ public interface IDebugInfo : IContract
/// Each entry describes where a variable is stored at a particular native offset range.
/// </summary>
IEnumerable<DebugVarInfo> GetMethodVarInfo(TargetCodePointer pCode, out uint codeOffset) => throw new NotImplementedException();
/// <summary>
/// Given a code pointer, return the async-suspension points for the method together with the
/// continuation-object locals captured at each suspension point. Returns an empty list when
/// the method has no async debug info.
/// </summary>
IReadOnlyList<AsyncSuspensionInfo> GetAsyncSuspensionPoints(TargetCodePointer pCode) =>
Array.Empty<AsyncSuspensionInfo>();
}

public readonly struct DebugInfo : IDebugInfo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public readonly record struct DelegateInfo(
TargetCodePointer TargetMethodPtr,
DelegateType DelegateType);

public readonly record struct ContinuationInfo(
TargetPointer Next,
TargetPointer DiagnosticIP,
uint State);

public interface IObject : IContract
{
static string IContract.Name { get; } = nameof(Object);
Expand All @@ -28,6 +33,7 @@ public interface IObject : IContract
// Returns the SyncBlock address for the object, or TargetPointer.Null if no sync block is associated with it.
TargetPointer GetSyncBlockAddress(TargetPointer address) => throw new NotImplementedException();
DelegateInfo GetDelegateInfo(TargetPointer address) => throw new NotImplementedException();
ContinuationInfo GetContinuationInfo(TargetPointer address) => throw new NotImplementedException();
}

public readonly struct Object : IObject
Expand Down
Loading
Loading