Skip to content
Closed
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
18 changes: 3 additions & 15 deletions docs/design/datacontracts/Object.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ Data descriptors used:
| `Object` | `m_pMethTab` | Method table for the object |
| `String` | `m_FirstChar` | First character of the string - `m_StringLength` can be used to read the full string (encoded in UTF-16) |
| `String` | `m_StringLength` | Length of the string in characters (encoded in UTF-16) |
| `SyncBlock` | `InteropInfo` | Optional `InteropSyncBlockInfo` for the sync block |
| `SyncTableEntry` | `SyncBlock` | `SyncBlock` corresponding to the entry |

Global variables used:
| Global Name | Type | Purpose |
Expand All @@ -39,13 +37,13 @@ Global variables used:
| `ObjectHeaderSize` | uint32 | Size of the object header (sync block and alignment) |
| `ObjectToMethodTableUnmask` | uint8 | Bits to clear for converting to a method table address |
| `StringMethodTable` | TargetPointer | The method table for System.String |
| `SyncTableEntries` | TargetPointer | The `SyncTableEntry` list |
| `SyncBlockValueToObjectOffset` | uint16 | Offset from the sync block value (in the object header) to the object itself |

Contracts used:
| Contract Name |
| --- |
| `RuntimeTypeSystem` |
| `SyncBlock` |

``` csharp
TargetPointer GetMethodTableAddress(TargetPointer address)
Expand Down Expand Up @@ -111,18 +109,8 @@ bool GetBuiltInComData(TargetPointer address, out TargetPointer rcw, out TargetP

// Get the offset into the sync table entries
uint index = syncBlockValue & SyncBlockValue.SyncBlockIndexMask;
ulong offsetInSyncTableEntries = index * /* SyncTableEntry size */;

TargetPointer syncBlock = target.ReadPointer(_syncTableEntries + offsetInSyncTableEntries + /* SyncTableEntry::SyncBlock offset */);
if (syncBlock == TargetPointer.Null)
return false;

TargetPointer interopInfo = target.ReadPointer(syncBlock + /* SyncTableEntry::InteropInfo offset */);
if (interopInfo == TargetPointer.Null)
return false;

rcw = target.ReadPointer(interopInfo + /* InteropSyncBlockInfo::RCW offset */);
ccw = target.ReadPointer(interopInfo + /* InteropSyncBlockInfo::CCW offset */);
return rcw != TargetPointer.Null && ccw != TargetPointer.Null;
Contracts.ISyncBlock sync = target.Contracts.SyncBlock;
return sync.TryGetBuiltInComData(index, out rcw, out ccw, out TargetPointer _);
}
```
165 changes: 165 additions & 0 deletions docs/design/datacontracts/SyncBlock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Contract Object

This contract is for getting information about SyncBlocks.

## APIs of contract

```csharp
public readonly struct SyncBlockData
Copy link
Copy Markdown
Member

@AaronRobinsonMSFT AaronRobinsonMSFT Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should hold off on this for a bit. @jkoritzinsky is reworking the monitors and how they exist on the sync block. See #118371.

{
public bool IsFree { get; init; }
public TargetPointer SyncBlock { get; init; }
public TargetPointer Object { get; init; }
public uint RecursionLevel { get; init; }
public uint HoldingThreadId { get; init; }
public uint MonitorHeldState { get; init; }
}
```

``` csharp
// Get the number of syncblocks that have ever been used in the target runtime.
uint GetSyncBlockCount();

// Get information about a syncblock at a given index.
SyncBlockData GetSyncBlockData(uint index);

// Get the number of threads waiting on the syncblock at a given index (up to the maximumIteration).
uint GetAdditionalThreadCount(uint index, uint maximumIterations = 1000);

// Get built-in COM data for the syncblock if available. Returns false if the syncblock at the given index does not have COM data.
bool TryGetBuiltInComData(uint index, out TargetPointer rcw, out TargetPointer ccw, out TargetPointer cf);
```

## Version 1

Data descriptors used:
| Data Descriptor Name | Field | Meaning |
| --- | --- | --- |
| `InteropSyncBlockInfo` | `ClassFactory` | Pointer to the ClassFactory for the object (if it exists) |
| `InteropSyncBlockInfo` | `RCW` | Pointer to the RCW for the object (if it exists) |
| `InteropSyncBlockInfo` | `CCW` | Pointer to the CCW for the object (if it exists) |
| `SyncBlock` | `Monitor` | `AwareLock` storing the `SyncBlock`s locking state |
| `SyncBlock` | `InteropInfo` | Pointer to an optional `InteropSyncBlockInfo` for the sync block |
| `SyncBlock` | `Link` | `SLink` to list of threads waiting on the `SyncBlock` |
| `SyncTableEntry` | `SyncBlock` | `SyncBlock` corresponding to the entry |
| `SyncTableEntry` | `Object` | `Object` corresponding to the entry |
| `SyncBlockCache` | `FreeSyncTableIndex` | Pointer to the first syncblock index which has never been used in the target runtime |
| `AwareLock` | `RecursionLevel` | RecursionLevel of the `AwareLock` |
| `AwareLock` | `HoldingThreadId` | ThreadId currently holding the `AwareLock` |
| `AwareLock` | `LockState` | `uint` flag mask containing lock state information |

Global variables used:
| Global Name | Type | Purpose |
| --- | --- | --- |
| `SyncTableEntries` | TargetPointer | The `SyncTableEntry` list |
| `SyncBlockCache` | TargetPointer | The global `SyncBlockCache` |

Constants used:
| Name | Type | Purpose | Value |
| --- | --- | --- | --- |
| `WAITER_COUNT_SHIFT` | uint | AwareLock LockState shift to get waiter count | `0x6` |
| `IS_LOCKED_MASK` | uint | AwareLock LockState mask to determine if locked | `0x1` |


Contracts used:
| Contract Name |
| --- |
| _(none)_ |

``` csharp
uint GetSyncBlockCount()
{
TargetPointer syncBlockCacheAddr = target.ReadPointer(target.ReadGlobalPointer("SyncBlockCache"));
uint freeSyncTableIndex = target.Read<uint>(syncBlockCacheAddr + /* SyncBlockCache::FreeSyncTableIndex offset */);
// freeSyncTableIndex points to the first unused SyncBlock
return freeSyncTableIndex - 1;
}

SyncBlockData GetSyncBlockData(uint index)
{
TargetPointer syncTableEntry = target.ReadPointer(GetSyncTableEntryAddress(index));

TargetPointer pObject = target.ReadPointer(syncTableEntry + /* SyncTableEntry::Object offset */);
TargetPointer pSyncBlock = target.ReadPointer(syncTableEntry + /* SyncTableEntry::SyncBlock offset */);

// SyncBlock is free if lowest bit of the Object field is set.
if ((pObject & 0x1) == 0x1)
return new SyncBlockData { IsFree = true };

if (pSyncBlock != TargetPointer.Null)
{
TargetPointer pMonitor = target.ReadPointer(pSyncBlock + /* SyncBlock::Monitor offset */);
uint lockState = target.Read<uint>(pMonitor + /* AwareLock::LockState offset */)
bool locked = (lockState & IS_LOCKED_MASK) == IS_LOCKED_MASK;
uint waiterCount = lockState >> WAITER_COUNT_SHIFT;

// monitorHeldState lsb is 1 if locked, 0 if unlocked
// the higher bits contain the waiter count shifted up
uint monitorHeldState = (locked ? 0x1 : 0x0) | (waiterCount << 1);
return new SyncBlockData
{
IsFree = false,
Object = pObject,
SyncBlock = pSyncBlock,
RecursionLevel = target.Read<uint>(pMonitor + /* AwareLock::RecursionLevel offset */),
HoldingThreadId = target.Read<uint>(pMonitor + /* AwareLock::HoldingThreadId offset */),
MonitorHeldState = monitorHeldState,
};
}

return new SyncBlockData
{
IsFree = false,
Object = pObject,
SyncBlock = pSyncBlock,
RecursionLevel = 0,
HoldingThreadId = 0,
MonitorHeldState = 0
};
}

uint GetAdditionalThreadCount(uint index, uint maximumIterations)
{
TargetPointer syncTableEntry = target.ReadPointer(GetSyncTableEntryAddress(index));
TargetPointer syncBlock = target.ReadPointer(syncTableEntry + /* SyncTableEntry::SyncBlock offset */);
if (syncBlock == TargetPointer.Null)
return 0;

uint additionalThreadCount = 0;
TargetPointer pLink = target.ReadPointer(syncBlock + /* SyncBlock::Link offset */);
while (pLink != TargetPointer.Null && additionalThreadCount < maximumIterations)
{
additionalThreadCount += 1;
pLink = target.ReadPointer(pLink);
}

return additionalThreadCount;
}

bool GetBuiltInComData(uint index, out TargetPointer rcw, out TargetPointer ccw, out TargetPointer cf);
{
TargetPointer syncTableEntry = target.ReadPointer(GetSyncTableEntryAddress(index));
TargetPointer syncBlock = target.ReadPointer(syncTableEntry + /* SyncTableEntry::SyncBlock offset */);
if (syncBlock == TargetPointer.Null)
return false;

TargetPointer interopInfo = target.ReadPointer(syncBlock + /* SyncBlock::InteropInfo offset */);
if (interopInfo == TargetPointer.Null)
return false;

rcw = target.ReadPointer(interopInfo + /* InteropSyncBlockInfo::RCW offset */);
ccw = target.ReadPointer(interopInfo + /* InteropSyncBlockInfo::CCW offset */);
cf = target.ReadPointer(interopInfo + /* InteropSyncBlockInfo::ClassFactory offset */);
return rcw != TargetPointer.Null || ccw != TargetPointer.Null || cf != TargetPointer.Null;
}
```

Helpers:
```csharp
private TargetPointer GetSyncTableEntryAddress(uint index)
{
TargetPointer syncTableEntries = target.ReadPointer(target.ReadGlobalPointer("SyncTableEntries"));
uint syncTableEntrySize = target.GetTypeInfo(DataType.SyncTableEntry).Size;
return syncTableEntries + (syncTableEntrySize * index)
}
```
3 changes: 2 additions & 1 deletion src/coreclr/vm/datadescriptor/contracts.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@
"RuntimeTypeSystem": 1,
"StackWalk": 1,
"StressLog": 2,
"SyncBlock": 1,
"Thread": 1
}
}
19 changes: 19 additions & 0 deletions src/coreclr/vm/datadescriptor/datadescriptor.inc
Original file line number Diff line number Diff line change
Expand Up @@ -134,21 +134,39 @@ CDAC_TYPE_END(Array)
CDAC_TYPE_BEGIN(InteropSyncBlockInfo)
CDAC_TYPE_INDETERMINATE(InteropSyncBlockInfo)
#ifdef FEATURE_COMINTEROP
#ifdef FEATURE_COMINTEROP_UNMANAGED_ACTIVATION
CDAC_TYPE_FIELD(InteropSyncBlockInfo, /*pointer*/, ClassFactory, cdac_data<InteropSyncBlockInfo>::ClassFactory)
#endif // FEATURE_COMINTEROP_UNMANAGED_ACTIVATION
CDAC_TYPE_FIELD(InteropSyncBlockInfo, /*pointer*/, CCW, cdac_data<InteropSyncBlockInfo>::CCW)
CDAC_TYPE_FIELD(InteropSyncBlockInfo, /*pointer*/, RCW, cdac_data<InteropSyncBlockInfo>::RCW)
#endif // FEATURE_COMINTEROP
CDAC_TYPE_END(InteropSyncBlockInfo)

CDAC_TYPE_BEGIN(SyncBlock)
CDAC_TYPE_INDETERMINATE(SyncBlock)
CDAC_TYPE_FIELD(SyncBlock, /*AwareLock*/, Monitor, cdac_data<SyncBlock>::Monitor)
CDAC_TYPE_FIELD(SyncBlock, /*pointer*/, InteropInfo, cdac_data<SyncBlock>::InteropInfo)
CDAC_TYPE_FIELD(SyncBlock, /*pointer*/, Link, cdac_data<SyncBlock>::Link)
CDAC_TYPE_END(SyncBlock)

CDAC_TYPE_BEGIN(SyncTableEntry)
CDAC_TYPE_SIZE(sizeof(SyncTableEntry))
CDAC_TYPE_FIELD(SyncTableEntry, /*pointer*/, SyncBlock, offsetof(SyncTableEntry, m_SyncBlock))
CDAC_TYPE_FIELD(SyncTableEntry, /*pointer*/, Object, offsetof(SyncTableEntry, m_Object))
CDAC_TYPE_END(SyncTableEntry)

CDAC_TYPE_BEGIN(SyncBlockCache)
CDAC_TYPE_INDETERMINATE(SyncBlockCache)
CDAC_TYPE_FIELD(SyncBlockCache, /*uint32*/, FreeSyncTableIndex, cdac_data<SyncBlockCache>::FreeSyncTableIndex)
CDAC_TYPE_END(SyncBlockCache)

CDAC_TYPE_BEGIN(AwareLock)
CDAC_TYPE_INDETERMINATE(AwareLock)
CDAC_TYPE_FIELD(AwareLock, /*uint32*/, LockState, cdac_data<AwareLock>::LockState)
CDAC_TYPE_FIELD(AwareLock, /*uint32*/, RecursionLevel, cdac_data<AwareLock>::RecursionLevel)
CDAC_TYPE_FIELD(AwareLock, /*uint32*/, HoldingThreadId, cdac_data<AwareLock>::HoldingThreadId)
CDAC_TYPE_END(AwareLock)

// Loader

CDAC_TYPE_BEGIN(Module)
Expand Down Expand Up @@ -981,6 +999,7 @@ CDAC_GLOBAL_POINTER(ObjectMethodTable, &::g_pObjectClass)
CDAC_GLOBAL_POINTER(ObjectArrayMethodTable, &::g_pPredefinedArrayTypes[ELEMENT_TYPE_OBJECT])
CDAC_GLOBAL_POINTER(StringMethodTable, &::g_pStringClass)
CDAC_GLOBAL_POINTER(SyncTableEntries, &::g_pSyncTable)
CDAC_GLOBAL_POINTER(SyncBlockCache, &SyncBlockCache::s_pSyncBlockCache)
CDAC_GLOBAL_POINTER(MiniMetaDataBuffAddress, &::g_MiniMetaDataBuffAddress)
CDAC_GLOBAL_POINTER(MiniMetaDataBuffMaxSize, &::g_MiniMetaDataBuffMaxSize)
CDAC_GLOBAL_POINTER(DacNotificationFlags, &::g_dacNotificationFlags)
Expand Down
24 changes: 24 additions & 0 deletions src/coreclr/vm/syncblk.h
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,16 @@ class AwareLock
LIMITED_METHOD_CONTRACT;
return (int)offsetof(AwareLock, m_HoldingOSThreadId);
}

friend struct ::cdac_data<AwareLock>;
};

template<>
struct cdac_data<AwareLock>
{
static constexpr size_t LockState = offsetof(AwareLock, m_lockState);
static constexpr size_t RecursionLevel = offsetof(AwareLock, m_Recursion);
static constexpr size_t HoldingThreadId = offsetof(AwareLock, m_HoldingThreadId);
};

class UMEntryThunkData;
Expand Down Expand Up @@ -830,6 +840,9 @@ template<>
struct cdac_data<InteropSyncBlockInfo>
{
#ifdef FEATURE_COMINTEROP
#ifdef FEATURE_COMINTEROP_UNMANAGED_ACTIVATION
static constexpr size_t ClassFactory = offsetof(InteropSyncBlockInfo, m_pCCF);
#endif // FEATURE_COMINTEROP_UNMANAGED_ACTIVATION
static constexpr size_t CCW = offsetof(InteropSyncBlockInfo, m_pCCW);
static constexpr size_t RCW = offsetof(InteropSyncBlockInfo, m_pRCW);
#endif // FEATURE_COMINTEROP
Expand Down Expand Up @@ -1132,7 +1145,10 @@ class SyncBlock
template<>
struct cdac_data<SyncBlock>
{
static constexpr size_t Monitor = offsetof(SyncBlock, m_Monitor);
static constexpr size_t InteropInfo = offsetof(SyncBlock, m_pInteropInfo);
static constexpr size_t Link = offsetof(SyncBlock, m_Link);

};

class SyncTableEntry
Expand Down Expand Up @@ -1291,6 +1307,14 @@ class SyncBlockCache
#ifdef VERIFY_HEAP
void VerifySyncTableEntry();
#endif

friend struct ::cdac_data<SyncBlockCache>;
};

template<>
struct cdac_data<SyncBlockCache>
{
static constexpr size_t FreeSyncTableIndex = offsetof(SyncBlockCache, m_FreeSyncTableIndex);
};

// See code:#SyncBlockOverView for more
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,8 @@ public abstract class ContractRegistry
/// Gets an instance of the GC contract for the target.
/// </summary>
public abstract IGC GC { get; }
/// <summary>
/// Gets an instance of the SyncBlock contract for the target.
/// </summary>
public abstract ISyncBlock SyncBlock { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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.Diagnostics.CodeAnalysis;

namespace Microsoft.Diagnostics.DataContractReader.Contracts;

public readonly struct SyncBlockData
{
public bool IsFree { get; init; }
public TargetPointer SyncBlock { get; init; }
public TargetPointer Object { get; init; }
public uint RecursionLevel { get; init; }
public uint HoldingThreadId { get; init; }
public uint MonitorHeldState { get; init; }
}

public interface ISyncBlock : IContract
{
static string IContract.Name { get; } = nameof(SyncBlock);

uint GetSyncBlockCount() => throw new NotImplementedException();
SyncBlockData GetSyncBlockData(uint index) => throw new NotImplementedException();
uint GetAdditionalThreadCount(uint index, uint maximumIterations = 1000) => throw new NotImplementedException();
bool TryGetBuiltInComData(uint index, out TargetPointer rcw, out TargetPointer ccw, out TargetPointer cf) => throw new NotImplementedException();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should reconsider this. This approach is coupling COM and the sync block, which is an implementation detail I would avoid. Can we instead place this on the object and abstract away the sync block?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed with Jeremy, we will plan to make a new BuiltInCOM (naming TBD) contract. This would handle reading the COM data off an InteropSyncBlock with the SyncBlock/Object contracts as dependencies.

To get the CCW/RCW of an Object, the BuiltInCOM would expose something like as follows:

    public bool GetObjectRCW(TargetPointer objectAddress)
    {
        IObject object = _target.Contracts.Object;
        ISyncBlock sync = _target.Contracts.SyncBlock;

        uint syncBlockIndex = object.GetSyncBlockIndex(objectAddress);
        TargetPointer interopSyncBlockAddress = sync.GetInteropData(syncBlockIndex);

        Data.InteropSyncBlockInfo interopInfo = _target.ProcessedData.GetOrAdd<Data.InteropSyncBlock>(interopSyncBlockAddress);
        return interopInfo.rcw;
    }

}

public readonly struct SyncBlock : ISyncBlock
{
// Everything throws NotImplementedException
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ public enum DataType
Array,
SyncBlock,
SyncTableEntry,
SyncBlockCache,
AwareLock,
InteropSyncBlockInfo,
InstantiatedMethodDesc,
DynamicMethodDesc,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public static class Globals
public const string SyncBlockValueToObjectOffset = nameof(SyncBlockValueToObjectOffset);

public const string SyncTableEntries = nameof(SyncTableEntries);
public const string SyncBlockCache = nameof(SyncBlockCache);

public const string ArrayBoundsZero = nameof(ArrayBoundsZero);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,9 @@ IObject IContractFactory<IObject>.CreateContract(Target target, int version)
byte objectToMethodTableUnmask = target.ReadGlobal<byte>(Constants.Globals.ObjectToMethodTableUnmask);
TargetPointer stringMethodTable = target.ReadPointer(
target.ReadGlobalPointer(Constants.Globals.StringMethodTable));
TargetPointer syncTableEntries = target.ReadPointer(
target.ReadGlobalPointer(Constants.Globals.SyncTableEntries));
return version switch
{
1 => new Object_1(target, methodTableOffset, objectToMethodTableUnmask, stringMethodTable, syncTableEntries),
1 => new Object_1(target, methodTableOffset, objectToMethodTableUnmask, stringMethodTable),
_ => default(Object),
};
}
Expand Down
Loading
Loading