diff --git a/docs/design/datacontracts/GCInfo.md b/docs/design/datacontracts/GCInfo.md index c878957aea5001..b97f8dbbd3a525 100644 --- a/docs/design/datacontracts/GCInfo.md +++ b/docs/design/datacontracts/GCInfo.md @@ -19,6 +19,9 @@ IGCInfoHandle DecodeInterpreterGCInfo(TargetPointer gcInfoAddress, uint gcVersio // Fetches length of code as reported in GCInfo uint GetCodeLength(IGCInfoHandle handle); + +// Returns the list of interruptible code offset ranges from the GCInfo +IReadOnlyList GetInterruptibleRanges(IGCInfoHandle handle); ``` ## Version 1 diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index 83182979b22088..975537166a3395 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -185,6 +185,14 @@ partial interface IRuntimeTypeSystem : IContract // Return true if a MethodDesc represents an IL stub with a special MethodDesc context arg public virtual bool HasMDContextArg(MethodDescHandle); + // Return true if the method requires a hidden instantiation argument (generic context parameter). + // Corresponds to native MethodDesc::RequiresInstArg(). + public virtual bool RequiresInstArg(MethodDescHandle methodDesc); + + // Return true if the method uses the async calling convention. + // Corresponds to native MethodDesc::IsAsyncMethod(). + public virtual bool IsAsyncMethod(MethodDescHandle methodDesc); + // Return true if a MethodDesc is in a collectible module public virtual bool IsCollectibleMethod(MethodDescHandle methodDesc); diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index 9d254591ddb0ac..6a5fa627ab06ab 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -60,9 +60,21 @@ This contract depends on the following descriptors: | `StubDispatchFrame` | `MethodDescPtr` | Pointer to Frame's method desc | | `StubDispatchFrame` | `RepresentativeMTPtr` | Pointer to Frame's method table pointer | | `StubDispatchFrame` | `RepresentativeSlot` | Frame's method table slot | +| `StubDispatchFrame` | `GCRefMap` | Cached pointer to GC reference map blob for caller stack promotion | +| `StubDispatchFrame` | `ZapModule` | Module pointer for lazy GCRefMap resolution via import sections | +| `StubDispatchFrame` | `Indirection` | Import slot pointer for lazy GCRefMap resolution | +| `ExternalMethodFrame` | `GCRefMap` | Cached pointer to GC reference map blob for caller stack promotion | +| `ExternalMethodFrame` | `Indirection` | Import slot pointer for lazy GCRefMap resolution | +| `ExternalMethodFrame` | `ZapModule` | Module pointer for lazy GCRefMap resolution via import sections | +| `DynamicHelperFrame` | `DynamicHelperFrameFlags` | Flags indicating which argument registers contain GC references | | `TransitionBlock` | `ReturnAddress` | Return address associated with the TransitionBlock | | `TransitionBlock` | `CalleeSavedRegisters` | Platform specific CalleeSavedRegisters struct associated with the TransitionBlock | | `TransitionBlock` (arm) | `ArgumentRegisters` | ARM specific `ArgumentRegisters` struct | +| `TransitionBlock` | `OffsetOfArgs` | Byte offset of stack arguments (first arg after registers) = `sizeof(TransitionBlock)` | +| `TransitionBlock` | `ArgumentRegistersOffset` | Byte offset of the ArgumentRegisters within the TransitionBlock | +| `TransitionBlock` | `FirstGCRefMapSlot` | Byte offset where GCRefMap slot enumeration begins. ARM64: RetBuffArgReg offset; others: ArgumentRegisters offset | +| `ReadyToRunInfo` | `ImportSections` | Pointer to array of `READYTORUN_IMPORT_SECTION` structs for GCRefMap resolution | +| `ReadyToRunInfo` | `NumImportSections` | Count of import sections in the array | | `FuncEvalFrame` | `DebuggerEvalPtr` | Pointer to the Frame's DebuggerEval object | | `DebuggerEval` | `TargetContext` | Context saved inside DebuggerEval | | `DebuggerEval` | `EvalDuringException` | Flag used in processing FuncEvalFrame | @@ -85,6 +97,8 @@ This contract depends on the following descriptors: | `ExceptionInfo` | `CallerOfActualHandlerFrame` | Stack frame of the caller of the catch handler | | `ExceptionInfo` | `PreviousNestedInfo` | Pointer to previous nested ExInfo | | `ExceptionInfo` | `PassNumber` | Exception handling pass (1 or 2) | +| `ExceptionInfo` | `ClauseForCatchHandlerStartPC` | Start PC offset of the catch handler clause, used for interruptible offset override | +| `ExceptionInfo` | `ClauseForCatchHandlerEndPC` | End PC offset of the catch handler clause, used for interruptible offset override | Global variables used: | Global Name | Type | Purpose | diff --git a/eng/Subsets.props b/eng/Subsets.props index ba8bc03906a1cb..bdb73e68c005cf 100644 --- a/eng/Subsets.props +++ b/eng/Subsets.props @@ -255,6 +255,7 @@ + @@ -534,6 +535,10 @@ + + + + diff --git a/eng/pipelines/cdac/prepare-cdac-stress-helix-steps.yml b/eng/pipelines/cdac/prepare-cdac-stress-helix-steps.yml new file mode 100644 index 00000000000000..436153bc7cc8ef --- /dev/null +++ b/eng/pipelines/cdac/prepare-cdac-stress-helix-steps.yml @@ -0,0 +1,45 @@ +# prepare-cdac-stress-helix-steps.yml - Steps for preparing cDAC stress test Helix payloads. +# +# Used by CdacDumpTests stage in runtime-diagnostics.yml. +# Handles: building stress test debuggees, preparing Helix payload, finding testhost. + +steps: +- script: $(Build.SourcesDirectory)$(dir).dotnet$(dir)dotnet$(exeExt) msbuild + $(Build.SourcesDirectory)/src/native/managed/cdac/tests/StressTests/Microsoft.Diagnostics.DataContractReader.StressTests.csproj + /t:BuildDebuggeesOnly + /p:Configuration=$(_BuildConfig) + /p:TargetArchitecture=$(archType) + -bl:$(Build.SourcesDirectory)/artifacts/log/BuildStressDebuggees.binlog + displayName: 'Build Stress Debuggees' + +- script: $(Build.SourcesDirectory)$(dir).dotnet$(dir)dotnet$(exeExt) build + $(Build.SourcesDirectory)/src/native/managed/cdac/tests/StressTests/Microsoft.Diagnostics.DataContractReader.StressTests.csproj + /p:PrepareHelixPayload=true + /p:Configuration=$(_BuildConfig) + /p:HelixPayloadDir=$(Build.SourcesDirectory)/artifacts/helixPayload/cdac-stress + -bl:$(Build.SourcesDirectory)/artifacts/log/StressTestPayload.binlog + displayName: 'Prepare Stress Test Helix Payload' + +- pwsh: | + $testhostDir = Get-ChildItem -Directory -Path "$(Build.SourcesDirectory)/artifacts/bin/testhost/net*-$(osGroup)-*-$(archType)" | Select-Object -First 1 -ExpandProperty FullName + if (-not $testhostDir) { + Write-Error "No testhost directory found" + exit 1 + } + Write-Host "TestHost root: $testhostDir" + Write-Host "##vso[task.setvariable variable=StressTestHostRootDir]$testhostDir" + + $queue = switch ("$(osGroup)_$(archType)") { + "windows_x64" { "$(helix_windows_x64)" } + "windows_x86" { "$(helix_windows_x64)" } + "windows_arm64" { "$(helix_windows_arm64)" } + "linux_x64" { "$(helix_linux_x64_oldest)" } + "linux_arm64" { "$(helix_linux_arm64_oldest)" } + "linux_arm" { "$(helix_linux_arm32_oldest)" } + "osx_x64" { "$(helix_macos_x64)" } + "osx_arm64" { "$(helix_macos_arm64)" } + default { Write-Error "Unsupported platform: $(osGroup)_$(archType)"; exit 1 } + } + Write-Host "Helix queue: $queue" + Write-Host "##vso[task.setvariable variable=CdacStressHelixQueue]$queue" + displayName: 'Find Stress TestHost and Helix Queue' diff --git a/eng/pipelines/runtime-diagnostics.yml b/eng/pipelines/runtime-diagnostics.yml index f6a3f9215fdd0b..01bb6723bebab8 100644 --- a/eng/pipelines/runtime-diagnostics.yml +++ b/eng/pipelines/runtime-diagnostics.yml @@ -267,7 +267,7 @@ extends: shouldContinueOnError: true jobParameters: nameSuffix: CdacDumpTest - buildArgs: -s clr+libs+tools.cdac+tools.cdacdumptests -c $(_BuildConfig) -rc checked -lc $(_BuildConfig) /p:SkipDumpVersions=net10.0 + buildArgs: -s clr+libs+tools.cdac+tools.cdacdumptests+tools.cdacstresstests -c $(_BuildConfig) -rc checked -lc $(_BuildConfig) /p:SkipDumpVersions=net10.0 timeoutInMinutes: 180 postBuildSteps: - template: /eng/pipelines/cdac/prepare-cdac-helix-steps.yml @@ -286,6 +286,16 @@ extends: displayName: 'Publish Dump Artifacts' condition: and(always(), ne(variables['Agent.JobStatus'], 'Succeeded')) continueOnError: true + # cDAC Stress Tests — run GC stress verification on the same Checked build + - template: /eng/pipelines/cdac/prepare-cdac-stress-helix-steps.yml + - template: /eng/pipelines/common/templates/runtimes/send-to-helix-inner-step.yml + parameters: + displayName: 'Send cDAC Stress Tests to Helix' + sendParams: $(Build.SourcesDirectory)/src/native/managed/cdac/tests/StressTests/cdac-stress-helix.proj /t:Test /p:TargetOS=$(osGroup) /p:TargetArchitecture=$(archType) /p:HelixTargetQueues="$(CdacStressHelixQueue)" /p:TestHostPayload=$(StressTestHostRootDir) /p:StressTestsPayload=$(Build.SourcesDirectory)/artifacts/helixPayload/cdac-stress /bl:$(Build.SourcesDirectory)/artifacts/log/SendStressToHelix.binlog + environment: + _Creator: dotnet-bot + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + NUGET_PACKAGES: $(Build.SourcesDirectory)$(dir).packages - pwsh: | if ("$(Agent.JobStatus)" -ne "Succeeded") { Write-Error "One or more cDAC dump test failures were detected. Failing the job." diff --git a/src/coreclr/vm/cdacstress.cpp b/src/coreclr/vm/cdacstress.cpp index fbf019d01c7d58..eb48dacce3e8fd 100644 --- a/src/coreclr/vm/cdacstress.cpp +++ b/src/coreclr/vm/cdacstress.cpp @@ -72,7 +72,7 @@ static CrstStatic s_cdacLock; // Serializes cDAC access from concurr // Unique-stack filtering: hash set of previously seen stack traces. // Protected by s_cdacLock (already held during VerifyAtStressPoint). - +static const int UNIQUE_STACK_DEPTH = 8; // Number of return addresses to hash static SHash>>* s_seenStacks = nullptr; // Thread-local reentrancy guard — prevents infinite recursion when @@ -146,12 +146,11 @@ static int ReadThreadContextCallback(uint32_t threadId, uint32_t contextFlags, u // Minimal ICLRDataTarget implementation for loading the legacy DAC in-process. // Routes ReadVirtual/GetThreadContext to the same callbacks as the cDAC. //----------------------------------------------------------------------------- -class InProcessDataTarget : public ICLRDataTarget, public ICLRRuntimeLocator +class InProcessDataTarget : public ICLRDataTarget { volatile LONG m_refCount; public: InProcessDataTarget() : m_refCount(1) {} - virtual ~InProcessDataTarget() = default; HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppObj) override { @@ -161,12 +160,6 @@ class InProcessDataTarget : public ICLRDataTarget, public ICLRRuntimeLocator AddRef(); return S_OK; } - if (riid == __uuidof(ICLRRuntimeLocator)) - { - *ppObj = static_cast(this); - AddRef(); - return S_OK; - } *ppObj = nullptr; return E_NOINTERFACE; } @@ -178,14 +171,6 @@ class InProcessDataTarget : public ICLRDataTarget, public ICLRRuntimeLocator return c; } - // ICLRRuntimeLocator — provides the CLR base address directly so the DAC - // does not fall back to GetImageBase (which needs GetModuleHandleW, unavailable on Linux). - HRESULT STDMETHODCALLTYPE GetRuntimeBase(CLRDATA_ADDRESS* baseAddress) override - { - *baseAddress = (CLRDATA_ADDRESS)GetCurrentModuleBase(); - return S_OK; - } - HRESULT STDMETHODCALLTYPE GetMachineType(ULONG32* machineType) override { #ifdef TARGET_AMD64 @@ -208,8 +193,10 @@ class InProcessDataTarget : public ICLRDataTarget, public ICLRRuntimeLocator HRESULT STDMETHODCALLTYPE GetImageBase(LPCWSTR imagePath, CLRDATA_ADDRESS* baseAddress) override { - // Not needed — the DAC uses ICLRRuntimeLocator::GetRuntimeBase() instead. - return E_NOTIMPL; + HMODULE hMod = ::GetModuleHandleW(imagePath); + if (hMod == NULL) return E_FAIL; + *baseAddress = (CLRDATA_ADDRESS)hMod; + return S_OK; } HRESULT STDMETHODCALLTYPE ReadVirtual(CLRDATA_ADDRESS address, BYTE* buffer, ULONG32 bytesRequested, ULONG32* bytesRead) override @@ -282,8 +269,8 @@ bool CdacStress::Initialize() } else { - // Legacy: GCSTRESS_CDAC maps to allocation-point + reference verification - s_cdacStressLevel = CDACSTRESS_ALLOC | CDACSTRESS_REFS; + // Legacy: GCSTRESS_CDAC maps to allocation-point verification + s_cdacStressLevel = CDACSTRESS_ALLOC; } // Load mscordaccore_universal from next to coreclr @@ -449,15 +436,27 @@ bool CdacStress::Initialize() pDacUnk->QueryInterface(__uuidof(IXCLRDataProcess), (void**)&s_dacProcess); pDacUnk->Release(); } + else if (s_logFile != nullptr) + { + fprintf(s_logFile, "DAC: CLRDataCreateInstance failed hr=0x%08x pDacUnk=%p\n", hr, pDacUnk); + } } } if (s_dacSosDac == nullptr) { + if (s_logFile != nullptr) + fprintf(s_logFile, "DAC: Loaded mscordaccore.dll but QI for ISOSDacInterface failed\n"); LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Legacy DAC loaded but QI for ISOSDacInterface failed\n")); } + else if (s_logFile != nullptr) + { + fprintf(s_logFile, "DAC: Legacy DAC loaded successfully (s_dacSosDac=%p)\n", s_dacSosDac); + } } else { + if (s_logFile != nullptr) + fprintf(s_logFile, "DAC: mscordaccore.dll not found at path\n"); LOG((LF_GCROOTS, LL_INFO10, "CDAC GC Stress: Legacy DAC not found (three-way comparison disabled)\n")); } } @@ -535,11 +534,53 @@ void CdacStress::Shutdown() LOG((LF_GCROOTS, LL_INFO10, "CDAC GC Stress: Shutdown complete\n")); } +//----------------------------------------------------------------------------- +// Resolve a managed IP to a method name using ISOSDacInterface. +// Falls back to "" or "" if resolution fails. +// Uses the cDAC's ISOSDacInterface by default. +//----------------------------------------------------------------------------- + +static void ResolveMethodName(CLRDATA_ADDRESS source, int sourceType, char* buf, int bufLen) +{ + if (bufLen <= 0) + return; + + // Frame addresses (not managed IPs) — show as frame type + if (sourceType != 0) // SOS_StackSourceFrame + { + snprintf(buf, bufLen, "", (unsigned long long)source); + return; + } + + // Try to resolve the IP using the cDAC's ISOSDacInterface + ISOSDacInterface* pSos = s_cdacSosDac; + if (pSos == nullptr) + pSos = s_dacSosDac; // Fallback to legacy DAC + + if (pSos != nullptr) + { + CLRDATA_ADDRESS mdAddr = 0; + if (SUCCEEDED(pSos->GetMethodDescPtrFromIP(source, &mdAddr)) && mdAddr != 0) + { + WCHAR wname[256] = {}; + unsigned int nameLen = 0; + if (SUCCEEDED(pSos->GetMethodDescName(mdAddr, ARRAY_SIZE(wname), wname, &nameLen)) && nameLen > 0) + { + WideCharToMultiByte(CP_UTF8, 0, wname, -1, buf, bufLen, NULL, NULL); + return; + } + } + } + + snprintf(buf, bufLen, "", (unsigned long long)source); +} + //----------------------------------------------------------------------------- // Collect stack refs from the cDAC //----------------------------------------------------------------------------- -static bool CollectStackRefs(ISOSDacInterface* pSosDac, DWORD osThreadId, SArray* pRefs) +static bool CollectStackRefs(ISOSDacInterface* pSosDac, DWORD osThreadId, SArray* pRefs, + const char* label = nullptr) { if (pSosDac == nullptr) return false; @@ -548,7 +589,20 @@ static bool CollectStackRefs(ISOSDacInterface* pSosDac, DWORD osThreadId, SArray HRESULT hr = pSosDac->GetStackReferences(osThreadId, &pEnum); if (FAILED(hr) || pEnum == nullptr) + { + // Log the first failure per source for diagnostics + static bool s_loggedCdacFailure = false; + static bool s_loggedDacFailure = false; + bool* pLogged = (label != nullptr && strcmp(label, "DAC") == 0) ? &s_loggedDacFailure : &s_loggedCdacFailure; + if (s_logFile != nullptr && !*pLogged) + { + fprintf(s_logFile, "%s: GetStackReferences failed hr=0x%08x pEnum=%p thread=0x%x\n", + label ? label : "???", hr, pEnum, osThreadId); + fflush(s_logFile); + *pLogged = true; + } return false; + } SOSStackRefData refData; unsigned int fetched = 0; @@ -835,7 +889,6 @@ static void CompareStackWalks(Thread* pThread, PCONTEXT regs) bool mismatch = false; while (frameIdx < 200) // safety limit { - // Compare GetContext BYTE cdacCtx[4096] = {}; BYTE dacCtx[4096] = {}; ULONG32 cdacCtxSize = 0, dacCtxSize = 0; @@ -854,18 +907,8 @@ static void CompareStackWalks(Thread* pThread, PCONTEXT regs) if (hr1 != S_OK) break; // both finished - if (cdacCtxSize != dacCtxSize) - { - if (s_logFile) - fprintf(s_logFile, " [WALK_MISMATCH] Frame %d: Context size differs cDAC=%u DAC=%u\n", - frameIdx, cdacCtxSize, dacCtxSize); - mismatch = true; - } - else if (cdacCtxSize >= sizeof(CONTEXT)) + if (cdacCtxSize >= sizeof(CONTEXT) && dacCtxSize >= sizeof(CONTEXT)) { - // Compare IP and SP — these are what matter for stack walk parity. - // Other CONTEXT fields (floating-point, debug registers, xstate) may - // differ between cDAC and DAC without affecting the walk. PCODE cdacIP = GetIP((CONTEXT*)cdacCtx); PCODE dacIP = GetIP((CONTEXT*)dacCtx); TADDR cdacSP = GetSP((CONTEXT*)cdacCtx); @@ -882,28 +925,6 @@ static void CompareStackWalks(Thread* pThread, PCONTEXT regs) } } - // Compare Request(FRAME_DATA) - ULONG64 cdacFrameAddr = 0, dacFrameAddr = 0; - hr1 = cdacWalk->Request(0xf0000000, 0, nullptr, sizeof(cdacFrameAddr), (BYTE*)&cdacFrameAddr); - hr2 = dacWalk->Request(0xf0000000, 0, nullptr, sizeof(dacFrameAddr), (BYTE*)&dacFrameAddr); - - if (hr1 == S_OK && hr2 == S_OK && cdacFrameAddr != dacFrameAddr) - { - if (s_logFile) - { - PCODE cdacIP = 0, dacIP = 0; - if (cdacCtxSize >= sizeof(CONTEXT)) - cdacIP = GetIP((CONTEXT*)cdacCtx); - if (dacCtxSize >= sizeof(CONTEXT)) - dacIP = GetIP((CONTEXT*)dacCtx); - fprintf(s_logFile, " [WALK_MISMATCH] Frame %d: FrameAddr cDAC=0x%llx DAC=0x%llx (cDAC_IP=0x%llx DAC_IP=0x%llx)\n", - frameIdx, (unsigned long long)cdacFrameAddr, (unsigned long long)dacFrameAddr, - (unsigned long long)cdacIP, (unsigned long long)dacIP); - } - mismatch = true; - } - - // Advance both hr1 = cdacWalk->Next(); hr2 = dacWalk->Next(); @@ -929,29 +950,29 @@ static void CompareStackWalks(Thread* pThread, PCONTEXT regs) } //----------------------------------------------------------------------------- -//----------------------------------------------------------------------------- -// Compare two ref sets using two-phase matching. -// Phase 1: Match stack refs (Address != 0) by exact (Address, Object, Flags). -// Phase 2: Match register refs (Address == 0) by (Object, Flags) only. -// Returns true if all refs in setA have a match in setB and counts are equal. +// Per-frame ref comparison. +// +// Groups refs by Source (IP or Frame address), aligns the two sets by Source, +// and reports per-frame differences with resolved method names. +// Returns true if all refs match. //----------------------------------------------------------------------------- -static bool CompareRefSets(StackRef* refsA, int countA, StackRef* refsB, int countB) +// Compare two ref sets using two-phase matching (for RT comparison where we +// don't have Source info). Returns true if all refs match. +static bool CompareRefSetsFlat(StackRef* refsA, int countA, StackRef* refsB, int countB) { if (countA != countB) return false; if (countA == 0) return true; - if (countA > MAX_COLLECTED_REFS) - return false; bool matched[MAX_COLLECTED_REFS] = {}; + // Phase 1: Match stack refs (Address != 0) by exact (Address, Object, Flags). for (int i = 0; i < countA; i++) { if (refsA[i].Address == 0) continue; - bool found = false; for (int j = 0; j < countB; j++) { if (matched[j]) continue; @@ -960,18 +981,16 @@ static bool CompareRefSets(StackRef* refsA, int countA, StackRef* refsB, int cou refsA[i].Flags == refsB[j].Flags) { matched[j] = true; - found = true; break; } } - if (!found) return false; } + // Phase 2: Match register refs (Address == 0) by (Object, Flags) only. for (int i = 0; i < countA; i++) { if (refsA[i].Address != 0) continue; - bool found = false; for (int j = 0; j < countB; j++) { if (matched[j]) continue; @@ -979,16 +998,223 @@ static bool CompareRefSets(StackRef* refsA, int countA, StackRef* refsB, int cou refsA[i].Flags == refsB[j].Flags) { matched[j] = true; - found = true; break; } } - if (!found) return false; } + // Check that every ref in B was matched + for (int j = 0; j < countB; j++) + { + if (!matched[j]) + return false; + } return true; } +// Represents a group of refs from the same Source (managed frame or explicit Frame). +struct FrameRefGroup +{ + CLRDATA_ADDRESS Source; + int SourceType; // 0 = IP, 1 = Frame + int StartIdx; // Index into the original ref array + int Count; // Number of refs in this group +}; + +// Build a sorted list of unique Sources with their ref index ranges. +// The refs array is sorted by Source as a side effect. +static int __cdecl CompareBySource(const void* a, const void* b) +{ + const StackRef* ra = static_cast(a); + const StackRef* rb = static_cast(b); + if (ra->Source != rb->Source) + return (ra->Source < rb->Source) ? -1 : 1; + return 0; +} + +static int GroupRefsByFrame(StackRef* refs, int count, FrameRefGroup* groups, int maxGroups) +{ + if (count == 0) + return 0; + + qsort(refs, count, sizeof(StackRef), CompareBySource); + + int groupCount = 0; + CLRDATA_ADDRESS currentSource = refs[0].Source; + int startIdx = 0; + + for (int i = 1; i <= count; i++) + { + if (i == count || refs[i].Source != currentSource) + { + if (groupCount < maxGroups) + { + groups[groupCount].Source = currentSource; + groups[groupCount].SourceType = refs[startIdx].SourceType; + groups[groupCount].StartIdx = startIdx; + groups[groupCount].Count = i - startIdx; + groupCount++; + } + if (i < count) + { + currentSource = refs[i].Source; + startIdx = i; + } + } + } + return groupCount; +} + +// Compare refs within a single frame. Returns the number of unmatched refs in each set. +static void CompareFrameRefs(StackRef* refsA, int countA, StackRef* refsB, int countB, + int* unmatchedA, int* unmatchedB, + bool* aUsed, bool* bUsed) +{ + // Phase 1: exact (Address, Object, Flags) for Address != 0 + for (int i = 0; i < countA; i++) + { + if (refsA[i].Address == 0) + continue; + for (int j = 0; j < countB; j++) + { + if (bUsed[j]) continue; + if (refsA[i].Address == refsB[j].Address && + refsA[i].Object == refsB[j].Object && + refsA[i].Flags == refsB[j].Flags) + { + aUsed[i] = bUsed[j] = true; + break; + } + } + } + + // Phase 2: (Object, Flags) for Address=0 or unmatched refs + for (int i = 0; i < countA; i++) + { + if (aUsed[i]) continue; + for (int j = 0; j < countB; j++) + { + if (bUsed[j]) continue; + if (refsA[i].Object == refsB[j].Object && + refsA[i].Flags == refsB[j].Flags) + { + aUsed[i] = bUsed[j] = true; + break; + } + } + } + + *unmatchedA = 0; + *unmatchedB = 0; + for (int i = 0; i < countA; i++) + if (!aUsed[i]) (*unmatchedA)++; + for (int j = 0; j < countB; j++) + if (!bUsed[j]) (*unmatchedB)++; +} + +// Per-frame comparison: groups refs by Source, compares per-frame, logs structured diff. +// Returns true if all refs match between the two sets. +static bool ComparePerFrame(StackRef* refsA, int countA, const char* labelA, + StackRef* refsB, int countB, const char* labelB) +{ + static const int MAX_GROUPS = 256; + FrameRefGroup groupsA[MAX_GROUPS], groupsB[MAX_GROUPS]; + int numGroupsA = GroupRefsByFrame(refsA, countA, groupsA, MAX_GROUPS); + int numGroupsB = GroupRefsByFrame(refsB, countB, groupsB, MAX_GROUPS); + + bool allMatch = true; + int idxA = 0, idxB = 0; + + if (s_logFile) + fprintf(s_logFile, " [COMPARE %s-vs-%s]\n", labelA, labelB); + + while (idxA < numGroupsA || idxB < numGroupsB) + { + if (idxA < numGroupsA && idxB < numGroupsB && groupsA[idxA].Source == groupsB[idxB].Source) + { + // Both have this frame — compare refs within + int cA = groupsA[idxA].Count; + int cB = groupsB[idxB].Count; + bool aUsed[MAX_COLLECTED_REFS] = {}; + bool bUsed[MAX_COLLECTED_REFS] = {}; + int unmatchedA = 0, unmatchedB = 0; + + CompareFrameRefs(&refsA[groupsA[idxA].StartIdx], cA, + &refsB[groupsB[idxB].StartIdx], cB, + &unmatchedA, &unmatchedB, aUsed, bUsed); + + if (unmatchedA > 0 || unmatchedB > 0) + { + allMatch = false; + if (s_logFile) + { + char methodName[256]; + ResolveMethodName(groupsA[idxA].Source, groupsA[idxA].SourceType, methodName, sizeof(methodName)); + + // Log SP from first ref in each group for unwinder comparison + auto spA = refsA[groupsA[idxA].StartIdx].StackPointer; + auto spB = refsB[groupsB[idxB].StartIdx].StackPointer; + fprintf(s_logFile, " [FRAME_DIFF] Source=0x%llx (%s): %s=%d %s=%d SP_%s=0x%llx SP_%s=0x%llx%s\n", + (unsigned long long)groupsA[idxA].Source, methodName, labelA, cA, labelB, cB, + labelA, (unsigned long long)spA, labelB, (unsigned long long)spB, + spA != spB ? " <-- SP MISMATCH" : ""); + + // Dump ALL refs from both sides for detailed comparison + for (int i = 0; i < cA; i++) + { + auto& r = refsA[groupsA[idxA].StartIdx + i]; + fprintf(s_logFile, " [%s_%s] Addr=0x%llx Obj=0x%llx Flags=0x%x Reg=%d Off=%d\n", + labelA, aUsed[i] ? "MATCHED" : "ONLY", + (unsigned long long)r.Address, (unsigned long long)r.Object, r.Flags, + r.Register, r.Offset); + } + for (int j = 0; j < cB; j++) + { + auto& r = refsB[groupsB[idxB].StartIdx + j]; + fprintf(s_logFile, " [%s_%s] Addr=0x%llx Obj=0x%llx Flags=0x%x Reg=%d Off=%d\n", + labelB, bUsed[j] ? "MATCHED" : "ONLY", + (unsigned long long)r.Address, (unsigned long long)r.Object, r.Flags, + r.Register, r.Offset); + } + } + } + idxA++; + idxB++; + } + else if (idxB >= numGroupsB || (idxA < numGroupsA && groupsA[idxA].Source < groupsB[idxB].Source)) + { + // Frame only in A + allMatch = false; + if (s_logFile) + { + char methodName[256]; + ResolveMethodName(groupsA[idxA].Source, groupsA[idxA].SourceType, methodName, sizeof(methodName)); + fprintf(s_logFile, " [FRAME_%s_ONLY] Source=0x%llx (%s): %s=%d\n", + labelA, (unsigned long long)groupsA[idxA].Source, methodName, labelA, groupsA[idxA].Count); + } + idxA++; + } + else + { + // Frame only in B + allMatch = false; + if (s_logFile) + { + char methodName[256]; + ResolveMethodName(groupsB[idxB].Source, groupsB[idxB].SourceType, methodName, sizeof(methodName)); + fprintf(s_logFile, " [FRAME_%s_ONLY] Source=0x%llx (%s): %s=%d\n", + labelB, (unsigned long long)groupsB[idxB].Source, methodName, labelB, groupsB[idxB].Count); + } + idxB++; + } + } + + if (allMatch && s_logFile) + fprintf(s_logFile, " [MATCH] All %d refs matched\n", countA); + + return allMatch; +} + //----------------------------------------------------------------------------- // Filter interior stack pointers and deduplicate a ref set in place. //----------------------------------------------------------------------------- @@ -1024,12 +1250,33 @@ void CdacStress::VerifyAtAllocPoint() if (t_inVerification) return; + if (ShouldSkipStressPoint()) + return; + Thread* pThread = GetThreadNULLOk(); if (pThread == nullptr || !pThread->PreemptiveGCDisabled()) return; + // Capture the current context and unwind past VerifyAtAllocPoint + // so the walk starts from the caller, not from inside this function. CONTEXT ctx; RtlCaptureContext(&ctx); +#ifndef TARGET_UNIX + { + ULONG64 imageBase = 0; + PRUNTIME_FUNCTION pFunctionEntry = RtlLookupFunctionEntry(GetIP(&ctx), &imageBase, nullptr); + if (pFunctionEntry != nullptr) + { + void* handlerData = nullptr; + ULONG64 establisherFrame = 0; + RtlVirtualUnwind(UNW_FLAG_NHANDLER, imageBase, GetIP(&ctx), + pFunctionEntry, &ctx, &handlerData, &establisherFrame, nullptr); + } + } +#else + PAL_VirtualUnwind(&ctx); +#endif + VerifyAtStressPoint(pThread, &ctx); } @@ -1067,21 +1314,15 @@ void CdacStress::VerifyAtStressPoint(Thread* pThread, PCONTEXT regs) // Flush the cDAC's ProcessedData cache so it re-reads from the live process. if (s_cdacProcess != nullptr) - { s_cdacProcess->Flush(); - } // Flush the legacy DAC cache too. if (s_dacProcess != nullptr) - { s_dacProcess->Flush(); - } // Compare IXCLRDataStackWalk frame-by-frame between cDAC and legacy DAC. if (s_cdacStressLevel & CDACSTRESS_WALK) - { CompareStackWalks(pThread, regs); - } // Compare GC stack references. if (!(s_cdacStressLevel & CDACSTRESS_REFS)) @@ -1092,54 +1333,45 @@ void CdacStress::VerifyAtStressPoint(Thread* pThread, PCONTEXT regs) } // Step 1: Collect raw refs from cDAC (always) and DAC (if USE_DAC). + // Save and restore the thread's Frame chain around the cDAC call because + // the cDAC runs as NativeAOT managed code on the same thread and may + // push its own Frames during execution. DWORD osThreadId = pThread->GetOSThreadId(); + Frame* pSavedFrame = pThread->GetFrame(); SArray cdacRefs; - bool haveCdac = CollectStackRefs(s_cdacSosDac, osThreadId, &cdacRefs); + bool haveCdac = CollectStackRefs(s_cdacSosDac, osThreadId, &cdacRefs, "cDAC"); + + // Restore the Frame chain to what it was before the cDAC walk + pThread->SetFrame(pSavedFrame); SArray dacRefs; bool haveDac = false; if (s_cdacStressLevel & CDACSTRESS_USE_DAC) - { - haveDac = (s_dacSosDac != nullptr) && CollectStackRefs(s_dacSosDac, osThreadId, &dacRefs); - } + haveDac = (s_dacSosDac != nullptr) && CollectStackRefs(s_dacSosDac, osThreadId, &dacRefs, "DAC"); s_currentContext = nullptr; s_currentThreadId = 0; StackRef runtimeRefsBuf[MAX_COLLECTED_REFS]; int runtimeCount = 0; - bool haveRuntime = CollectRuntimeStackRefs(pThread, regs, runtimeRefsBuf, &runtimeCount); + bool rtOverflow = !CollectRuntimeStackRefs(pThread, regs, runtimeRefsBuf, &runtimeCount); + if (rtOverflow && s_logFile != nullptr) + { + fprintf(s_logFile, "[SKIP] Thread=0x%x IP=0x%p - RT overflow (>%d refs)\n", + osThreadId, (void*)GetIP(regs), MAX_COLLECTED_REFS); + } - if (!haveCdac || !haveRuntime) + if (!haveCdac) { InterlockedIncrement(&s_verifySkip); if (s_logFile != nullptr) - { - if (!haveCdac) - fprintf(s_logFile, "[SKIP] Thread=0x%x IP=0x%p - cDAC GetStackReferences failed\n", - osThreadId, (void*)GetIP(regs)); - else - fprintf(s_logFile, "[SKIP] Thread=0x%x IP=0x%p - runtime CollectRuntimeStackRefs overflowed\n", - osThreadId, (void*)GetIP(regs)); - } + fprintf(s_logFile, "[SKIP] Thread=0x%x IP=0x%p - cDAC GetStackReferences failed\n", + osThreadId, (void*)GetIP(regs)); return; } - // Step 2: Compare cDAC vs DAC raw (before any filtering). - int rawCdacCount = (int)cdacRefs.GetCount(); - int rawDacCount = haveDac ? (int)dacRefs.GetCount() : -1; - bool dacMatch = true; - if (haveDac) - { - StackRef* cdacBuf = cdacRefs.OpenRawBuffer(); - StackRef* dacBuf = dacRefs.OpenRawBuffer(); - dacMatch = CompareRefSets(cdacBuf, rawCdacCount, dacBuf, rawDacCount); - cdacRefs.CloseRawBuffer(); - dacRefs.CloseRawBuffer(); - } - - // Step 3: Filter cDAC refs and compare vs RT (always). + // Step 2: Compute stack limit for filtering. Frame* pTopFrame = pThread->GetFrame(); Object** topStack = (Object**)pTopFrame; if (InlinedCallFrame::FrameHasActiveCall(pTopFrame)) @@ -1149,56 +1381,168 @@ void CdacStress::VerifyAtStressPoint(Thread* pThread, PCONTEXT regs) } uintptr_t stackLimit = (uintptr_t)topStack; + // Step 3: Apply unified filtering to both cDAC and DAC refs. + int rawCdacCount = (int)cdacRefs.GetCount(); + int rawDacCount = haveDac ? (int)dacRefs.GetCount() : -1; + int filteredCdacCount = rawCdacCount; if (filteredCdacCount > 0) { - StackRef* cdacBuf = cdacRefs.OpenRawBuffer(); - filteredCdacCount = FilterAndDedup(cdacBuf, filteredCdacCount, pThread, stackLimit); + StackRef* buf = cdacRefs.OpenRawBuffer(); + filteredCdacCount = FilterAndDedup(buf, filteredCdacCount, pThread, stackLimit); cdacRefs.CloseRawBuffer(); } + + int filteredDacCount = rawDacCount; + if (haveDac && filteredDacCount > 0) + { + StackRef* buf = dacRefs.OpenRawBuffer(); + filteredDacCount = FilterAndDedup(buf, filteredDacCount, pThread, stackLimit); + dacRefs.CloseRawBuffer(); + } + runtimeCount = DeduplicateRefs(runtimeRefsBuf, runtimeCount); - StackRef* cdacBuf = cdacRefs.OpenRawBuffer(); - bool rtMatch = CompareRefSets(cdacBuf, filteredCdacCount, runtimeRefsBuf, runtimeCount); - cdacRefs.CloseRawBuffer(); + StackRef* cdacBuf = nullptr; - // Step 4: Pass requires cDAC vs RT match. - // DAC mismatch is logged separately but doesn't affect pass/fail. - bool pass = rtMatch; + // Step 4: Compare cDAC vs DAC (determines pass/fail when USE_DAC is set). + bool dacMatch = true; + if (haveDac) + { + cdacBuf = cdacRefs.OpenRawBuffer(); + StackRef* dacBuf = dacRefs.OpenRawBuffer(); + dacMatch = CompareRefSetsFlat(cdacBuf, filteredCdacCount, dacBuf, filteredDacCount); + cdacRefs.CloseRawBuffer(); + dacRefs.CloseRawBuffer(); + } + + // Step 5: Compare cDAC vs RT (informational). + cdacBuf = cdacRefs.OpenRawBuffer(); + bool rtMatch = CompareRefSetsFlat(cdacBuf, filteredCdacCount, runtimeRefsBuf, runtimeCount); + cdacRefs.CloseRawBuffer(); + bool pass = haveDac ? dacMatch : rtMatch; if (pass) InterlockedIncrement(&s_verifyPass); else InterlockedIncrement(&s_verifyFail); - // Step 5: Log results. + // Step 6: Log structured results. if (s_logFile != nullptr) { - const char* label = pass ? "PASS" : "FAIL"; - if (pass && !dacMatch) - label = "DAC_MISMATCH"; - fprintf(s_logFile, "[%s] Thread=0x%x IP=0x%p cDAC=%d DAC=%d RT=%d\n", - label, osThreadId, (void*)GetIP(regs), - rawCdacCount, rawDacCount, runtimeCount); - - if (!pass || !dacMatch) + if (pass && rtMatch) { - for (int i = 0; i < rawCdacCount; i++) - fprintf(s_logFile, " cDAC [%d]: Address=0x%llx Object=0x%llx Flags=0x%x Source=0x%llx SourceType=%d SP=0x%llx\n", - i, (unsigned long long)cdacRefs[i].Address, (unsigned long long)cdacRefs[i].Object, - cdacRefs[i].Flags, (unsigned long long)cdacRefs[i].Source, cdacRefs[i].SourceType, - (unsigned long long)cdacRefs[i].StackPointer); - if (haveDac) + // Clean pass — one-liner + fprintf(s_logFile, "[PASS] Thread=0x%x IP=0x%p cDAC=%d DAC=%d RT=%d\n", + osThreadId, (void*)GetIP(regs), filteredCdacCount, filteredDacCount, runtimeCount); + } + else + { + // Failure or RT mismatch — structured per-frame output + const char* label = pass ? "PASS" : "FAIL"; + fprintf(s_logFile, "[%s] Thread=0x%x IP=0x%p cDAC=%d DAC=%d RT=%d\n", + label, osThreadId, (void*)GetIP(regs), filteredCdacCount, filteredDacCount, runtimeCount); + + if (!dacMatch && haveDac) { - for (int i = 0; i < rawDacCount; i++) - fprintf(s_logFile, " DAC [%d]: Address=0x%llx Object=0x%llx Flags=0x%x Source=0x%llx\n", - i, (unsigned long long)dacRefs[i].Address, (unsigned long long)dacRefs[i].Object, - dacRefs[i].Flags, (unsigned long long)dacRefs[i].Source); + cdacBuf = cdacRefs.OpenRawBuffer(); + StackRef* dacBuf = dacRefs.OpenRawBuffer(); + ComparePerFrame(cdacBuf, filteredCdacCount, "cDAC", + dacBuf, filteredDacCount, "DAC"); + cdacRefs.CloseRawBuffer(); + dacRefs.CloseRawBuffer(); + } + + if (!rtMatch) + { + fprintf(s_logFile, " [RT_DIFF] cDAC=%d RT=%d (cDAC matches DAC but differs from RT)\n", + filteredCdacCount, runtimeCount); + } + + // Log a stack trace derived from unique Source IPs in the refs + if (!pass) + { + fprintf(s_logFile, " [STACK_TRACE] (cDAC=%d DAC=%d RT=%d)\n", + filteredCdacCount, filteredDacCount, runtimeCount); + + // Collect unique Sources from cDAC refs (sorted by SP descending = top of stack first) + struct FrameEntry { CLRDATA_ADDRESS Source; int SourceType; CLRDATA_ADDRESS SP; }; + FrameEntry frames[256]; + int frameCount = 0; + + cdacBuf = cdacRefs.OpenRawBuffer(); + for (int i = 0; i < filteredCdacCount && frameCount < 256; i++) + { + bool dup = false; + for (int j = 0; j < frameCount; j++) + { + if (frames[j].Source == cdacBuf[i].Source) + { + dup = true; + break; + } + } + if (!dup) + { + frames[frameCount].Source = cdacBuf[i].Source; + frames[frameCount].SourceType = cdacBuf[i].SourceType; + frames[frameCount].SP = cdacBuf[i].StackPointer; + frameCount++; + } + } + cdacRefs.CloseRawBuffer(); + + // Also include DAC-only sources that the cDAC missed + if (haveDac) + { + StackRef* dacBuf = dacRefs.OpenRawBuffer(); + for (int i = 0; i < filteredDacCount && frameCount < 256; i++) + { + bool dup = false; + for (int j = 0; j < frameCount; j++) + { + if (frames[j].Source == dacBuf[i].Source) + { + dup = true; + break; + } + } + if (!dup) + { + frames[frameCount].Source = dacBuf[i].Source; + frames[frameCount].SourceType = dacBuf[i].SourceType; + frames[frameCount].SP = 0; // DAC doesn't provide SP + frameCount++; + } + } + dacRefs.CloseRawBuffer(); + } + + for (int i = 0; i < frameCount; i++) + { + char methodName[256]; + ResolveMethodName(frames[i].Source, frames[i].SourceType, methodName, sizeof(methodName)); + + // Count refs from this source in cDAC and DAC + int cdacCount = 0, dacCount = 0; + cdacBuf = cdacRefs.OpenRawBuffer(); + for (int j = 0; j < filteredCdacCount; j++) + if (cdacBuf[j].Source == frames[i].Source) cdacCount++; + cdacRefs.CloseRawBuffer(); + + if (haveDac) + { + StackRef* dacBuf2 = dacRefs.OpenRawBuffer(); + for (int j = 0; j < filteredDacCount; j++) + if (dacBuf2[j].Source == frames[i].Source) dacCount++; + dacRefs.CloseRawBuffer(); + } + + const char* marker = (cdacCount != dacCount) ? " <-- MISMATCH" : ""; + fprintf(s_logFile, " #%d %s (cDAC=%d DAC=%d)%s\n", + i, methodName, cdacCount, dacCount, marker); + } } - for (int i = 0; i < runtimeCount; i++) - fprintf(s_logFile, " RT [%d]: Address=0x%llx Object=0x%llx Flags=0x%x\n", - i, (unsigned long long)runtimeRefsBuf[i].Address, (unsigned long long)runtimeRefsBuf[i].Object, - runtimeRefsBuf[i].Flags); fflush(s_logFile); } diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index b7e13c1f8574db..62714b3266a672 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -149,6 +149,8 @@ CDAC_TYPE_FIELD(ExceptionInfo, T_UINT8, PassNumber, offsetof(ExInfo, m_passNumbe CDAC_TYPE_FIELD(ExceptionInfo, T_POINTER, CSFEHClause, offsetof(ExInfo, m_csfEHClause)) CDAC_TYPE_FIELD(ExceptionInfo, T_POINTER, CSFEnclosingClause, offsetof(ExInfo, m_csfEnclosingClause)) CDAC_TYPE_FIELD(ExceptionInfo, T_POINTER, CallerOfActualHandlerFrame, offsetof(ExInfo, m_sfCallerOfActualHandlerFrame)) +CDAC_TYPE_FIELD(ExceptionInfo, T_UINT32, ClauseForCatchHandlerStartPC, offsetof(ExInfo, m_ClauseForCatch) + offsetof(EE_ILEXCEPTION_CLAUSE, HandlerStartPC)) +CDAC_TYPE_FIELD(ExceptionInfo, T_UINT32, ClauseForCatchHandlerEndPC, offsetof(ExInfo, m_ClauseForCatch) + offsetof(EE_ILEXCEPTION_CLAUSE, HandlerEndPC)) CDAC_TYPE_END(ExceptionInfo) CDAC_TYPE_BEGIN(ObjectHandle) @@ -724,6 +726,8 @@ CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, HotColdMap, cdac_data CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, DelayLoadMethodCallThunks, cdac_data::DelayLoadMethodCallThunks) CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, DebugInfoSection, cdac_data::DebugInfoSection) CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, ExceptionInfoSection, cdac_data::ExceptionInfoSection) +CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, ImportSections, cdac_data::ImportSections) +CDAC_TYPE_FIELD(ReadyToRunInfo, T_UINT32, NumImportSections, cdac_data::NumImportSections) CDAC_TYPE_FIELD(ReadyToRunInfo, TYPE(HashMap), EntryPointToMethodDescMap, cdac_data::EntryPointToMethodDescMap) CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, LoadedImageBase, cdac_data::LoadedImageBase) CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, Composite, cdac_data::Composite) @@ -969,6 +973,31 @@ CDAC_TYPE_FIELD(TransitionBlock, TYPE(CalleeSavedRegisters), CalleeSavedRegister #ifdef TARGET_ARM CDAC_TYPE_FIELD(TransitionBlock, TYPE(ArgumentRegisters), ArgumentRegisters, offsetof(TransitionBlock, m_argumentRegisters)) #endif // TARGET_ARM +// Offset to where stack arguments begin (just past the end of the TransitionBlock) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, OffsetOfArgs, sizeof(TransitionBlock)) +// Offset to where argument registers are saved in the TransitionBlock +#if (defined(TARGET_AMD64) && !defined(UNIX_AMD64_ABI)) || defined(TARGET_WASM) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegistersOffset, sizeof(TransitionBlock)) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, FirstGCRefMapSlot, sizeof(TransitionBlock)) +#elif defined(TARGET_ARM64) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegistersOffset, offsetof(TransitionBlock, m_argumentRegisters)) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, FirstGCRefMapSlot, offsetof(TransitionBlock, m_x8RetBuffReg)) +#else +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegistersOffset, offsetof(TransitionBlock, m_argumentRegisters)) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, FirstGCRefMapSlot, offsetof(TransitionBlock, m_argumentRegisters)) +#endif +// Negative offset to where float argument registers are saved (relative to TransitionBlock pointer). +// This is -sizeof(FloatArgumentRegisters) (-padding) on platforms that have them, 0 otherwise. +#ifdef CALLDESCR_FPARGREGS +#ifdef TARGET_ARM +// ARM has 8-byte alignment padding after FloatArgumentRegisters +CDAC_TYPE_FIELD(TransitionBlock, T_INT32, OffsetOfFloatArgumentRegisters, -(int)(sizeof(FloatArgumentRegisters) + TARGET_POINTER_SIZE)) +#else +CDAC_TYPE_FIELD(TransitionBlock, T_INT32, OffsetOfFloatArgumentRegisters, -(int)sizeof(FloatArgumentRegisters)) +#endif +#else +CDAC_TYPE_FIELD(TransitionBlock, T_INT32, OffsetOfFloatArgumentRegisters, 0) +#endif CDAC_TYPE_END(TransitionBlock) #ifdef DEBUGGING_SUPPORTED @@ -989,8 +1018,23 @@ CDAC_TYPE_SIZE(sizeof(StubDispatchFrame)) CDAC_TYPE_FIELD(StubDispatchFrame, T_POINTER, RepresentativeMTPtr, cdac_data::RepresentativeMTPtr) CDAC_TYPE_FIELD(StubDispatchFrame, T_POINTER, MethodDescPtr, cdac_data::MethodDescPtr) CDAC_TYPE_FIELD(StubDispatchFrame, T_UINT32, RepresentativeSlot, cdac_data::RepresentativeSlot) +CDAC_TYPE_FIELD(StubDispatchFrame, T_POINTER, GCRefMap, cdac_data::GCRefMap) +CDAC_TYPE_FIELD(StubDispatchFrame, T_POINTER, ZapModule, cdac_data::ZapModule) +CDAC_TYPE_FIELD(StubDispatchFrame, T_POINTER, Indirection, cdac_data::Indirection) CDAC_TYPE_END(StubDispatchFrame) +CDAC_TYPE_BEGIN(ExternalMethodFrame) +CDAC_TYPE_SIZE(sizeof(ExternalMethodFrame)) +CDAC_TYPE_FIELD(ExternalMethodFrame, T_POINTER, GCRefMap, cdac_data::GCRefMap) +CDAC_TYPE_FIELD(ExternalMethodFrame, T_POINTER, Indirection, cdac_data::Indirection) +CDAC_TYPE_FIELD(ExternalMethodFrame, T_POINTER, ZapModule, cdac_data::ZapModule) +CDAC_TYPE_END(ExternalMethodFrame) + +CDAC_TYPE_BEGIN(DynamicHelperFrame) +CDAC_TYPE_SIZE(sizeof(DynamicHelperFrame)) +CDAC_TYPE_FIELD(DynamicHelperFrame, T_INT32, DynamicHelperFrameFlags, cdac_data::DynamicHelperFrameFlags) +CDAC_TYPE_END(DynamicHelperFrame) + #ifdef FEATURE_HIJACK CDAC_TYPE_BEGIN(ResumableFrame) CDAC_TYPE_SIZE(sizeof(ResumableFrame)) @@ -1374,6 +1418,7 @@ CDAC_GLOBAL_POINTER(MetadataUpdatesApplied, &::g_metadataUpdatesApplied) #undef FRAME_TYPE_NAME CDAC_GLOBAL(MethodDescTokenRemainderBitCount, T_UINT8, METHOD_TOKEN_REMAINDER_BIT_COUNT) + #if FEATURE_COMINTEROP CDAC_GLOBAL(FeatureCOMInterop, T_UINT8, 1) #else diff --git a/src/coreclr/vm/frames.h b/src/coreclr/vm/frames.h index f3fccab5615efa..708b7eec57f983 100644 --- a/src/coreclr/vm/frames.h +++ b/src/coreclr/vm/frames.h @@ -1490,6 +1490,9 @@ struct cdac_data { static constexpr size_t RepresentativeMTPtr = offsetof(StubDispatchFrame, m_pRepresentativeMT); static constexpr uint32_t RepresentativeSlot = offsetof(StubDispatchFrame, m_representativeSlot); + static constexpr size_t GCRefMap = offsetof(StubDispatchFrame, m_pGCRefMap); + static constexpr size_t ZapModule = offsetof(StubDispatchFrame, m_pZapModule); + static constexpr size_t Indirection = offsetof(StubDispatchFrame, m_pIndirection); }; typedef DPTR(class StubDispatchFrame) PTR_StubDispatchFrame; @@ -1561,10 +1564,20 @@ class ExternalMethodFrame : public FramedMethodFrame #ifdef TARGET_X86 void UpdateRegDisplay_Impl(const PREGDISPLAY pRD, bool updateFloats = false); #endif + + friend struct ::cdac_data; }; typedef DPTR(class ExternalMethodFrame) PTR_ExternalMethodFrame; +template <> +struct cdac_data +{ + static constexpr size_t GCRefMap = offsetof(ExternalMethodFrame, m_pGCRefMap); + static constexpr size_t Indirection = offsetof(ExternalMethodFrame, m_pIndirection); + static constexpr size_t ZapModule = offsetof(ExternalMethodFrame, m_pZapModule); +}; + class DynamicHelperFrame : public FramedMethodFrame { int m_dynamicHelperFrameFlags; @@ -1583,10 +1596,18 @@ class DynamicHelperFrame : public FramedMethodFrame LIMITED_METHOD_DAC_CONTRACT; return TT_InternalCall; } + + friend struct ::cdac_data; }; typedef DPTR(class DynamicHelperFrame) PTR_DynamicHelperFrame; +template <> +struct cdac_data +{ + static constexpr size_t DynamicHelperFrameFlags = offsetof(DynamicHelperFrame, m_dynamicHelperFrameFlags); +}; + //------------------------------------------------------------------------ // This frame protects object references for the EE's convenience. // This frame type actually is created from C++. diff --git a/src/coreclr/vm/gccover.cpp b/src/coreclr/vm/gccover.cpp index 64f22359891a57..7069ddb818f7d6 100644 --- a/src/coreclr/vm/gccover.cpp +++ b/src/coreclr/vm/gccover.cpp @@ -853,6 +853,24 @@ void DoGcStress (PCONTEXT regs, NativeCodeVersion nativeCodeVersion) enableWhenDone = true; } + // When DOTNET_CdacStressStep > 1, skip most stress points (both cDAC verification + // and StressHeap) to reduce overhead. + if (CdacStress::IsInitialized() && CdacStress::ShouldSkipStressPoint()) + { + if (pThread->HasPendingGCStressInstructionUpdate()) + UpdateGCStressInstructionWithoutGC(); + + FlushInstructionCache(GetCurrentProcess(), (LPCVOID)instrPtr, 4); + + if (enableWhenDone) + { + BOOL b = GC_ON_TRANSITIONS(FALSE); + pThread->EnablePreemptiveGC(); + GC_ON_TRANSITIONS(b); + } + return; + } + // // If we redirect for gc stress, we don't need this frame on the stack, // the redirection will push a resumable frame. @@ -1177,6 +1195,18 @@ void DoGcStress (PCONTEXT regs, NativeCodeVersion nativeCodeVersion) // code and it will just raise a STATUS_ACCESS_VIOLATION. pThread->PostGCStressInstructionUpdate((BYTE*)instrPtr, &gcCover->savedCode[offset]); + // When DOTNET_CdacStressStep > 1, skip most stress points (both cDAC verification + // and StressHeap) to reduce overhead. We still restore the instruction since the + // breakpoint must be removed regardless. + if (CdacStress::IsInitialized() && CdacStress::ShouldSkipStressPoint()) + { + if (pThread->HasPendingGCStressInstructionUpdate()) + UpdateGCStressInstructionWithoutGC(); + + FlushInstructionCache(GetCurrentProcess(), (LPCVOID)instrPtr, 4); + return; + } + // we should be in coop mode. _ASSERTE(pThread->PreemptiveGCDisabled()); diff --git a/src/coreclr/vm/readytoruninfo.h b/src/coreclr/vm/readytoruninfo.h index 6963a5000311e7..64c3324d9b2acc 100644 --- a/src/coreclr/vm/readytoruninfo.h +++ b/src/coreclr/vm/readytoruninfo.h @@ -406,6 +406,8 @@ struct cdac_data static constexpr size_t DelayLoadMethodCallThunks = offsetof(ReadyToRunInfo, m_pSectionDelayLoadMethodCallThunks); static constexpr size_t DebugInfoSection = offsetof(ReadyToRunInfo, m_pSectionDebugInfo); static constexpr size_t ExceptionInfoSection = offsetof(ReadyToRunInfo, m_pSectionExceptionInfo); + static constexpr size_t ImportSections = offsetof(ReadyToRunInfo, m_pImportSections); + static constexpr size_t NumImportSections = offsetof(ReadyToRunInfo, m_nImportSections); static constexpr size_t EntryPointToMethodDescMap = offsetof(ReadyToRunInfo, m_entryPointToMethodDescMap); static constexpr size_t LoadedImageBase = offsetof(ReadyToRunInfo, m_pLoadedImageBase); static constexpr size_t Composite = offsetof(ReadyToRunInfo, m_pComposite); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs index 0dddd31417e5ad..bb31d1434b8832 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs @@ -101,6 +101,13 @@ public interface IExecutionManager : IContract List GetExceptionClauses(CodeBlockHandle codeInfoHandle) => throw new NotImplementedException(); JitManagerInfo GetEEJitManagerInfo() => throw new NotImplementedException(); IEnumerable GetCodeHeapInfos() => throw new NotImplementedException(); + + /// + /// Finds the R2R module that contains the given address. + /// Used by FindGCRefMap to resolve m_pZapModule when it's null. + /// Matches native ExecutionManager::FindReadyToRunModule (codeman.cpp). + /// + TargetPointer FindReadyToRunModule(TargetPointer address) => throw new NotImplementedException(); } public readonly struct ExecutionManager : IExecutionManager diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs index d94ed45048b256..70ab5217cd7976 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs @@ -2,10 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; namespace Microsoft.Diagnostics.DataContractReader.Contracts; public interface IGCInfoHandle { } +public readonly record struct InterruptibleRange(uint StartOffset, uint EndOffset); public interface IGCInfo : IContract { @@ -14,6 +16,7 @@ public interface IGCInfo : IContract IGCInfoHandle DecodePlatformSpecificGCInfo(TargetPointer gcInfoAddress, uint gcVersion) => throw new NotImplementedException(); IGCInfoHandle DecodeInterpreterGCInfo(TargetPointer gcInfoAddress, uint gcVersion) => throw new NotImplementedException(); uint GetCodeLength(IGCInfoHandle handle) => throw new NotImplementedException(); + IReadOnlyList GetInterruptibleRanges(IGCInfoHandle handle) => throw new NotImplementedException(); } public readonly struct GCInfo : IGCInfo diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs index 8cc74fc5d1ee72..7f700a0c479367 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs @@ -179,6 +179,14 @@ public interface IRuntimeTypeSystem : IContract bool IsGenericMethodDefinition(MethodDescHandle methodDesc) => throw new NotImplementedException(); ReadOnlySpan GetGenericMethodInstantiation(MethodDescHandle methodDesc) => throw new NotImplementedException(); + // Return true if the method requires a hidden instantiation argument (generic context parameter). + // This corresponds to native MethodDesc::RequiresInstArg(). + bool RequiresInstArg(MethodDescHandle methodDesc) => throw new NotImplementedException(); + + // Return true if the method uses the async calling convention (CORINFO_CALLCONV_ASYNCCALL). + // This corresponds to native MethodDesc::IsAsyncMethod(). + bool IsAsyncMethod(MethodDescHandle methodDesc) => throw new NotImplementedException(); + // Return mdtMethodDef (0x06000000) if the method doesn't have a token, otherwise return the token of the method uint GetMethodToken(MethodDescHandle methodDesc) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs index 989db1625b33f9..db776c50a53057 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs @@ -160,6 +160,9 @@ public enum DataType HijackFrame, TailCallFrame, StubDispatchFrame, + ExternalMethodFrame, + DynamicHelperFrame, + ComCallWrapper, SimpleComCallWrapper, ComMethodTable, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs index 43e89fbe2237e7..47f629ffd5700b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs @@ -390,6 +390,19 @@ TargetNUInt IExecutionManager.GetRelativeOffset(CodeBlockHandle codeInfoHandle) return info.RelativeOffset; } + TargetPointer IExecutionManager.FindReadyToRunModule(TargetPointer address) + { + // Use the range section map to find the RangeSection containing the address. + // The R2R range section covers the entire PE image (code + data), so this + // works for import section addresses used by FindGCRefMap. + TargetCodePointer codeAddr = CodePointerUtils.CodePointerFromAddress(address, _target); + RangeSection range = RangeSection.Find(_target, _topRangeSectionMap, _rangeSectionMapLookup, codeAddr); + if (range.Data is null) + return TargetPointer.Null; + + return range.Data.R2RModule; + } + JitManagerInfo IExecutionManager.GetEEJitManagerInfo() { TargetPointer eeJitManagerPtr = _target.ReadGlobalPointer(Constants.Globals.EEJitManagerAddress); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs index b636a2914c36ec..c082eb9ccdc969 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs @@ -34,5 +34,6 @@ internal ExecutionManager_1(Target target) public List GetExceptionClauses(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetExceptionClauses(codeInfoHandle); public JitManagerInfo GetEEJitManagerInfo() => _executionManagerCore.GetEEJitManagerInfo(); public IEnumerable GetCodeHeapInfos() => _executionManagerCore.GetCodeHeapInfos(); + public TargetPointer FindReadyToRunModule(TargetPointer address) => _executionManagerCore.FindReadyToRunModule(address); public void Flush() => _executionManagerCore.Flush(); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_2.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_2.cs index 6b84fda982ab5e..da5b1d6dc71f93 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_2.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_2.cs @@ -34,5 +34,6 @@ internal ExecutionManager_2(Target target) public List GetExceptionClauses(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetExceptionClauses(codeInfoHandle); public JitManagerInfo GetEEJitManagerInfo() => _executionManagerCore.GetEEJitManagerInfo(); public IEnumerable GetCodeHeapInfos() => _executionManagerCore.GetCodeHeapInfos(); + public TargetPointer FindReadyToRunModule(TargetPointer address) => _executionManagerCore.FindReadyToRunModule(address); public void Flush() => _executionManagerCore.Flush(); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs index d6a6a0da8b39f4..20a300df238ec7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs @@ -68,8 +68,6 @@ internal enum GcStackSlotBase : uint GC_SPBASE_LAST = GC_FRAMEREG_REL, } - public readonly record struct InterruptibleRange(uint StartOffset, uint EndOffset); - public readonly record struct GcSlotDesc { /* Register Slot */ @@ -569,7 +567,7 @@ public bool EnumerateLiveSlots( uint numTracked = NumTrackedSlots; if (numTracked == 0) - goto ReportUntracked; + return ReportUntrackedAndSucceed(); uint normBreakOffset = TTraits.NormalizeCodeOffset(instructionOffset); @@ -655,7 +653,7 @@ public bool EnumerateLiveSlots( fReport = !fReport; } Debug.Assert(readSlots == numTracked); - goto ReportUntracked; + return ReportUntrackedAndSucceed(); } // Normal 1-bit-per-slot encoding follows } @@ -669,7 +667,7 @@ public bool EnumerateLiveSlots( if (_reader.ReadBits(1, ref bitOffset) != 0) ReportSlot(slotIndex, reportScratchSlots, reportFpBasedSlotsOnly, reportSlot); } - goto ReportUntracked; + return ReportUntrackedAndSucceed(); } else { @@ -682,7 +680,7 @@ public bool EnumerateLiveSlots( bitOffset += (int)(_numSafePoints * numTracked); if (_numInterruptibleRanges == 0) - goto ReportUntracked; + return ReportUntrackedAndSucceed(); } // ---- Fully-interruptible path ---- @@ -695,7 +693,7 @@ public bool EnumerateLiveSlots( uint numBitsPerPointer = (uint)_reader.DecodeVarLengthUnsigned(TTraits.POINTER_SIZE_ENCBASE, ref bitOffset); if (numBitsPerPointer == 0) - goto ReportUntracked; + return ReportUntrackedAndSucceed(); int pointerTablePos = bitOffset; @@ -709,7 +707,7 @@ public bool EnumerateLiveSlots( if (chunkPointer != 0) break; if (chunk-- == 0) - goto ReportUntracked; + return ReportUntrackedAndSucceed(); } int chunksStartPos = (int)(((uint)pointerTablePos + numChunks * numBitsPerPointer + 7) & (~7u)); @@ -815,14 +813,22 @@ public bool EnumerateLiveSlots( } } - ReportUntracked: - if (_numUntrackedSlots > 0 && (flags & (CodeManagerFlags.ParentOfFuncletStackFrame | CodeManagerFlags.NoReportUntracked)) == 0) + return ReportUntrackedAndSucceed(); + + bool ReportUntrackedAndSucceed() { - for (uint slotIndex = numTracked; slotIndex < _numSlots; slotIndex++) - ReportSlot(slotIndex, reportScratchSlots, reportFpBasedSlotsOnly, reportSlot); + if (_numUntrackedSlots > 0 && (flags & (CodeManagerFlags.ParentOfFuncletStackFrame | CodeManagerFlags.NoReportUntracked)) == 0) + { + // Native passes reportScratchSlots=true for untracked slots (see native + // ReportUntrackedSlots: "Report everything (although there should *never* + // be any scratch slots that are untracked)"). In practice the JIT can + // produce untracked scratch register slots for interior pointers, so they + // must be reported regardless of whether this is a leaf frame. + for (uint slotIndex = numTracked; slotIndex < _numSlots; slotIndex++) + ReportSlot(slotIndex, reportScratchSlots: true, reportFpBasedSlotsOnly, reportSlot); + } + return true; } - - return true; } private void ReportSlot(uint slotIndex, bool reportScratchSlots, bool reportFpBasedSlotsOnly, Action reportSlot) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfo_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfo_1.cs index f34292572a936e..9e6ce0252128bd 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfo_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfo_1.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; namespace Microsoft.Diagnostics.DataContractReader.Contracts; @@ -27,6 +28,12 @@ uint IGCInfo.GetCodeLength(IGCInfoHandle gcInfoHandle) return handle.GetCodeLength(); } + IReadOnlyList IGCInfo.GetInterruptibleRanges(IGCInfoHandle gcInfoHandle) + { + IGCInfoDecoder handle = AssertCorrectHandle(gcInfoHandle); + return handle.GetInterruptibleRanges(); + } + private static IGCInfoDecoder AssertCorrectHandle(IGCInfoHandle gcInfoHandle) { if (gcInfoHandle is not IGCInfoDecoder handle) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs index 86f4210a7cb91d..e21bc79661062f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; @@ -24,6 +26,11 @@ internal interface IGCInfoDecoder : IGCInfoHandle uint GetCodeLength(); uint StackBaseRegister { get; } + /// + /// Gets the interruptible code ranges decoded from the GC info. + /// + IReadOnlyList GetInterruptibleRanges(); + /// /// Enumerates all live GC slots at the given instruction offset. /// diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs index 27e971fc0cd615..72b46fc4813ade 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs @@ -155,6 +155,7 @@ internal enum DynamicMethodDescExtendedFlags : uint internal enum AsyncMethodFlags : uint { None = 0, + AsyncCall = 0x1, Thunk = 16, } @@ -1251,6 +1252,99 @@ public ReadOnlySpan GetGenericMethodInstantiation(MethodDescHandle m return AsInstantiatedMethodDesc(methodDesc).Instantiation; } + /// + /// Returns true if the method requires a hidden instantiation argument (generic context parameter). + /// Matches native MethodDesc::RequiresInstArg(). + /// + public bool RequiresInstArg(MethodDescHandle methodDescHandle) + { + MethodDesc methodDesc = _methodDescs[methodDescHandle.Address]; + + // RequiresInstArg = IsSharedByGenericInstantiations && (HasMethodInstantiation || IsStatic || IsValueType || IsInterface) + if (!IsSharedByGenericInstantiations(methodDesc)) + return false; + + if (HasMethodInstantiation(methodDesc)) + return true; + + MethodTable mt = _methodTables[methodDesc.MethodTable]; + if (mt.Flags.IsInterface) + return true; + + if (mt.Flags.IsValueType) + return true; + + if (IsStaticMethod(methodDesc)) + return true; + + return false; + } + + /// + /// Matches native MethodDesc::IsStatic(). + /// + private bool IsStaticMethod(MethodDesc methodDesc) + { + try + { + uint token = methodDesc.Token; + if (token != 0x06000000) + { + TypeHandle typeHandle = GetTypeHandle(methodDesc.MethodTable); + TargetPointer modulePtr = GetModule(typeHandle); + ILoader loader = _target.Contracts.Loader; + ModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(modulePtr); + MetadataReader? mdReader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); + if (mdReader is not null) + { + MethodDefinitionHandle methodDefHandle = + MetadataTokens.MethodDefinitionHandle((int)(token & 0x00FFFFFF)); + MethodDefinition methodDef = mdReader.GetMethodDefinition(methodDefHandle); + return (methodDef.Attributes & MethodAttributes.Static) != 0; + } + } + } + catch + { + } + + return false; + } + + private bool IsSharedByGenericInstantiations(MethodDesc methodDesc) + { + // Check method-level sharing: InstantiatedMethodDesc with SharedMethodInstantiation + if (methodDesc.Classification == MethodClassification.Instantiated) + { + InstantiatedMethodDesc imd = AsInstantiatedMethodDesc(methodDesc); + if (imd.IsWrapperStubWithInstantiations) + return false; + + // Check SharedMethodInstantiation flag + Data.InstantiatedMethodDesc imdData = _target.ProcessedData.GetOrAdd(methodDesc.Address); + if ((imdData.Flags2 & (ushort)InstantiatedMethodDescFlags2.KindMask) + == (ushort)InstantiatedMethodDescFlags2.SharedMethodInstantiation) + return true; + } + + // Check class-level sharing: canonical MethodTable with generic instantiation + MethodTable mt = _methodTables[methodDesc.MethodTable]; + return mt.IsCanonMT && mt.Flags.HasInstantiation; + } + + public bool IsAsyncMethod(MethodDescHandle methodDescHandle) + { + MethodDesc methodDesc = _methodDescs[methodDescHandle.Address]; + if (!methodDesc.HasAsyncMethodData) + return false; + + // AsyncMethodData is the last optional slot, placed after NativeCodeSlot. + // Read the AsyncMethodFlags (first field) and check for AsyncCall. + TargetPointer asyncDataAddr = methodDesc.GetAddressOfAsyncMethodData(); + uint asyncFlags = _target.Read(asyncDataAddr); + return (asyncFlags & (uint)AsyncMethodFlags.AsyncCall) != 0; + } + public uint GetMethodToken(MethodDescHandle methodDescHandle) { MethodDesc methodDesc = _methodDescs[methodDescHandle.Address]; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIterator.cs new file mode 100644 index 00000000000000..cbb42062124f0a --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIterator.cs @@ -0,0 +1,797 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Provides an abstraction over platform specific calling conventions. +// Ported from crossgen2's ArgIterator.cs. + +using System; +using System.Diagnostics; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.CallingConvention; + +/// +/// Enumerates method arguments and maps each to a register or stack offset +/// within a TransitionBlock. +/// Ported from crossgen2's ArgIterator. +/// +internal struct ArgIterator +{ + private readonly CallingConventionInfo _ccInfo; + private readonly ArgIteratorData _argData; + private readonly bool _hasThis; + private readonly bool _hasParamType; + private readonly bool _hasAsyncContinuation; + private readonly bool _extraFunctionPointerArg; + private readonly bool[] _forcedByRefParams; + private readonly bool _skipFirstArg; + private readonly bool _extraObjectFirstArg; + + // Iteration state + private bool _ITERATION_STARTED; + private bool _SIZE_OF_ARG_STACK_COMPUTED; + private bool _RETURN_FLAGS_COMPUTED; + private bool _RETURN_HAS_RET_BUFFER; + + private CorElementType _argType; + private ArgTypeInfo _argTypeHandle; + private ArgTypeInfo _argTypeHandleOfByRefParam; + private int _argSize; + private int _argNum; + private bool _argForceByRef; + private int _nSizeOfArgStack; + + // Per-architecture register allocation state + // x86 + private int _x86NumRegistersUsed; + private int _x86OfsStack; + + // x64 Windows + private int _x64WindowsCurOfs; + + // x64 Unix + private int _x64UnixIdxGenReg; + private int _x64UnixIdxStack; + private int _x64UnixIdxFPReg; + + // ARM32 + private int _armIdxGenReg; + private int _armOfsStack; + private ushort _armWFPRegs; + private bool _armRequires64BitAlignment; + + // ARM64 + private int _arm64IdxGenReg; + private int _arm64OfsStack; + private int _arm64IdxFPReg; + + // LoongArch64 / RISC-V64 + private int _rvLa64IdxGenReg; + private int _rvLa64OfsStack; + private int _rvLa64IdxFPReg; + + // Struct-in-registers tracking + private bool _hasArgLocDescForStructInRegs; +#pragma warning disable CS0649 // Assigned in platform-specific paths (ARM64 HFA, Unix AMD64 struct-in-regs) + private ArgLocDesc _argLocDescForStructInRegs; +#pragma warning restore CS0649 + + // x86 param type location + private enum ParamTypeLocation + { + Stack, + Ecx, + Edx, + } + private ParamTypeLocation _paramTypeLoc; + + private enum AsyncContinuationLocation + { + Stack, + Ecx, + Edx, + } + private AsyncContinuationLocation _asyncContinuationLoc; + + public bool HasThis => _hasThis; + public bool IsVarArg => _argData.IsVarArg(); + public bool HasParamType => _hasParamType; + public bool HasAsyncContinuation => _hasAsyncContinuation; + public int NumFixedArgs => _argData.NumFixedArgs() + (_extraFunctionPointerArg ? 1 : 0) + (_extraObjectFirstArg ? 1 : 0); + + public ArgIterator( + CallingConventionInfo ccInfo, + ArgIteratorData argData, + bool hasParamType, + bool hasAsyncContinuation, + bool[] forcedByRefParams, + bool skipFirstArg = false, + bool extraObjectFirstArg = false, + bool extraFunctionPointerArg = false) + { + this = default; + _ccInfo = ccInfo; + _argData = argData; + _hasThis = argData.HasThis(); + _hasParamType = hasParamType; + _hasAsyncContinuation = hasAsyncContinuation; + _extraFunctionPointerArg = extraFunctionPointerArg; + _forcedByRefParams = forcedByRefParams; + _skipFirstArg = skipFirstArg; + _extraObjectFirstArg = extraObjectFirstArg; + } + + public CorElementType GetArgumentType(int argNum, out ArgTypeInfo thArgType, out bool forceByRefReturn) + { + forceByRefReturn = false; + + if (_extraObjectFirstArg && argNum == 0) + { + thArgType = ArgTypeInfo.ForPrimitive(CorElementType.Class, _ccInfo.PointerSize); + return CorElementType.Class; + } + + argNum = _extraObjectFirstArg ? argNum - 1 : argNum; + + if (_forcedByRefParams is not null && (argNum + 1) < _forcedByRefParams.Length) + forceByRefReturn = _forcedByRefParams[argNum + 1]; + + if (_extraFunctionPointerArg && argNum == _argData.NumFixedArgs()) + { + thArgType = ArgTypeInfo.ForPrimitive(CorElementType.I, _ccInfo.PointerSize); + return CorElementType.I; + } + + return _argData.GetArgumentType(argNum, out thArgType); + } + + public CorElementType GetReturnType(out ArgTypeInfo thRetType, out bool forceByRefReturn) + { + forceByRefReturn = _forcedByRefParams is not null && _forcedByRefParams.Length > 0 && _forcedByRefParams[0]; + return _argData.GetReturnType(out thRetType); + } + + public void Reset() + { + _argType = default; + _argTypeHandle = default; + _argSize = 0; + _argNum = 0; + _argForceByRef = false; + _ITERATION_STARTED = false; + } + + private uint SizeOfArgStack() + { + if (!_SIZE_OF_ARG_STACK_COMPUTED) + ForceSigWalk(); + Debug.Assert(_SIZE_OF_ARG_STACK_COMPUTED); + return (uint)_nSizeOfArgStack; + } + + public int SizeOfFrameArgumentArray() + { + uint size = SizeOfArgStack(); + + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X64 && !_ccInfo.IsX64UnixABI) + { + size += (uint)_ccInfo.SizeOfArgumentRegisters; + } + + return (int)size; + } + + public uint CbStackPop() + { + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X86) + { + return IsVarArg ? 0 : SizeOfArgStack(); + } + throw new NotImplementedException(); + } + + public bool HasRetBuffArg() + { + if (!_RETURN_FLAGS_COMPUTED) + ComputeReturnFlags(); + return _RETURN_HAS_RET_BUFFER; + } + + public int GetThisOffset() => _ccInfo.ThisOffset; + + public int GetVASigCookieOffset() + { + Debug.Assert(IsVarArg); + + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X86) + { + return (int)_ccInfo.SizeOfTransitionBlock; + } + + int ret = (int)_ccInfo.ArgumentRegistersOffset; + if (HasThis) + ret += _ccInfo.PointerSize; + if (HasRetBuffArg() && _ccInfo.IsRetBuffPassedAsFirstArg) + ret += _ccInfo.PointerSize; + return ret; + } + + public int GetParamTypeArgOffset() + { + Debug.Assert(HasParamType); + + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X86) + { + if (!_SIZE_OF_ARG_STACK_COMPUTED) + ForceSigWalk(); + + return _paramTypeLoc switch + { + ParamTypeLocation.Ecx => (int)_ccInfo.ArgumentRegistersOffset + _ccInfo.PointerSize, // ECX offset + ParamTypeLocation.Edx => (int)_ccInfo.ArgumentRegistersOffset, // EDX offset + _ => (int)_ccInfo.SizeOfTransitionBlock, + }; + } + + int ret = (int)_ccInfo.ArgumentRegistersOffset; + if (HasThis) ret += _ccInfo.PointerSize; + if (HasRetBuffArg() && _ccInfo.IsRetBuffPassedAsFirstArg) ret += _ccInfo.PointerSize; + return ret; + } + + public int GetAsyncContinuationArgOffset() + { + Debug.Assert(HasAsyncContinuation); + + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X86) + { + if (!_SIZE_OF_ARG_STACK_COMPUTED) + ForceSigWalk(); + + return _asyncContinuationLoc switch + { + AsyncContinuationLocation.Ecx => (int)_ccInfo.ArgumentRegistersOffset + _ccInfo.PointerSize, + AsyncContinuationLocation.Edx => (int)_ccInfo.ArgumentRegistersOffset, + _ => HasParamType && _paramTypeLoc == ParamTypeLocation.Stack + ? (int)_ccInfo.SizeOfTransitionBlock + _ccInfo.PointerSize + : (int)_ccInfo.SizeOfTransitionBlock, + }; + } + + int ret = (int)_ccInfo.ArgumentRegistersOffset; + if (HasThis) ret += _ccInfo.PointerSize; + if (HasRetBuffArg() && _ccInfo.IsRetBuffPassedAsFirstArg) ret += _ccInfo.PointerSize; + if (HasParamType) ret += _ccInfo.PointerSize; + return ret; + } + + public bool IsArgPassedByRef() + { + if (_argForceByRef) + return true; + if (_argType == CorElementType.Byref) + return true; + + if (_ccInfo.EnregisteredParamTypeMaxSize != 0) + { + return _ccInfo.Architecture switch + { + RuntimeInfoArchitecture.X64 => _ccInfo.IsArgPassedByRef(_argSize), + RuntimeInfoArchitecture.Arm64 => _argType == CorElementType.ValueType + && (_argSize > _ccInfo.EnregisteredParamTypeMaxSize) && (!_argTypeHandle.IsHomogeneousAggregate || IsVarArg), + RuntimeInfoArchitecture.LoongArch64 or RuntimeInfoArchitecture.RiscV64 => _argType == CorElementType.ValueType + && _argSize > _ccInfo.EnregisteredParamTypeMaxSize, + _ => false, + }; + } + return false; + } + + public ArgLocDesc? GetArgLoc(int _) + { + return _hasArgLocDescForStructInRegs ? _argLocDescForStructInRegs : null; + } + + public int GetArgSize() => _argSize; + + /// + /// Returns the next argument's offset in the transition block, or + /// when all arguments + /// have been enumerated. + /// + public int GetNextOffset() + { + if (!_ITERATION_STARTED) + { + int numRegistersUsed = 0; + + if (HasThis) numRegistersUsed++; + if (HasRetBuffArg() && _ccInfo.IsRetBuffPassedAsFirstArg) numRegistersUsed++; + + Debug.Assert(!IsVarArg || !HasParamType); + + if (_ccInfo.Architecture != RuntimeInfoArchitecture.X86) + { + if (HasParamType) numRegistersUsed++; + if (HasAsyncContinuation) numRegistersUsed++; + } + + if (_ccInfo.Architecture != RuntimeInfoArchitecture.X86 && IsVarArg) + numRegistersUsed++; + + switch (_ccInfo.Architecture) + { + case RuntimeInfoArchitecture.X86: + if (IsVarArg) numRegistersUsed = _ccInfo.NumArgumentRegisters; + _x86NumRegistersUsed = numRegistersUsed; + _x86OfsStack = (int)(_ccInfo.OffsetOfArgs + SizeOfArgStack()); + break; + + case RuntimeInfoArchitecture.X64: + if (_ccInfo.IsX64UnixABI) + { + _x64UnixIdxGenReg = numRegistersUsed; + _x64UnixIdxStack = 0; + _x64UnixIdxFPReg = 0; + } + else + { + _x64WindowsCurOfs = (int)_ccInfo.OffsetOfArgs + numRegistersUsed * _ccInfo.PointerSize; + } + break; + + case RuntimeInfoArchitecture.Arm: + _armIdxGenReg = numRegistersUsed; + _armOfsStack = 0; + _armWFPRegs = 0; + break; + + case RuntimeInfoArchitecture.Arm64: + _arm64IdxGenReg = numRegistersUsed; + _arm64OfsStack = 0; + _arm64IdxFPReg = 0; + break; + + case RuntimeInfoArchitecture.LoongArch64: + case RuntimeInfoArchitecture.RiscV64: + _rvLa64IdxGenReg = numRegistersUsed; + _rvLa64OfsStack = 0; + _rvLa64IdxFPReg = 0; + break; + + default: + throw new NotSupportedException(_ccInfo.Architecture.ToString()); + } + + _argNum = _skipFirstArg ? 1 : 0; + _ITERATION_STARTED = true; + } + + if (_argNum >= NumFixedArgs) + return CallingConventionInfo.InvalidOffset; + + CorElementType argType = GetArgumentType(_argNum, out _argTypeHandle, out _argForceByRef); + _argTypeHandleOfByRefParam = argType == CorElementType.Byref ? _argData.GetByRefArgumentType(_argNum) : default; + _argNum++; + + int argSize = ArgTypeInfo.GetElemSize(argType, _argTypeHandle, _ccInfo.PointerSize); + _argType = argType; + _argSize = argSize; + + argType = _argForceByRef ? CorElementType.Byref : argType; + argSize = _argForceByRef ? _ccInfo.PointerSize : argSize; + + _hasArgLocDescForStructInRegs = false; + + switch (_ccInfo.Architecture) + { + case RuntimeInfoArchitecture.X64: + return GetNextOffsetX64(argType, argSize); + + case RuntimeInfoArchitecture.Arm64: + return GetNextOffsetArm64(argType, argSize); + + case RuntimeInfoArchitecture.X86: + return GetNextOffsetX86(argType, argSize); + + case RuntimeInfoArchitecture.Arm: + return GetNextOffsetArm32(argType, argSize); + + case RuntimeInfoArchitecture.LoongArch64: + case RuntimeInfoArchitecture.RiscV64: + return GetNextOffsetRiscVLoongArch(argType, argSize); + + default: + throw new NotSupportedException(_ccInfo.Architecture.ToString()); + } + } + + // ---- Per-architecture GetNextOffset implementations ---- + // These match crossgen2's ArgIterator.GetNextOffset() switch cases. + + private int GetNextOffsetX64(CorElementType argType, int argSize) + { + if (_ccInfo.IsX64UnixABI) + { + // TODO: Full Unix AMD64 implementation with SystemV struct classification + // For now, simplified: all args go through GP regs then stack + int cbArg = _ccInfo.StackElemSize(argSize); + int cGenRegs = cbArg / 8; + + if (argType is CorElementType.R4 or CorElementType.R8) + { + if (_x64UnixIdxFPReg < _ccInfo.NumFloatArgumentRegisters) + { + int argOfs = _ccInfo.OffsetOfFloatArgumentRegisters + _x64UnixIdxFPReg * _ccInfo.FloatRegisterSize; + _x64UnixIdxFPReg++; + return argOfs; + } + } + else if (cGenRegs > 0 && _x64UnixIdxGenReg + cGenRegs <= _ccInfo.NumArgumentRegisters) + { + int argOfs = (int)_ccInfo.ArgumentRegistersOffset + _x64UnixIdxGenReg * _ccInfo.PointerSize; + _x64UnixIdxGenReg += cGenRegs; + return argOfs; + } + + int stackOfs = (int)_ccInfo.OffsetOfArgs + _x64UnixIdxStack * _ccInfo.PointerSize; + _x64UnixIdxStack += _ccInfo.StackElemSize(argSize) / _ccInfo.PointerSize; + return stackOfs; + } + else + { + // Windows x64: each arg takes exactly one slot + int cFPRegs = argType is CorElementType.R4 or CorElementType.R8 ? 1 : 0; + + int argOfs = _x64WindowsCurOfs - (int)_ccInfo.OffsetOfArgs; + _x64WindowsCurOfs += _ccInfo.PointerSize; + + if (cFPRegs == 0 || argOfs >= _ccInfo.SizeOfArgumentRegisters) + { + return argOfs + (int)_ccInfo.OffsetOfArgs; + } + else + { + int idxFpReg = argOfs / _ccInfo.PointerSize; + return _ccInfo.OffsetOfFloatArgumentRegisters + idxFpReg * 16; // SizeOfM128A + } + } + } + + private int GetNextOffsetArm64(CorElementType argType, int argSize) + { + int cFPRegs = 0; + bool isFloatHFA = false; + + switch (argType) + { + case CorElementType.R4: + case CorElementType.R8: + cFPRegs = 1; + break; + + case CorElementType.ValueType: + if (_argTypeHandle.IsHomogeneousAggregate) + { + int haElementSize = _argTypeHandle.HomogeneousAggregateElementSize; + if (haElementSize == 4) isFloatHFA = true; + cFPRegs = argSize / haElementSize; + } + else if (argSize > _ccInfo.EnregisteredParamTypeMaxSize) + { + argSize = _ccInfo.PointerSize; + } + break; + } + + bool isValueType = argType == CorElementType.ValueType; + int cbArg = _ccInfo.StackElemSize(argSize, isValueType, isFloatHFA); + + if (cFPRegs > 0 && !IsVarArg) + { + if (cFPRegs + _arm64IdxFPReg <= 8) + { + int argOfs = _ccInfo.OffsetOfFloatArgumentRegisters + _arm64IdxFPReg * 16; + _arm64IdxFPReg += cFPRegs; + return argOfs; + } + else + { + _arm64IdxFPReg = 8; + } + } + else + { + int regSlots = CallingConventionInfo.AlignUp(cbArg, _ccInfo.PointerSize) / _ccInfo.PointerSize; + if (_arm64IdxGenReg + regSlots <= 8) + { + int argOfs = (int)_ccInfo.ArgumentRegistersOffset + _arm64IdxGenReg * 8; + _arm64IdxGenReg += regSlots; + return argOfs; + } + else + { + _arm64IdxGenReg = 8; + } + } + + if (_ccInfo.IsAppleArm64ABI) + { + int alignment = isValueType ? (isFloatHFA ? 4 : 8) : cbArg; + _arm64OfsStack = CallingConventionInfo.AlignUp(_arm64OfsStack, alignment); + } + + int result = (int)_ccInfo.OffsetOfArgs + _arm64OfsStack; + _arm64OfsStack += cbArg; + return result; + } + + private int GetNextOffsetX86(CorElementType argType, int argSize) + { + if (_x86NumRegistersUsed < _ccInfo.NumArgumentRegisters + && argType is not CorElementType.ValueType + and not CorElementType.R4 + and not CorElementType.R8 + and not CorElementType.I8 + and not CorElementType.U8) + { + _x86NumRegistersUsed++; + return (int)_ccInfo.ArgumentRegistersOffset + + (_ccInfo.NumArgumentRegisters - _x86NumRegistersUsed) * _ccInfo.PointerSize; + } + + int cbArg = _ccInfo.StackElemSize(argSize); + _x86OfsStack -= cbArg; + return _x86OfsStack; + } + + private int GetNextOffsetArm32(CorElementType argType, int argSize) + { + bool fFloatingPoint = false; + bool fRequiresAlign64Bit = false; + + switch (argType) + { + case CorElementType.I8: + case CorElementType.U8: + fRequiresAlign64Bit = true; + break; + case CorElementType.R4: + fFloatingPoint = true; + break; + case CorElementType.R8: + fFloatingPoint = true; + fRequiresAlign64Bit = true; + break; + case CorElementType.ValueType: + fRequiresAlign64Bit = _argTypeHandle.RequiresAlign8; + if (_argTypeHandle.IsHomogeneousAggregate) fFloatingPoint = true; + break; + } + + _armRequires64BitAlignment = fRequiresAlign64Bit; + int cbArg = _ccInfo.StackElemSize(argSize); + + if (fFloatingPoint && _ccInfo.IsArmhfABI && !IsVarArg) + { + ushort wAllocMask = checked((ushort)((1 << (cbArg / 4)) - 1)); + ushort cSteps = (ushort)(fRequiresAlign64Bit ? 9 - (cbArg / 8) : 17 - (cbArg / 4)); + ushort cShift = fRequiresAlign64Bit ? (ushort)2 : (ushort)1; + + for (ushort i = 0; i < cSteps; i++) + { + if ((_armWFPRegs & wAllocMask) == 0) + { + _armWFPRegs |= wAllocMask; + return _ccInfo.OffsetOfFloatArgumentRegisters + (i * cShift * 4); + } + wAllocMask <<= cShift; + } + + _armWFPRegs = 0xffff; + + if (fRequiresAlign64Bit) + _armOfsStack = CallingConventionInfo.AlignUp(_armOfsStack, _ccInfo.PointerSize * 2); + + int argOfs = (int)_ccInfo.OffsetOfArgs + _armOfsStack; + _armOfsStack += cbArg; + return argOfs; + } + + if (_armIdxGenReg < 4) + { + if (fRequiresAlign64Bit) + _armIdxGenReg = CallingConventionInfo.AlignUp(_armIdxGenReg, 2); + + int argOfs = (int)_ccInfo.ArgumentRegistersOffset + _armIdxGenReg * 4; + int cRemainingRegs = 4 - _armIdxGenReg; + + if (cbArg <= cRemainingRegs * _ccInfo.PointerSize) + { + _armIdxGenReg += CallingConventionInfo.AlignUp(cbArg, _ccInfo.PointerSize) / _ccInfo.PointerSize; + return argOfs; + } + + _armIdxGenReg = 4; + + if (_armOfsStack == 0) + { + _armOfsStack += cbArg - cRemainingRegs * _ccInfo.PointerSize; + return argOfs; + } + } + + if (fRequiresAlign64Bit) + _armOfsStack = CallingConventionInfo.AlignUp(_armOfsStack, _ccInfo.PointerSize * 2); + + int result = (int)_ccInfo.OffsetOfArgs + _armOfsStack; + _armOfsStack += cbArg; + return result; + } + + private int GetNextOffsetRiscVLoongArch(CorElementType argType, int argSize) + { + // Simplified: no FP struct detection, just use integer calling convention + int cFPRegs = argType is CorElementType.R4 or CorElementType.R8 ? 1 : 0; + + if (argType == CorElementType.ValueType && argSize > _ccInfo.EnregisteredParamTypeMaxSize) + argSize = _ccInfo.PointerSize; + + int cbArg = _ccInfo.StackElemSize(argSize); + + if (cFPRegs > 0 && !IsVarArg && cFPRegs + _rvLa64IdxFPReg <= _ccInfo.NumFloatArgumentRegisters) + { + int argOfs = _ccInfo.OffsetOfFloatArgumentRegisters + _rvLa64IdxFPReg * _ccInfo.FloatRegisterSize; + _rvLa64IdxFPReg += cFPRegs; + return argOfs; + } + + int regSlots = CallingConventionInfo.AlignUp(cbArg, _ccInfo.PointerSize) / _ccInfo.PointerSize; + if (_rvLa64IdxGenReg + regSlots <= _ccInfo.NumArgumentRegisters) + { + int argOfs = (int)_ccInfo.ArgumentRegistersOffset + _rvLa64IdxGenReg * _ccInfo.PointerSize; + _rvLa64IdxGenReg += regSlots; + return argOfs; + } + else + { + _rvLa64IdxGenReg = _ccInfo.NumArgumentRegisters; + } + + int result = (int)_ccInfo.OffsetOfArgs + _rvLa64OfsStack; + _rvLa64OfsStack += cbArg; + return result; + } + + // ---- Return type computation ---- + + private void ComputeReturnFlags() + { + _RETURN_FLAGS_COMPUTED = true; + CorElementType retType = GetReturnType(out ArgTypeInfo thRetType, out bool forceByRef); + + if (forceByRef) + { + _RETURN_HAS_RET_BUFFER = true; + return; + } + + switch (retType) + { + case CorElementType.TypedByRef: + _RETURN_HAS_RET_BUFFER = true; + break; + + case CorElementType.ValueType: + if (thRetType.Size > _ccInfo.EnregisteredParamTypeMaxSize && _ccInfo.EnregisteredParamTypeMaxSize > 0) + { + _RETURN_HAS_RET_BUFFER = true; + } + else if (_ccInfo.Architecture is RuntimeInfoArchitecture.X86 or RuntimeInfoArchitecture.X64) + { + int size = thRetType.Size; + if ((size & (size - 1)) != 0) // not power of 2 + _RETURN_HAS_RET_BUFFER = true; + } + break; + } + } + + private void ForceSigWalk() + { + Debug.Assert(!_ITERATION_STARTED); + + int numRegistersUsed = 0; + int nSizeOfArgStack = 0; + + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X86) + { + if (HasThis) numRegistersUsed++; + if (HasRetBuffArg() && _ccInfo.IsRetBuffPassedAsFirstArg) numRegistersUsed++; + if (IsVarArg) + { + nSizeOfArgStack += _ccInfo.PointerSize; + numRegistersUsed = _ccInfo.NumArgumentRegisters; + } + + int nArgs = NumFixedArgs; + for (int i = _skipFirstArg ? 1 : 0; i < nArgs; i++) + { + CorElementType type = GetArgumentType(i, out ArgTypeInfo thArgType, out bool argForced); + if (argForced) type = CorElementType.Byref; + + // Simplified: assume all non-trivial types go to stack + int structSize = ArgTypeInfo.GetElemSize(type, thArgType, _ccInfo.PointerSize); + nSizeOfArgStack += _ccInfo.StackElemSize(structSize); + } + + if (HasAsyncContinuation) + { + if (numRegistersUsed < _ccInfo.NumArgumentRegisters) + { + numRegistersUsed++; + _asyncContinuationLoc = numRegistersUsed == 1 ? AsyncContinuationLocation.Ecx : AsyncContinuationLocation.Edx; + } + else + { + nSizeOfArgStack += _ccInfo.PointerSize; + _asyncContinuationLoc = AsyncContinuationLocation.Stack; + } + } + + if (HasParamType) + { + if (numRegistersUsed < _ccInfo.NumArgumentRegisters) + { + numRegistersUsed++; + _paramTypeLoc = numRegistersUsed == 1 ? ParamTypeLocation.Ecx : ParamTypeLocation.Edx; + } + else + { + nSizeOfArgStack += _ccInfo.PointerSize; + _paramTypeLoc = ParamTypeLocation.Stack; + } + } + } + else + { + // Non-x86: iterate through GetNextOffset to compute stack size + int maxOffset = (int)_ccInfo.OffsetOfArgs; + int ofs; + while (CallingConventionInfo.InvalidOffset != (ofs = GetNextOffset())) + { + int stackElemSize; + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X64) + { + stackElemSize = _ccInfo.IsX64UnixABI + ? _ccInfo.StackElemSize(GetArgSize()) + : _ccInfo.PointerSize; + } + else + { + stackElemSize = _ccInfo.StackElemSize(GetArgSize()); + } + + int endOfs = ofs + stackElemSize; + if (IsArgumentRegisterOffset(ofs)) + continue; + if (CallingConventionInfo.IsFloatArgumentRegisterOffset(ofs)) + continue; + if (ofs == CallingConventionInfo.StructInRegsOffset) + continue; + if (endOfs > maxOffset) + maxOffset = endOfs; + } + + nSizeOfArgStack = maxOffset - (int)_ccInfo.OffsetOfArgs; + Reset(); + } + + _nSizeOfArgStack = nSizeOfArgStack; + _SIZE_OF_ARG_STACK_COMPUTED = true; + } + + private bool IsArgumentRegisterOffset(int offset) + { + return _ccInfo.IsArgumentRegisterOffset(offset); + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIteratorData.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIteratorData.cs new file mode 100644 index 00000000000000..b48a86f5b2e0ee --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIteratorData.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Ported from crossgen2's ArgIterator.cs — data holder types. + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.CallingConvention; + +/// +/// Describes how a single argument is laid out in registers and/or stack locations. +/// Ported from crossgen2's ArgLocDesc. +/// +internal struct ArgLocDesc +{ + public int m_idxFloatReg; + public int m_cFloatReg; + + public int m_idxGenReg; + public short m_cGenReg; + + public bool m_fRequires64BitAlignment; + + public int m_byteStackIndex; + public int m_byteStackSize; + + public void Init() + { + m_idxFloatReg = -1; + m_cFloatReg = 0; + m_idxGenReg = -1; + m_cGenReg = 0; + m_byteStackIndex = -1; + m_byteStackSize = 0; + m_fRequires64BitAlignment = false; + } +} + +/// +/// Holds parsed method signature data for . +/// Ported from crossgen2's ArgIteratorData. +/// +internal sealed class ArgIteratorData +{ + private readonly bool _hasThis; + private readonly bool _isVarArg; + private readonly ArgTypeInfo[] _parameterTypes; + private readonly ArgTypeInfo _returnType; + + public ArgIteratorData( + bool hasThis, + bool isVarArg, + ArgTypeInfo[] parameterTypes, + ArgTypeInfo returnType) + { + _hasThis = hasThis; + _isVarArg = isVarArg; + _parameterTypes = parameterTypes; + _returnType = returnType; + } + + public bool HasThis() => _hasThis; + public bool IsVarArg() => _isVarArg; + public int NumFixedArgs() => _parameterTypes.Length; + + public CorElementType GetArgumentType(int argNum, out ArgTypeInfo thArgType) + { + thArgType = _parameterTypes[argNum]; + return thArgType.CorElementType; + } + + public ArgTypeInfo GetByRefArgumentType(int argNum) + { + if (argNum < _parameterTypes.Length && _parameterTypes[argNum].CorElementType == CorElementType.Byref) + return _parameterTypes[argNum]; + return default; + } + + public CorElementType GetReturnType(out ArgTypeInfo thRetType) + { + thRetType = _returnType; + return thRetType.CorElementType; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgTypeInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgTypeInfo.cs new file mode 100644 index 00000000000000..bd3ca6ecf632d5 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgTypeInfo.cs @@ -0,0 +1,142 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Type information needed by ArgIterator for calling convention analysis. +// Ported from crossgen2's TypeHandle struct in ArgIterator.cs. + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.CallingConvention; + +/// +/// Pre-computed type information needed by for +/// calling convention analysis. This is a value type to avoid allocations +/// during argument iteration. +/// +/// +/// Mirrors crossgen2's TypeHandle struct in ArgIterator.cs, but uses +/// data from the cDAC's rather than +/// crossgen2's TypeDesc. +/// +internal readonly struct ArgTypeInfo +{ + public CorElementType CorElementType { get; init; } + public int Size { get; init; } + public bool IsValueType { get; init; } + public bool RequiresAlign8 { get; init; } + public bool IsHomogeneousAggregate { get; init; } + public int HomogeneousAggregateElementSize { get; init; } + + /// + /// The TypeHandle from the target runtime, used for value type field enumeration + /// and SystemV struct classification. + /// + public TypeHandle RuntimeTypeHandle { get; init; } + + public bool IsNull => CorElementType == default && Size == 0; + + /// + /// Gets the element size for a given CorElementType, matching crossgen2's + /// TypeHandle.GetElemSize. Returns the type's actual size for value + /// types, or pointer size for reference types. + /// + public static int GetElemSize(CorElementType t, ArgTypeInfo thValueType, int pointerSize) + { + if ((int)t <= 0x1d) + { + int elemSize = s_elemSizes[(int)t]; + if (elemSize == -1) + return thValueType.Size; + if (elemSize == -2) + return pointerSize; + return elemSize; + } + return 0; + } + + private static readonly int[] s_elemSizes = + [ + 0, // ELEMENT_TYPE_END 0x0 + 0, // ELEMENT_TYPE_VOID 0x1 + 1, // ELEMENT_TYPE_BOOLEAN 0x2 + 2, // ELEMENT_TYPE_CHAR 0x3 + 1, // ELEMENT_TYPE_I1 0x4 + 1, // ELEMENT_TYPE_U1 0x5 + 2, // ELEMENT_TYPE_I2 0x6 + 2, // ELEMENT_TYPE_U2 0x7 + 4, // ELEMENT_TYPE_I4 0x8 + 4, // ELEMENT_TYPE_U4 0x9 + 8, // ELEMENT_TYPE_I8 0xa + 8, // ELEMENT_TYPE_U8 0xb + 4, // ELEMENT_TYPE_R4 0xc + 8, // ELEMENT_TYPE_R8 0xd + -2, // ELEMENT_TYPE_STRING 0xe + -2, // ELEMENT_TYPE_PTR 0xf + -2, // ELEMENT_TYPE_BYREF 0x10 + -1, // ELEMENT_TYPE_VALUETYPE 0x11 + -2, // ELEMENT_TYPE_CLASS 0x12 + 0, // ELEMENT_TYPE_VAR 0x13 + -2, // ELEMENT_TYPE_ARRAY 0x14 + 0, // ELEMENT_TYPE_GENERICINST 0x15 + 0, // ELEMENT_TYPE_TYPEDBYREF 0x16 + 0, // UNUSED 0x17 + -2, // ELEMENT_TYPE_I 0x18 + -2, // ELEMENT_TYPE_U 0x19 + 0, // UNUSED 0x1a + -2, // ELEMENT_TYPE_FPTR 0x1b + -2, // ELEMENT_TYPE_OBJECT 0x1c + -2, // ELEMENT_TYPE_SZARRAY 0x1d + ]; + + /// + /// Creates an from a target TypeHandle using the + /// runtime type system contract. + /// + public static ArgTypeInfo FromTypeHandle(Target target, TypeHandle th) + { + IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; + CorElementType corType = rts.GetSignatureCorElementType(th); + + bool isValueType = corType is CorElementType.ValueType; + int size = isValueType + ? (int)rts.GetBaseSize(th) - 2 * target.PointerSize // InstanceFieldSize = BaseSize - ObjHeader - MethodTable ptr + : target.PointerSize; + + bool requiresAlign8 = false; + bool isHfa = false; + int hfaElemSize = 0; + + if (isValueType) + { + // TODO: Implement RequiresAlign8 via IRuntimeTypeSystem + // TODO: Implement IsHomogeneousAggregate via IRuntimeTypeSystem + } + + return new ArgTypeInfo + { + CorElementType = corType, + Size = size, + IsValueType = isValueType, + RequiresAlign8 = requiresAlign8, + IsHomogeneousAggregate = isHfa, + HomogeneousAggregateElementSize = hfaElemSize, + RuntimeTypeHandle = th, + }; + } + + /// + /// Creates an for a primitive type that doesn't need + /// type handle resolution. + /// + public static ArgTypeInfo ForPrimitive(CorElementType corType, int pointerSize) + { + return new ArgTypeInfo + { + CorElementType = corType, + Size = GetElemSize(corType, default, pointerSize), + IsValueType = false, + RequiresAlign8 = false, + IsHomogeneousAggregate = false, + HomogeneousAggregateElementSize = 0, + RuntimeTypeHandle = default, + }; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/CallingConventionInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/CallingConventionInfo.cs new file mode 100644 index 00000000000000..ee89c064c24a27 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/CallingConventionInfo.cs @@ -0,0 +1,274 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Provides an abstraction over platform-specific calling conventions +// (specifically, the managed calling convention utilized by the JIT). +// Ported from crossgen2's TransitionBlock.cs. + +using System; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.CallingConvention; + +/// +/// Architecture-specific calling convention constants and methods for +/// mapping method arguments to register and stack locations. +/// +/// +/// Layout-dependent values (offsets, sizes) come from the data descriptor +/// (). ABI-invariant values (register counts, +/// alignment rules) are hardcoded per architecture since they are defined by +/// the hardware/OS ABI and never change. +/// +internal sealed class CallingConventionInfo +{ + // Layout values from the data descriptor + private readonly uint _sizeOfTransitionBlock; + private readonly uint _argumentRegistersOffset; + private readonly uint _firstGCRefMapSlot; + private readonly uint _offsetOfArgs; + private readonly int _offsetOfFloatArgumentRegisters; + + // ABI invariants + public int PointerSize { get; } + public int NumArgumentRegisters { get; } + public int NumFloatArgumentRegisters { get; } + public int FloatRegisterSize { get; } + public int EnregisteredParamTypeMaxSize { get; } + public int StackSlotSize { get; } + public bool IsRetBuffPassedAsFirstArg { get; } + public bool IsX64UnixABI { get; } + public bool IsAppleArm64ABI { get; } + public bool IsArmhfABI { get; } + + public RuntimeInfoArchitecture Architecture { get; } + + // Convenience accessors + public uint SizeOfTransitionBlock => _sizeOfTransitionBlock; + public uint ArgumentRegistersOffset => _argumentRegistersOffset; + public uint FirstGCRefMapSlot => _firstGCRefMapSlot; + public uint OffsetOfArgs => _offsetOfArgs; + public int OffsetOfFloatArgumentRegisters => _offsetOfFloatArgumentRegisters; + public int SizeOfArgumentRegisters => NumArgumentRegisters * PointerSize; + + public const int InvalidOffset = -1; + public const int StructInRegsOffset = -2; + + /// + /// Creates a for the given target, reading + /// layout data from the data descriptor and filling in ABI constants from + /// the target's architecture and OS. + /// + public CallingConventionInfo(Target target) + { + IRuntimeInfo runtimeInfo = target.Contracts.RuntimeInfo; + Architecture = runtimeInfo.GetTargetArchitecture(); + RuntimeInfoOperatingSystem os = runtimeInfo.GetTargetOperatingSystem(); + PointerSize = target.PointerSize; + + // Read layout values from the data descriptor + Target.TypeInfo tbType = target.GetTypeInfo(DataType.TransitionBlock); + _sizeOfTransitionBlock = (uint)tbType.Size!; + _argumentRegistersOffset = (uint)tbType.Fields["ArgumentRegistersOffset"].Offset; + _firstGCRefMapSlot = (uint)tbType.Fields["FirstGCRefMapSlot"].Offset; + _offsetOfArgs = (uint)tbType.Fields["OffsetOfArgs"].Offset; + _offsetOfFloatArgumentRegisters = tbType.Fields["OffsetOfFloatArgumentRegisters"].Offset; + + // Fill in ABI invariants based on architecture + switch (Architecture) + { + case RuntimeInfoArchitecture.X86: + NumArgumentRegisters = 2; // ECX, EDX + NumFloatArgumentRegisters = 0; + FloatRegisterSize = 0; + EnregisteredParamTypeMaxSize = 0; + StackSlotSize = 4; + IsRetBuffPassedAsFirstArg = true; + break; + + case RuntimeInfoArchitecture.X64: + if (os is RuntimeInfoOperatingSystem.Unix or RuntimeInfoOperatingSystem.Apple) + { + // Unix/Apple AMD64 ABI (SysV) + NumArgumentRegisters = 6; // RDI, RSI, RDX, RCX, R8, R9 + NumFloatArgumentRegisters = 8; // XMM0-XMM7 + FloatRegisterSize = 16; // M128A + EnregisteredParamTypeMaxSize = 16; + IsX64UnixABI = true; + } + else + { + // Windows AMD64 ABI + NumArgumentRegisters = 4; // RCX, RDX, R8, R9 + NumFloatArgumentRegisters = 0; // Shared with GP regs on Windows + FloatRegisterSize = 16; + EnregisteredParamTypeMaxSize = 8; + } + StackSlotSize = 8; + IsRetBuffPassedAsFirstArg = true; + break; + + case RuntimeInfoArchitecture.Arm: + NumArgumentRegisters = 4; // R0-R3 + NumFloatArgumentRegisters = 16; // 16 single-precision slots (D0-D7 / S0-S15) + FloatRegisterSize = 4; + EnregisteredParamTypeMaxSize = 0; + StackSlotSize = 4; + IsRetBuffPassedAsFirstArg = true; + IsArmhfABI = true; // TODO: detect armel + break; + + case RuntimeInfoArchitecture.Arm64: + NumArgumentRegisters = 8; // X0-X7 + NumFloatArgumentRegisters = 8; // V0-V7 + FloatRegisterSize = 16; + EnregisteredParamTypeMaxSize = 16; + StackSlotSize = 8; + IsRetBuffPassedAsFirstArg = false; // ARM64 uses X8 for retbuf + IsAppleArm64ABI = os == RuntimeInfoOperatingSystem.Apple; + break; + + case RuntimeInfoArchitecture.LoongArch64: + NumArgumentRegisters = 8; // A0-A7 + NumFloatArgumentRegisters = 8; // FA0-FA7 + FloatRegisterSize = 8; + EnregisteredParamTypeMaxSize = 16; + StackSlotSize = 8; + IsRetBuffPassedAsFirstArg = true; + break; + + case RuntimeInfoArchitecture.RiscV64: + NumArgumentRegisters = 8; // a0-a7 + NumFloatArgumentRegisters = 8; // fa0-fa7 + FloatRegisterSize = 8; + EnregisteredParamTypeMaxSize = 16; + StackSlotSize = 8; + IsRetBuffPassedAsFirstArg = true; + break; + + default: + throw new NotSupportedException($"Architecture {Architecture} is not supported for calling convention analysis."); + } + } + + // ---- Derived methods ---- + + /// + /// Returns the byte offset of the 'this' pointer in the transition block. + /// + public int ThisOffset + { + get + { + if (Architecture == RuntimeInfoArchitecture.X86) + { + // ECX offset within ArgumentRegisters: ECX is at offset PointerSize (after EDX at 0) + return (int)ArgumentRegistersOffset + PointerSize; + } + return (int)ArgumentRegistersOffset; + } + } + + /// + /// Rounds up a parameter size to the stack slot size for the platform. + /// + public int StackElemSize(int parmSize, bool isValueType = false, bool isFloatHfa = false) + { + if (IsAppleArm64ABI) + { + if (!isValueType) + { + // Primitives use their natural size, no padding + return parmSize; + } + if (isFloatHfa) + { + // Float HFA: 4-byte alignment + return parmSize; + } + } + return AlignUp(parmSize, StackSlotSize); + } + + /// + /// Maps a GCRefMap position index to the byte offset in the transition block. + /// + public int OffsetFromGCRefMapPos(int pos) + { + if (Architecture == RuntimeInfoArchitecture.X86) + { + if (pos < NumArgumentRegisters) + { + return (int)ArgumentRegistersOffset + SizeOfArgumentRegisters - (pos + 1) * PointerSize; + } + return (int)OffsetOfArgs + (pos - NumArgumentRegisters) * PointerSize; + } + return (int)FirstGCRefMapSlot + pos * PointerSize; + } + + /// + /// Returns true if the argument at the given offset is a float register. + /// Float register offsets are negative. + /// + public static bool IsFloatArgumentRegisterOffset(int offset) => offset < 0; + + /// + /// Returns true if the argument at the given offset is in a general-purpose register. + /// + public bool IsArgumentRegisterOffset(int offset) + { + return offset >= (int)ArgumentRegistersOffset + && offset < (int)ArgumentRegistersOffset + SizeOfArgumentRegisters; + } + + /// + /// Returns true if the argument at the given offset is on the stack. + /// + public bool IsStackArgumentOffset(int offset) + { + return offset >= (int)ArgumentRegistersOffset + SizeOfArgumentRegisters; + } + + /// + /// Checks if a value type of the given size should be passed by reference + /// (applies to x64 and ARM64). + /// + public bool IsArgPassedByRef(int size) + { + if (EnregisteredParamTypeMaxSize == 0) + return false; + + if (Architecture == RuntimeInfoArchitecture.X64) + { + // On x64, also check power-of-2 rule + return size > EnregisteredParamTypeMaxSize || (size & (size - 1)) != 0; + } + + return size > EnregisteredParamTypeMaxSize; + } + + /// + /// Returns the byte offset of the return buffer argument. + /// + public int GetRetBuffArgOffset(bool hasThis) + { + if (Architecture == RuntimeInfoArchitecture.X86) + { + // x86: retbuf goes in EDX if hasThis (this in ECX), else ECX + return hasThis + ? (int)ArgumentRegistersOffset // EDX offset = 0 + : (int)ArgumentRegistersOffset + PointerSize; // ECX offset + } + if (Architecture == RuntimeInfoArchitecture.Arm64) + { + // ARM64: retbuf is in X8, which is at FirstGCRefMapSlot + return (int)FirstGCRefMapSlot; + } + // Default: retbuf is after 'this' in the argument registers + return (int)ArgumentRegistersOffset + (hasThis ? PointerSize : 0); + } + + internal static int AlignUp(int value, int alignment) + { + return (value + (alignment - 1)) & ~(alignment - 1); + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs index 4edd821c203dc9..88bfcaece90d1d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs @@ -2,6 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.CallingConvention; +using Microsoft.Diagnostics.DataContractReader.Data; namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; @@ -47,11 +51,15 @@ internal enum FrameType private readonly Target target; private readonly TargetPointer terminator; private TargetPointer currentFramePointer; + private CallingConventionInfo? _callingConventionInfo; internal Data.Frame CurrentFrame => target.ProcessedData.GetOrAdd(currentFramePointer); public TargetPointer CurrentFrameAddress => currentFramePointer; + private CallingConventionInfo GetCallingConventionInfo() + => _callingConventionInfo ??= new CallingConventionInfo(target); + public FrameIterator(Target target, ThreadData threadData) { this.target = target; @@ -132,20 +140,81 @@ public void UpdateContextFromFrame(IPlatformAgnosticContext context) } } - public bool IsInlineCallFrameWithActiveCall() + /// + /// Returns the return address for the current Frame, matching native Frame::GetReturnAddress(). + /// Returns TargetPointer.Null if the Frame has no return address (e.g., non-active ICF, + /// base Frame types, FuncEvalFrame during exception eval). + /// + public TargetPointer GetReturnAddress() { - if (GetFrameType(target, CurrentFrame.Identifier) != FrameType.InlinedCallFrame) + FrameType frameType = GetCurrentFrameType(); + switch (frameType) { - return false; - } - Data.InlinedCallFrame inlinedCallFrame = target.ProcessedData.GetOrAdd(currentFramePointer); - return InlinedCallFrameHasActiveCall(inlinedCallFrame); - } + // InlinedCallFrame: returns 0 if inactive, else m_pCallerReturnAddress + case FrameType.InlinedCallFrame: + Data.InlinedCallFrame icf = target.ProcessedData.GetOrAdd(currentFramePointer); + return InlinedCallFrameHasActiveCall(icf) ? new TargetPointer(icf.CallerReturnAddress) : TargetPointer.Null; - public static bool IsInlinedCallFrame(Target target, TargetPointer framePointer) - { - Data.Frame frame = target.ProcessedData.GetOrAdd(framePointer); - return GetFrameType(target, frame.Identifier) == FrameType.InlinedCallFrame; + // TransitionFrame types: read return address from the transition block + case FrameType.FramedMethodFrame: + case FrameType.PInvokeCalliFrame: + case FrameType.PrestubMethodFrame: + case FrameType.StubDispatchFrame: + case FrameType.CallCountingHelperFrame: + case FrameType.ExternalMethodFrame: + case FrameType.DynamicHelperFrame: + Data.FramedMethodFrame fmf = target.ProcessedData.GetOrAdd(currentFramePointer); + Data.TransitionBlock tb = target.ProcessedData.GetOrAdd(fmf.TransitionBlockPtr); + return tb.ReturnAddress; + + // SoftwareExceptionFrame: stored m_ReturnAddress + case FrameType.SoftwareExceptionFrame: + Data.SoftwareExceptionFrame sef = target.ProcessedData.GetOrAdd(currentFramePointer); + return sef.ReturnAddress; + + // ResumableFrame / RedirectedThreadFrame: RIP from captured context + case FrameType.ResumableFrame: + case FrameType.RedirectedThreadFrame: + { + Data.ResumableFrame rf = target.ProcessedData.GetOrAdd(currentFramePointer); + IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(target); + ctx.ReadFromAddress(target, rf.TargetContextPtr); + return ctx.InstructionPointer; + } + + // FaultingExceptionFrame: RIP from embedded context + case FrameType.FaultingExceptionFrame: + { + Data.FaultingExceptionFrame fef = target.ProcessedData.GetOrAdd(currentFramePointer); + IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(target); + ctx.ReadFromAddress(target, fef.TargetContext); + return ctx.InstructionPointer; + } + + // HijackFrame: stored m_ReturnAddress + case FrameType.HijackFrame: + Data.HijackFrame hf = target.ProcessedData.GetOrAdd(currentFramePointer); + return hf.ReturnAddress; + + // TailCallFrame: stored m_ReturnAddress + case FrameType.TailCallFrame: + Data.TailCallFrame tcf = target.ProcessedData.GetOrAdd(currentFramePointer); + return tcf.ReturnAddress; + + // FuncEvalFrame: returns 0 during exception eval, else from transition block + case FrameType.FuncEvalFrame: + Data.FuncEvalFrame funcEval = target.ProcessedData.GetOrAdd(currentFramePointer); + Data.DebuggerEval dbgEval = target.ProcessedData.GetOrAdd(funcEval.DebuggerEvalPtr); + if (dbgEval.EvalDuringException) + return TargetPointer.Null; + Data.FramedMethodFrame funcEvalFmf = target.ProcessedData.GetOrAdd(currentFramePointer); + Data.TransitionBlock funcEvalTb = target.ProcessedData.GetOrAdd(funcEvalFmf.TransitionBlockPtr); + return funcEvalTb.ReturnAddress; + + // Base Frame and unknown types: return 0 (matches native Frame::GetReturnAddressPtr_Impl) + default: + return TargetPointer.Null; + } } public static string GetFrameName(Target target, TargetPointer frameIdentifier) @@ -160,7 +229,7 @@ public static string GetFrameName(Target target, TargetPointer frameIdentifier) public FrameType GetCurrentFrameType() => GetFrameType(target, CurrentFrame.Identifier); - private static FrameType GetFrameType(Target target, TargetPointer frameIdentifier) + internal static FrameType GetFrameType(Target target, TargetPointer frameIdentifier) { foreach (FrameType frameType in Enum.GetValues()) { @@ -233,35 +302,488 @@ public static TargetPointer GetMethodDescPtr(Target target, TargetPointer frameP } } - public static TargetPointer GetReturnAddress(Target target, TargetPointer framePtr) + private static bool InlinedCallFrameHasFunction(Data.InlinedCallFrame frame, Target target) { - Data.Frame frame = target.ProcessedData.GetOrAdd(framePtr); - FrameType frameType = GetFrameType(target, frame.Identifier); + if (target.PointerSize == sizeof(ulong)) + { + return frame.Datum != TargetPointer.Null && (frame.Datum.Value & 0x1) == 0; + } + else + { + return ((long)frame.Datum.Value & ~0xffff) != 0; + } + } + + private static bool InlinedCallFrameHasActiveCall(Data.InlinedCallFrame frame) + { + return frame.CallerReturnAddress != TargetPointer.Null; + } + + // ===== Frame GC Root Scanning ===== + + /// + /// Scans GC roots for a Frame based on its type. + /// Dispatches to the appropriate scanning method (GCRefMap, MetaSig, or custom). + /// Matches native Frame::GcScanRoots_Impl virtual dispatch. + /// + internal void GcScanRoots(TargetPointer frameAddress, GcScanContext scanContext) + { + if (frameAddress == TargetPointer.Null) + return; + + Data.Frame frameData = target.ProcessedData.GetOrAdd(frameAddress); + FrameType frameType = GetFrameType(target, frameData.Identifier); + switch (frameType) { - case FrameType.InlinedCallFrame: - Data.InlinedCallFrame inlinedCallFrame = target.ProcessedData.GetOrAdd(frame.Address); - return InlinedCallFrameHasActiveCall(inlinedCallFrame) ? inlinedCallFrame.CallerReturnAddress : TargetPointer.Null; + case FrameType.StubDispatchFrame: + { + Data.FramedMethodFrame fmf = target.ProcessedData.GetOrAdd(frameAddress); + Data.StubDispatchFrame sdf = target.ProcessedData.GetOrAdd(frameAddress); + TargetPointer gcRefMap = sdf.GCRefMap; + + // Resolve GCRefMap via indirection if not yet cached + if (gcRefMap == TargetPointer.Null && sdf.Indirection != TargetPointer.Null) + gcRefMap = FindGCRefMap(sdf.ZapModule, sdf.Indirection); + + if (gcRefMap != TargetPointer.Null) + PromoteCallerStackUsingGCRefMap(fmf.TransitionBlockPtr, gcRefMap, scanContext); + else + PromoteCallerStack(frameAddress, fmf.TransitionBlockPtr, scanContext); + break; + } + + case FrameType.ExternalMethodFrame: + { + Data.FramedMethodFrame fmf = target.ProcessedData.GetOrAdd(frameAddress); + Data.ExternalMethodFrame emf = target.ProcessedData.GetOrAdd(frameAddress); + TargetPointer gcRefMap = emf.GCRefMap; + + // Resolve GCRefMap via FindGCRefMap if not yet cached by the runtime + if (gcRefMap == TargetPointer.Null && emf.Indirection != TargetPointer.Null) + gcRefMap = FindGCRefMap(emf.ZapModule, emf.Indirection); + + if (gcRefMap != TargetPointer.Null) + PromoteCallerStackUsingGCRefMap(fmf.TransitionBlockPtr, gcRefMap, scanContext); + else + PromoteCallerStack(frameAddress, fmf.TransitionBlockPtr, scanContext); + break; + } + + case FrameType.DynamicHelperFrame: + { + Data.FramedMethodFrame fmf = target.ProcessedData.GetOrAdd(frameAddress); + Data.DynamicHelperFrame dhf = target.ProcessedData.GetOrAdd(frameAddress); + ScanDynamicHelperFrame(fmf.TransitionBlockPtr, dhf.DynamicHelperFrameFlags, scanContext); + break; + } + + case FrameType.CallCountingHelperFrame: + case FrameType.PrestubMethodFrame: + { + Data.FramedMethodFrame fmf = target.ProcessedData.GetOrAdd(frameAddress); + PromoteCallerStack(frameAddress, fmf.TransitionBlockPtr, scanContext); + break; + } + + case FrameType.HijackFrame: + // TODO(stackref): Implement HijackFrame scanning (X86 only with FEATURE_HIJACK) + break; + + case FrameType.ProtectValueClassFrame: + // TODO(stackref): Implement ProtectValueClassFrame scanning + break; + default: - // NotImplemented for other frame types + // Base Frame::GcScanRoots_Impl is a no-op for most frame types. + break; + } + } + + /// + /// Decodes a GCRefMap bitstream and reports GC references in the transition block. + /// Port of native TransitionFrame::PromoteCallerStackUsingGCRefMap (frames.cpp). + /// + private void PromoteCallerStackUsingGCRefMap( + TargetPointer transitionBlock, + TargetPointer gcRefMapBlob, + GcScanContext scanContext) + { + GCRefMapDecoder decoder = new(target, gcRefMapBlob); + + if (target.PointerSize == 4) + decoder.ReadStackPop(); + + while (!decoder.AtEnd) + { + int pos = decoder.CurrentPos; + GCRefMapToken token = decoder.ReadToken(); + int offset = GetCallingConventionInfo().OffsetFromGCRefMapPos(pos); + TargetPointer slotAddress = new(transitionBlock.Value + (ulong)offset); + + switch (token) + { + case GCRefMapToken.Skip: + break; + case GCRefMapToken.Ref: + scanContext.GCReportCallback(slotAddress, GcScanFlags.None); + break; + case GCRefMapToken.Interior: + scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); + break; + case GCRefMapToken.MethodParam: + case GCRefMapToken.TypeParam: + break; + case GCRefMapToken.VASigCookie: + // TODO(stackref): Implement VASIG_COOKIE handling + break; + } + } + } + + /// + /// Scans GC roots for a DynamicHelperFrame based on its flags. + /// Port of native DynamicHelperFrame::GcScanRoots_Impl (frames.cpp). + /// + private void ScanDynamicHelperFrame( + TargetPointer transitionBlock, + int dynamicHelperFrameFlags, + GcScanContext scanContext) + { + const int DynamicHelperFrameFlags_ObjectArg = 1; + const int DynamicHelperFrameFlags_ObjectArg2 = 2; + + Target.TypeInfo tbType = target.GetTypeInfo(DataType.TransitionBlock); + uint argRegOffset = (uint)tbType.Fields[nameof(Data.TransitionBlock.ArgumentRegistersOffset)].Offset; + + if ((dynamicHelperFrameFlags & DynamicHelperFrameFlags_ObjectArg) != 0) + { + TargetPointer argAddr = new(transitionBlock.Value + argRegOffset); + scanContext.GCReportCallback(argAddr, GcScanFlags.None); + } + + if ((dynamicHelperFrameFlags & DynamicHelperFrameFlags_ObjectArg2) != 0) + { + TargetPointer argAddr = new(transitionBlock.Value + argRegOffset + (uint)target.PointerSize); + scanContext.GCReportCallback(argAddr, GcScanFlags.None); + } + } + + /// + /// Resolves the GCRefMap for a Frame with m_pIndirection set but m_pGCRefMap not yet cached. + /// Port of native FindGCRefMap (frames.cpp:853). + /// + private TargetPointer FindGCRefMap(TargetPointer zapModule, TargetPointer indirection) + { + if (indirection == TargetPointer.Null) + return TargetPointer.Null; + + // If ZapModule is null, resolve it from the indirection address. + // Matches native GetGCRefMap which calls FindModuleForGCRefMap(m_pIndirection) + // → ExecutionManager::FindReadyToRunModule. + if (zapModule == TargetPointer.Null) + { + IExecutionManager eman = target.Contracts.ExecutionManager; + zapModule = eman.FindReadyToRunModule(indirection); + if (zapModule == TargetPointer.Null) return TargetPointer.Null; } + + // Get the ReadyToRunInfo from the module + Data.Module module = target.ProcessedData.GetOrAdd(zapModule); + if (module.ReadyToRunInfo == TargetPointer.Null) + return TargetPointer.Null; + + Data.ReadyToRunInfo r2rInfo = target.ProcessedData.GetOrAdd(module.ReadyToRunInfo); + if (r2rInfo.ImportSections == TargetPointer.Null || r2rInfo.NumImportSections == 0) + return TargetPointer.Null; + + // Compute RVA = indirection - imageBase + ulong imageBase = r2rInfo.LoadedImageBase.Value; + if (indirection.Value < imageBase) + return TargetPointer.Null; + ulong diff = indirection.Value - imageBase; + if (diff > uint.MaxValue) + return TargetPointer.Null; + uint rva = (uint)diff; + + // READYTORUN_IMPORT_SECTION layout: + // IMAGE_DATA_DIRECTORY Section (VirtualAddress:4, Size:4) = 8 bytes + // ReadyToRunImportSectionFlags Flags (2 bytes) + // ReadyToRunImportSectionType Type (1 byte) + // BYTE EntrySize (1 byte) + // DWORD Signatures (4 bytes) + // DWORD AuxiliaryData (4 bytes) + // Total: 20 bytes + const int ImportSectionSize = 20; + const int SectionVAOffset = 0; + const int SectionSizeOffset = 4; + const int EntrySizeOffset = 11; + const int AuxiliaryDataOffset = 16; + + TargetPointer sectionsBase = r2rInfo.ImportSections; + for (uint i = 0; i < r2rInfo.NumImportSections; i++) + { + TargetPointer sectionAddr = new(sectionsBase.Value + i * ImportSectionSize); + uint sectionVA = target.Read(sectionAddr + SectionVAOffset); + uint sectionSize = target.Read(sectionAddr + SectionSizeOffset); + + if (rva >= sectionVA && rva < sectionVA + sectionSize) + { + byte entrySize = target.Read(sectionAddr + EntrySizeOffset); + if (entrySize == 0) + return TargetPointer.Null; + + uint index = (rva - sectionVA) / entrySize; + uint auxDataRva = target.Read(sectionAddr + AuxiliaryDataOffset); + if (auxDataRva == 0) + return TargetPointer.Null; + + TargetPointer gcRefMapBase = new(imageBase + auxDataRva); + + // GCRefMap starts with a lookup index for stride-based access. + // GCREFMAP_LOOKUP_STRIDE is 1024 in the native code. + const uint GCREFMAP_LOOKUP_STRIDE = 1024; + uint lookupIndex = index / GCREFMAP_LOOKUP_STRIDE; + uint remaining = index % GCREFMAP_LOOKUP_STRIDE; + + // Read the offset from the lookup table (array of DWORDs) + uint lookupOffset = target.Read(new TargetPointer(gcRefMapBase.Value + lookupIndex * 4)); + TargetPointer p = new(gcRefMapBase.Value + lookupOffset); + + // Linear scan past 'remaining' entries + while (remaining > 0) + { + // Each entry is a variable-length sequence of bytes where the high bit + // indicates continuation. Skip until we find a byte without the high bit set. + while ((target.Read(p) & 0x80) != 0) + p = new(p.Value + 1); + p = new(p.Value + 1); // skip the final byte of this entry + + remaining--; + } + + return p; + } + } + + return TargetPointer.Null; } - private static bool InlinedCallFrameHasFunction(Data.InlinedCallFrame frame, Target target) + /// + /// Entry point for promoting caller stack GC references via method signature. + /// Matches native TransitionFrame::PromoteCallerStack (frames.cpp:1494). + /// + private void PromoteCallerStack( + TargetPointer frameAddress, + TargetPointer transitionBlock, + GcScanContext scanContext) { - if (target.PointerSize == sizeof(ulong)) + Data.FramedMethodFrame fmf = target.ProcessedData.GetOrAdd(frameAddress); + TargetPointer methodDescPtr = fmf.MethodDescPtr; + if (methodDescPtr == TargetPointer.Null) + return; + + ReadOnlySpan signature; + MetadataReader? metadataReader; + try { - return frame.Datum != TargetPointer.Null && (frame.Datum.Value & 0x1) == 0; + signature = GetMethodSignatureBytes(methodDescPtr, out metadataReader); } - else + catch (System.Exception) { - return ((long)frame.Datum.Value & ~0xffff) != 0; + return; + } + + if (signature.IsEmpty) + return; + + MethodSignature methodSig; + try + { + RuntimeSignatureDecoder decoder = new( + GcSignatureTypeProvider.Instance, target, genericContext: null, + new SpanSignatureReader(signature, target.IsLittleEndian), metadataReader); + methodSig = decoder.DecodeMethodSignature(); + } + catch (System.Exception) + { + // If signature decoding fails for any reason, skip this frame. + // The GCRefMap path handles these cases when available. + return; + } + + if (methodSig.Header.CallingConvention is SignatureCallingConvention.VarArgs) + { + // TODO(stackref): VarArg path — read VASigCookie from frame + return; } + + IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; + MethodDescHandle mdh = rts.GetMethodDescHandle(methodDescPtr); + + bool hasThis = methodSig.Header.IsInstance; + bool requiresInstArg = false; + bool isAsync = false; + bool isValueTypeThis = false; + + try + { + requiresInstArg = rts.RequiresInstArg(mdh); + isAsync = rts.IsAsyncMethod(mdh); + + // TODO(stackref): Detect value type 'this' (needs IRuntimeTypeSystem.IsValueType) + // TODO(stackref): String constructor clears HasThis + } + catch + { + } + + PromoteCallerStackHelper(transitionBlock, methodSig, hasThis, + requiresInstArg, isAsync, isValueTypeThis, scanContext); } - private static bool InlinedCallFrameHasActiveCall(Data.InlinedCallFrame frame) + /// + /// Core logic for promoting caller stack GC references. + /// Uses to correctly map arguments to their + /// register/stack locations per the target's calling convention. + /// Matches native TransitionFrame::PromoteCallerStackHelper (frames.cpp:1546). + /// + private void PromoteCallerStackHelper( + TargetPointer transitionBlock, + MethodSignature methodSig, + bool hasThis, + bool requiresInstArg, + bool isAsync, + bool isValueTypeThis, + GcScanContext scanContext) { - return frame.CallerReturnAddress != TargetPointer.Null; + CallingConventionInfo ccInfo; + try + { + ccInfo = GetCallingConventionInfo(); + } + catch + { + return; + } + + // Build ArgTypeInfo array from decoded signature + ArgTypeInfo[] paramTypes = new ArgTypeInfo[methodSig.ParameterTypes.Length]; + for (int i = 0; i < paramTypes.Length; i++) + { + paramTypes[i] = GcTypeKindToArgTypeInfo(methodSig.ParameterTypes[i], ccInfo.PointerSize); + } + ArgTypeInfo returnTypeInfo = GcTypeKindToArgTypeInfo(methodSig.ReturnType, ccInfo.PointerSize); + + ArgIteratorData argData = new(hasThis, methodSig.Header.CallingConvention is SignatureCallingConvention.VarArgs, paramTypes, returnTypeInfo); + CallingConvention.ArgIterator argit = new(ccInfo, argData, hasParamType: requiresInstArg, hasAsyncContinuation: isAsync, forcedByRefParams: System.Array.Empty()); + + // Promote 'this' for non-static methods + if (argit.HasThis) + { + int thisOffset = argit.GetThisOffset(); + TargetPointer thisAddr = new(transitionBlock.Value + (ulong)thisOffset); + GcScanFlags thisFlags = isValueTypeThis ? GcScanFlags.GC_CALL_INTERIOR : GcScanFlags.None; + scanContext.GCReportCallback(thisAddr, thisFlags); + } + + // Promote async continuation + if (argit.HasAsyncContinuation) + { + int asyncOffset = argit.GetAsyncContinuationArgOffset(); + TargetPointer asyncAddr = new(transitionBlock.Value + (ulong)asyncOffset); + scanContext.GCReportCallback(asyncAddr, GcScanFlags.None); + } + + // Walk each argument using ArgIterator for correct offsets + int argIndex = 0; + int argOffset; + while ((argOffset = argit.GetNextOffset()) != CallingConventionInfo.InvalidOffset) + { + if (argIndex >= methodSig.ParameterTypes.Length) + break; + + GcTypeKind kind = methodSig.ParameterTypes[argIndex]; + TargetPointer slotAddress = new(transitionBlock.Value + (ulong)argOffset); + + switch (kind) + { + case GcTypeKind.Ref: + scanContext.GCReportCallback(slotAddress, GcScanFlags.None); + break; + case GcTypeKind.Interior: + scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); + break; + case GcTypeKind.Other: + // Value type: if passed by ref, report as interior pointer + if (argit.IsArgPassedByRef()) + { + scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); + } + // TODO: For value types passed by value, enumerate fields for embedded GC refs + break; + case GcTypeKind.None: + break; + } + argIndex++; + } + } + + /// + /// Converts a to a minimal for + /// ArgIterator consumption. This is a bridge until we have a full type-aware provider. + /// + private static ArgTypeInfo GcTypeKindToArgTypeInfo(GcTypeKind kind, int pointerSize) + { + return kind switch + { + GcTypeKind.None => ArgTypeInfo.ForPrimitive(CorElementType.I, pointerSize), + GcTypeKind.Ref => ArgTypeInfo.ForPrimitive(CorElementType.Class, pointerSize), + GcTypeKind.Interior => ArgTypeInfo.ForPrimitive(CorElementType.Byref, pointerSize), + GcTypeKind.Other => new ArgTypeInfo + { + CorElementType = CorElementType.ValueType, + Size = pointerSize, // Conservative: assume pointer-sized for now + IsValueType = true, + }, + _ => ArgTypeInfo.ForPrimitive(CorElementType.I, pointerSize), + }; + } + + private ReadOnlySpan GetMethodSignatureBytes(TargetPointer methodDescPtr, out MetadataReader? metadataReader) + { + metadataReader = null; + IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; + MethodDescHandle mdh = rts.GetMethodDescHandle(methodDescPtr); + + if (rts.IsStoredSigMethodDesc(mdh, out ReadOnlySpan storedSig)) + return storedSig; + + uint methodToken = rts.GetMethodToken(mdh); + if (methodToken == 0x06000000) + return default; + + TargetPointer methodTablePtr = rts.GetMethodTable(mdh); + TypeHandle typeHandle = rts.GetTypeHandle(methodTablePtr); + TargetPointer modulePtr = rts.GetModule(typeHandle); + + ILoader loader = target.Contracts.Loader; + ModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(modulePtr); + + IEcmaMetadata ecmaMetadata = target.Contracts.EcmaMetadata; + metadataReader = ecmaMetadata.GetMetadata(moduleHandle); + if (metadataReader is null) + return default; + + MethodDefinitionHandle methodDefHandle = MetadataTokens.MethodDefinitionHandle((int)(methodToken & 0x00FFFFFF)); + MethodDefinition methodDef = metadataReader.GetMethodDefinition(methodDefHandle); + BlobReader blobReader = metadataReader.GetBlobReader(methodDef.Signature); + return blobReader.ReadBytes(blobReader.Length); + } + + private bool IsTargetArm64() + { + return target.Contracts.RuntimeInfo.GetTargetArchitecture() is RuntimeInfoArchitecture.Arm64; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs new file mode 100644 index 00000000000000..6815878ec65c86 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +/// +/// Token values from CORCOMPILE_GCREFMAP_TOKENS (corcompile.h). +/// These indicate the type of GC reference at each transition block slot. +/// +internal enum GCRefMapToken +{ + Skip = 0, + Ref = 1, + Interior = 2, + MethodParam = 3, + TypeParam = 4, + VASigCookie = 5, +} + +/// +/// Managed port of the native GCRefMapDecoder (gcrefmap.h). +/// +/// A GCRefMap is a compact bitstream that describes which transition block slots +/// contain GC references for a given call site (e.g., in ReadyToRun stubs). +/// It is used by ExternalMethodFrame and StubDispatchFrame to report GC roots +/// without needing the full MethodDesc/signature decoding path. +/// +/// Encoding: each slot is encoded as a variable-length integer using 3 bits per +/// token (see ), with a high-bit continuation flag. +/// A "skip" token advances the slot position without reporting. The stream ends +/// when all slots have been consumed (indicated by a zero byte after the last token). +/// +/// The native implementation lives in coreclr/inc/gcrefmap.h (GCRefMapDecoder class). +/// +internal ref struct GCRefMapDecoder +{ + private readonly Target _target; + private TargetPointer _currentByte; + private int _pendingByte; + private int _pos; + + public GCRefMapDecoder(Target target, TargetPointer blob) + { + _target = target; + _currentByte = blob; + _pendingByte = 0x80; // Forces first byte read + _pos = 0; + } + + public readonly bool AtEnd => _pendingByte == 0; + + public readonly int CurrentPos => _pos; + + private int GetBit() + { + int x = _pendingByte; + if ((x & 0x80) != 0) + { + x = _target.Read(_currentByte); + _currentByte = new TargetPointer(_currentByte.Value + 1); + x |= (x & 0x80) << 7; + } + _pendingByte = x >> 1; + return x & 1; + } + + private int GetTwoBit() + { + int result = GetBit(); + result |= GetBit() << 1; + return result; + } + + private int GetInt() + { + int result = 0; + int bit = 0; + do + { + result |= GetBit() << (bit++); + result |= GetBit() << (bit++); + result |= GetBit() << (bit++); + } + while (GetBit() != 0); + return result; + } + + /// + /// x86 only: Read the stack pop count from the stream. + /// + public uint ReadStackPop() + { + int x = GetTwoBit(); + if (x == 3) + x = GetInt() + 3; + return (uint)x; + } + + /// + /// Read the next GC reference token from the stream. + /// Advances CurrentPos as appropriate. + /// + public GCRefMapToken ReadToken() + { + int val = GetTwoBit(); + if (val == 3) + { + int ext = GetInt(); + if ((ext & 1) == 0) + { + _pos += (ext >> 1) + 4; + return GCRefMapToken.Skip; + } + else + { + _pos++; + return (GCRefMapToken)((ext >> 1) + 3); + } + } + _pos++; + return (GCRefMapToken)val; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs deleted file mode 100644 index fa72eb606fad75..00000000000000 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ /dev/null @@ -1,109 +0,0 @@ -// 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 Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; - -namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; - -internal class GcScanner -{ - private readonly Target _target; - private readonly IExecutionManager _eman; - private readonly IGCInfo _gcInfo; - - internal GcScanner(Target target) - { - _target = target; - _eman = target.Contracts.ExecutionManager; - _gcInfo = target.Contracts.GCInfo; - } - - public bool EnumGcRefs( - IPlatformAgnosticContext context, - CodeBlockHandle cbh, - CodeManagerFlags flags, - GcScanContext scanContext) - { - TargetNUInt relativeOffset = _eman.GetRelativeOffset(cbh); - _eman.GetGCInfo(cbh, out TargetPointer gcInfoAddr, out uint gcVersion); - - if (_eman.IsFilterFunclet(cbh)) - flags |= CodeManagerFlags.NoReportUntracked; - - IGCInfoHandle handle = _gcInfo.DecodePlatformSpecificGCInfo(gcInfoAddr, gcVersion); - if (handle is not IGCInfoDecoder decoder) - return false; - - uint stackBaseRegister = decoder.StackBaseRegister; - - // Lazily compute the caller SP for GC_CALLER_SP_REL slots. - // The native code uses GET_CALLER_SP(pRD) which comes from EnsureCallerContextIsValid. - TargetPointer? callerSP = null; - - return decoder.EnumerateLiveSlots( - (uint)relativeOffset.Value, - flags, - (bool isRegister, uint registerNumber, int spOffset, uint spBase, uint gcFlags) => - { - GcScanFlags scanFlags = GcScanFlags.None; - if ((gcFlags & 0x1) != 0) // GC_SLOT_INTERIOR - scanFlags |= GcScanFlags.GC_CALL_INTERIOR; - if ((gcFlags & 0x2) != 0) // GC_SLOT_PINNED - scanFlags |= GcScanFlags.GC_CALL_PINNED; - - if (isRegister) - { - TargetPointer regValue = ReadRegisterValue(context, (int)registerNumber); - GcScanSlotLocation loc = new((int)registerNumber, 0, false); - scanContext.GCEnumCallback(regValue, scanFlags, loc); - } - else - { - int spReg = context.StackPointerRegister; - int reg = spBase switch - { - 1 => spReg, // GC_SP_REL → SP register number - 2 => (int)stackBaseRegister, // GC_FRAMEREG_REL → frame base register - 0 => -(spReg + 1), // GC_CALLER_SP_REL → -(SP + 1) - _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), - }; - TargetPointer baseAddr = spBase switch - { - 1 => context.StackPointer, // GC_SP_REL - 2 => ReadRegisterValue(context, (int)stackBaseRegister), // GC_FRAMEREG_REL - 0 => GetCallerSP(context, ref callerSP), // GC_CALLER_SP_REL - _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), - }; - - TargetPointer addr = new(baseAddr.Value + (ulong)(long)spOffset); - GcScanSlotLocation loc = new(reg, spOffset, true); - scanContext.GCEnumCallback(addr, scanFlags, loc); - } - }); - } - - /// - /// Compute the caller's SP by unwinding the current context one frame. - /// Cached in to avoid repeated unwinds for the same frame. - /// - private TargetPointer GetCallerSP(IPlatformAgnosticContext context, ref TargetPointer? cached) - { - if (cached is null) - { - IPlatformAgnosticContext callerContext = context.Clone(); - callerContext.Unwind(_target); - cached = callerContext.StackPointer; - } - return cached.Value; - } - - private static TargetPointer ReadRegisterValue(IPlatformAgnosticContext context, int registerNumber) - { - if (!context.TryReadRegister(registerNumber, out TargetNUInt value)) - throw new ArgumentOutOfRangeException(nameof(registerNumber), $"Register number {registerNumber} not found"); - - return new TargetPointer(value.Value); - } - -} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs new file mode 100644 index 00000000000000..46658212eb81e4 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.Reflection.Metadata; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +/// +/// Classification of a signature type for GC scanning purposes. +/// +internal enum GcTypeKind +{ + /// Not a GC reference (primitives, pointers). + None, + /// Object reference (class, string, array). + Ref, + /// Interior pointer (byref). + Interior, + /// Value type that may contain embedded GC references. + Other, +} + +/// +/// Classifies signature types for GC scanning purposes. +/// Implements which +/// is a superset of SRM's , +/// adding support for ELEMENT_TYPE_INTERNAL. +/// +internal sealed class GcSignatureTypeProvider + : IRuntimeSignatureTypeProvider +{ + public static readonly GcSignatureTypeProvider Instance = new(); + + public GcTypeKind GetPrimitiveType(PrimitiveTypeCode typeCode) + => typeCode switch + { + PrimitiveTypeCode.String or PrimitiveTypeCode.Object => GcTypeKind.Ref, + PrimitiveTypeCode.TypedReference => GcTypeKind.Other, + _ => GcTypeKind.None, + }; + + public GcTypeKind GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) + => rawTypeKind == 0x11 ? GcTypeKind.Other : GcTypeKind.Ref; + + public GcTypeKind GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + => rawTypeKind == 0x11 ? GcTypeKind.Other : GcTypeKind.Ref; + + public GcTypeKind GetTypeFromSpecification(MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind) + => rawTypeKind == 0x11 ? GcTypeKind.Other : GcTypeKind.Ref; + + public GcTypeKind GetSZArrayType(GcTypeKind elementType) => GcTypeKind.Ref; + public GcTypeKind GetArrayType(GcTypeKind elementType, ArrayShape shape) => GcTypeKind.Ref; + public GcTypeKind GetByReferenceType(GcTypeKind elementType) => GcTypeKind.Interior; + public GcTypeKind GetPointerType(GcTypeKind elementType) => GcTypeKind.None; + + public GcTypeKind GetGenericInstantiation(GcTypeKind genericType, ImmutableArray typeArguments) + => genericType; + + public GcTypeKind GetGenericMethodParameter(object? genericContext, int index) => GcTypeKind.Ref; + public GcTypeKind GetGenericTypeParameter(object? genericContext, int index) => GcTypeKind.Ref; + public GcTypeKind GetFunctionPointerType(MethodSignature signature) => GcTypeKind.None; + public GcTypeKind GetModifiedType(GcTypeKind modifier, GcTypeKind unmodifiedType, bool isRequired) => unmodifiedType; + public GcTypeKind GetInternalModifiedType(Target target, TargetPointer typeHandlePointer, GcTypeKind unmodifiedType, bool isRequired) => unmodifiedType; + public GcTypeKind GetPinnedType(GcTypeKind elementType) => elementType; + + public GcTypeKind GetInternalType(Target target, TargetPointer typeHandlePointer) + { + if (typeHandlePointer == TargetPointer.Null) + return GcTypeKind.None; + + try + { + IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; + TypeHandle th = rts.GetTypeHandle(typeHandlePointer); + CorElementType corType = rts.GetSignatureCorElementType(th); + + return corType switch + { + CorElementType.Void or CorElementType.Boolean or CorElementType.Char + or CorElementType.I1 or CorElementType.U1 + or CorElementType.I2 or CorElementType.U2 + or CorElementType.I4 or CorElementType.U4 + or CorElementType.I8 or CorElementType.U8 + or CorElementType.R4 or CorElementType.R8 + or CorElementType.I or CorElementType.U + or CorElementType.FnPtr or CorElementType.Ptr + => GcTypeKind.None, + + CorElementType.Byref => GcTypeKind.Interior, + CorElementType.ValueType => GcTypeKind.Other, + + _ => GcTypeKind.Ref, + }; + } + catch + { + return GcTypeKind.Ref; + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/RuntimeSignatureDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/RuntimeSignatureDecoder.cs new file mode 100644 index 00000000000000..9998176d7e4f04 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/RuntimeSignatureDecoder.cs @@ -0,0 +1,422 @@ +// 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.Buffers.Binary; +using System.Collections.Immutable; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +/// +/// Superset of SRM's +/// that adds support for runtime-internal type codes (ELEMENT_TYPE_INTERNAL). +/// +/// +/// Providers implementing this interface automatically satisfy SRM's +/// and can be used +/// with both SRM's SignatureDecoder and our +/// . +/// +internal interface IRuntimeSignatureTypeProvider + : ISignatureTypeProvider +{ + /// + /// Classify an ELEMENT_TYPE_INTERNAL (0x21) type by resolving the + /// embedded TypeHandle pointer via the target's runtime type system. + /// + TType GetInternalType(Target target, TargetPointer typeHandlePointer); + + /// + /// Classify an ELEMENT_TYPE_CMOD_INTERNAL (0x22) custom modifier by + /// resolving the embedded TypeHandle pointer via the target's runtime type system. + /// + TType GetInternalModifiedType(Target target, TargetPointer typeHandlePointer, TType unmodifiedType, bool isRequired); +} + +/// +/// Abstraction for reading bytes from a signature blob. +/// +/// +/// Allows the decoder to read from different sources (in-memory spans, +/// target process memory) without allocating intermediate byte arrays. +/// +internal interface ISignatureReader +{ + byte ReadByte(); + byte PeekByte(); + int Remaining { get; } + + /// Reads a pointer-sized unsigned value (4 or 8 bytes). + ulong ReadPointerSized(int pointerSize); +} + +/// +/// Reads signature bytes from a . +/// +internal ref struct SpanSignatureReader : ISignatureReader +{ + private readonly ReadOnlySpan _blob; + private readonly bool _isLittleEndian; + private int _offset; + + public SpanSignatureReader(ReadOnlySpan blob, bool isLittleEndian = true) + { + _blob = blob; + _isLittleEndian = isLittleEndian; + _offset = 0; + } + + public int Remaining => _blob.Length - _offset; + + public byte ReadByte() + { + if (_offset >= _blob.Length) + throw new BadImageFormatException("Unexpected end of signature blob"); + return _blob[_offset++]; + } + + public byte PeekByte() + { + if (_offset >= _blob.Length) + throw new BadImageFormatException("Unexpected end of signature blob"); + return _blob[_offset]; + } + + public ulong ReadPointerSized(int pointerSize) + { + if (_offset + pointerSize > _blob.Length) + throw new BadImageFormatException("Unexpected end of signature blob"); + + ReadOnlySpan slice = _blob.Slice(_offset, pointerSize); + ulong val = pointerSize == 8 + ? (_isLittleEndian ? BinaryPrimitives.ReadUInt64LittleEndian(slice) : BinaryPrimitives.ReadUInt64BigEndian(slice)) + : (_isLittleEndian ? BinaryPrimitives.ReadUInt32LittleEndian(slice) : BinaryPrimitives.ReadUInt32BigEndian(slice)); + _offset += pointerSize; + return val; + } +} + +/// +/// Decodes method and local variable signatures, handling both standard ECMA-335 +/// types and runtime-internal types like ELEMENT_TYPE_INTERNAL (0x21). +/// +/// +/// +/// Handles the same ECMA-335 type codes as SRM's +/// , plus runtime-internal +/// types (ELEMENT_TYPE_INTERNAL 0x21 and ELEMENT_TYPE_CMOD_INTERNAL 0x22). +/// +/// +/// Internal custom modifiers (ELEMENT_TYPE_CMOD_INTERNAL) are skipped since +/// they carry runtime TypeHandle pointers that are not meaningful for type classification. +/// Standard custom modifiers (modreq/modopt) are decoded and dispatched +/// to . +/// +/// +internal ref struct RuntimeSignatureDecoder + where TReader : ISignatureReader, allows ref struct +{ + private const byte ELEMENT_TYPE_PTR = 0x0f; + private const byte ELEMENT_TYPE_BYREF = 0x10; + private const byte ELEMENT_TYPE_VALUETYPE = 0x11; + private const byte ELEMENT_TYPE_CLASS = 0x12; + private const byte ELEMENT_TYPE_VAR = 0x13; + private const byte ELEMENT_TYPE_ARRAY = 0x14; + private const byte ELEMENT_TYPE_GENERICINST = 0x15; + private const byte ELEMENT_TYPE_FNPTR = 0x1b; + private const byte ELEMENT_TYPE_SZARRAY = 0x1d; + private const byte ELEMENT_TYPE_MVAR = 0x1e; + private const byte ELEMENT_TYPE_CMOD_REQD = 0x1f; + private const byte ELEMENT_TYPE_CMOD_OPT = 0x20; + private const byte ELEMENT_TYPE_INTERNAL = 0x21; + private const byte ELEMENT_TYPE_CMOD_INTERNAL = 0x22; + private const byte ELEMENT_TYPE_SENTINEL = 0x41; + private const byte ELEMENT_TYPE_PINNED = 0x45; + + private readonly IRuntimeSignatureTypeProvider _provider; + private readonly MetadataReader? _metadataReader; + private readonly Target _target; + private readonly TGenericContext _genericContext; + private TReader _reader; + + public RuntimeSignatureDecoder( + IRuntimeSignatureTypeProvider provider, + Target target, + TGenericContext genericContext, + TReader reader, + MetadataReader? metadataReader = null) + { + _provider = provider; + _metadataReader = metadataReader; + _target = target; + _genericContext = genericContext; + _reader = reader; + } + + /// Decodes a method signature (MethodDefSig/MethodRefSig). + public MethodSignature DecodeMethodSignature() + { + byte rawHeader = _reader.ReadByte(); + SignatureHeader header = new(rawHeader); + + if (header.Kind is not SignatureKind.Method and not SignatureKind.Property) + throw new BadImageFormatException($"Unexpected signature header kind: {header.Kind}"); + + int genericParameterCount = 0; + if (header.IsGeneric) + genericParameterCount = ReadCompressedUInt(); + + int parameterCount = ReadCompressedUInt(); + if (parameterCount > _reader.Remaining) + throw new BadImageFormatException($"Parameter count {parameterCount} exceeds remaining signature bytes"); + TType returnType = DecodeType(); + + var parameterTypes = ImmutableArray.CreateBuilder(parameterCount); + int requiredParameterCount = parameterCount; + bool sentinelSeen = false; + + for (int i = 0; i < parameterCount; i++) + { + if (_reader.Remaining > 0 && _reader.PeekByte() == ELEMENT_TYPE_SENTINEL) + { + if (sentinelSeen) + throw new BadImageFormatException("Multiple sentinels in method signature"); + sentinelSeen = true; + requiredParameterCount = i; + _reader.ReadByte(); + } + parameterTypes.Add(DecodeType()); + } + + return new MethodSignature( + header, returnType, requiredParameterCount, genericParameterCount, + parameterTypes.MoveToImmutable()); + } + + /// Decodes a local variable signature (LocalVarSig). + public ImmutableArray DecodeLocalSignature() + { + byte header = _reader.ReadByte(); + if (header != 0x07) // IMAGE_CEE_CS_CALLCONV_LOCAL_SIG + throw new BadImageFormatException($"Expected LocalVarSig header (0x07), got 0x{header:X2}"); + + int count = ReadCompressedUInt(); + if (count == 0) + throw new BadImageFormatException("Local variable signature must have at least one entry"); + if (count > _reader.Remaining) + throw new BadImageFormatException($"Local count {count} exceeds remaining signature bytes"); + var locals = ImmutableArray.CreateBuilder(count); + for (int i = 0; i < count; i++) + locals.Add(DecodeType()); + return locals.MoveToImmutable(); + } + + /// Decodes a single type embedded in a signature. + public TType DecodeType() + { + // Handle custom modifiers (standard and internal) + while (_reader.Remaining > 0) + { + byte peek = _reader.PeekByte(); + if (peek is ELEMENT_TYPE_CMOD_REQD or ELEMENT_TYPE_CMOD_OPT) + { + bool isRequired = peek == ELEMENT_TYPE_CMOD_REQD; + _reader.ReadByte(); + TType modifier = DecodeTypeDefOrRefOrSpec(0); + TType unmodifiedType = DecodeType(); + return _provider.GetModifiedType(modifier, unmodifiedType, isRequired); + } + else if (peek == ELEMENT_TYPE_CMOD_INTERNAL) + { + _reader.ReadByte(); + bool isRequired = _reader.ReadByte() != 0; + ulong val = _reader.ReadPointerSized(_target.PointerSize); + TType unmodifiedType = DecodeType(); + return _provider.GetInternalModifiedType( + _target, new TargetPointer(val), unmodifiedType, isRequired); + } + else + { + break; + } + } + + byte typeCode = _reader.ReadByte(); + + switch (typeCode) + { + case (byte)SignatureTypeCode.Boolean: + case (byte)SignatureTypeCode.Char: + case (byte)SignatureTypeCode.SByte: + case (byte)SignatureTypeCode.Byte: + case (byte)SignatureTypeCode.Int16: + case (byte)SignatureTypeCode.UInt16: + case (byte)SignatureTypeCode.Int32: + case (byte)SignatureTypeCode.UInt32: + case (byte)SignatureTypeCode.Int64: + case (byte)SignatureTypeCode.UInt64: + case (byte)SignatureTypeCode.Single: + case (byte)SignatureTypeCode.Double: + case (byte)SignatureTypeCode.IntPtr: + case (byte)SignatureTypeCode.UIntPtr: + case (byte)SignatureTypeCode.Object: + case (byte)SignatureTypeCode.String: + case (byte)SignatureTypeCode.Void: + case (byte)SignatureTypeCode.TypedReference: + return _provider.GetPrimitiveType((PrimitiveTypeCode)typeCode); + + case ELEMENT_TYPE_CLASS: + case ELEMENT_TYPE_VALUETYPE: + return DecodeTypeDefOrRefOrSpec(typeCode); + + case ELEMENT_TYPE_PTR: + return _provider.GetPointerType(DecodeType()); + + case ELEMENT_TYPE_BYREF: + return _provider.GetByReferenceType(DecodeType()); + + case ELEMENT_TYPE_SZARRAY: + return _provider.GetSZArrayType(DecodeType()); + + case ELEMENT_TYPE_ARRAY: + { + TType elementType = DecodeType(); + ArrayShape shape = DecodeArrayShape(); + return _provider.GetArrayType(elementType, shape); + } + + case ELEMENT_TYPE_GENERICINST: + { + TType baseType = DecodeType(); + int count = ReadCompressedUInt(); + if (count == 0) + throw new BadImageFormatException("Generic instantiation must have at least one type argument"); + if (count > _reader.Remaining) + throw new BadImageFormatException($"Generic argument count {count} exceeds remaining signature bytes"); + var args = ImmutableArray.CreateBuilder(count); + for (int i = 0; i < count; i++) + args.Add(DecodeType()); + return _provider.GetGenericInstantiation(baseType, args.MoveToImmutable()); + } + + case ELEMENT_TYPE_VAR: + return _provider.GetGenericTypeParameter(_genericContext, ReadCompressedUInt()); + + case ELEMENT_TYPE_MVAR: + return _provider.GetGenericMethodParameter(_genericContext, ReadCompressedUInt()); + + case ELEMENT_TYPE_FNPTR: + { + MethodSignature fnSig = DecodeMethodSignature(); + return _provider.GetFunctionPointerType(fnSig); + } + + case ELEMENT_TYPE_PINNED: + return _provider.GetPinnedType(DecodeType()); + + case ELEMENT_TYPE_INTERNAL: + { + ulong val = _reader.ReadPointerSized(_target.PointerSize); + return _provider.GetInternalType(_target, new TargetPointer(val)); + } + + default: + throw new BadImageFormatException($"Unexpected signature type code: 0x{typeCode:X2}"); + } + } + + /// + /// Decodes a TypeDefOrRefOrSpecEncoded token (ECMA-335 II.23.2.8). + /// The compressed value encodes tag in the low 2 bits and RID in the upper bits. + /// + private TType DecodeTypeDefOrRefOrSpec(byte rawTypeKind) + { + int coded = ReadCompressedUInt(); + int tag = coded & 0x3; + int rid = coded >> 2; + + if (rid == 0) + throw new BadImageFormatException("Nil TypeDefOrRefOrSpecEncoded handle in signature"); + + if (rid > 0x00FFFFFF) + throw new BadImageFormatException($"TypeDefOrRefOrSpecEncoded RID out of range: {rid}"); + + return tag switch + { + 0 => _provider.GetTypeFromDefinition(_metadataReader!, MetadataTokens.TypeDefinitionHandle(rid), rawTypeKind), + 1 => _provider.GetTypeFromReference(_metadataReader!, MetadataTokens.TypeReferenceHandle(rid), rawTypeKind), + 2 => _provider.GetTypeFromSpecification(_metadataReader!, _genericContext, MetadataTokens.TypeSpecificationHandle(rid), rawTypeKind), + _ => _provider.GetPrimitiveType(PrimitiveTypeCode.Object), // tag=3 is BaseType in native + }; + } + + private ArrayShape DecodeArrayShape() + { + int rank = ReadCompressedUInt(); + int numSizes = ReadCompressedUInt(); + if (numSizes > _reader.Remaining) + throw new BadImageFormatException($"Array size count {numSizes} exceeds remaining signature bytes"); + var sizes = ImmutableArray.CreateBuilder(numSizes); + for (int i = 0; i < numSizes; i++) + sizes.Add(ReadCompressedUInt()); + int numLoBounds = ReadCompressedUInt(); + if (numLoBounds > _reader.Remaining) + throw new BadImageFormatException($"Array lower bound count {numLoBounds} exceeds remaining signature bytes"); + var loBounds = ImmutableArray.CreateBuilder(numLoBounds); + for (int i = 0; i < numLoBounds; i++) + loBounds.Add(ReadCompressedSignedInt()); + return new ArrayShape(rank, sizes.MoveToImmutable(), loBounds.MoveToImmutable()); + } + + /// + /// Reads a compressed unsigned integer per ECMA-335 II.23.2. + /// + private int ReadCompressedUInt() + { + byte first = _reader.ReadByte(); + if ((first & 0x80) == 0) + return first; + if ((first & 0xC0) == 0x80) + return ((first & 0x3F) << 8) | _reader.ReadByte(); + if ((first & 0xE0) == 0xC0) + return ((first & 0x1F) << 24) | (_reader.ReadByte() << 16) | (_reader.ReadByte() << 8) | _reader.ReadByte(); + + throw new BadImageFormatException("Invalid compressed integer encoding"); + } + + /// + /// Reads a compressed signed integer per ECMA-335 II.23.2. + /// Uses sign extension based on encoded width, matching SRM's BlobReader.ReadCompressedSignedInteger. + /// + private int ReadCompressedSignedInt() + { + byte first = _reader.ReadByte(); + + if ((first & 0x80) == 0) + { + // 1-byte: 7 bits, sign bit is bit 0 of the encoded value + int value = first >> 1; + return (first & 1) != 0 ? value - 0x40 : value; + } + + if ((first & 0xC0) == 0x80) + { + // 2-byte: 14 bits + int raw = ((first & 0x3F) << 8) | _reader.ReadByte(); + int value = raw >> 1; + return (raw & 1) != 0 ? value - 0x2000 : value; + } + + if ((first & 0xE0) == 0xC0) + { + // 4-byte: 29 bits + int raw = ((first & 0x1F) << 24) | (_reader.ReadByte() << 16) | (_reader.ReadByte() << 8) | _reader.ReadByte(); + int value = raw >> 1; + return (raw & 1) != 0 ? value - 0x10000000 : value; + } + + throw new BadImageFormatException("Invalid compressed signed integer encoding"); + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs index e7a3222806464b..1b4fef66afe058 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs @@ -62,6 +62,19 @@ private class StackWalkData(IPlatformAgnosticContext context, StackWalkState sta // set back to true when encountering a ResumableFrame (FRAME_ATTR_RESUMABLE). public bool IsFirst { get; set; } = true; + // Track isInterrupted like native CrawlFrame::isInterrupted. + // Set in UpdateState when transitioning to SW_FRAMELESS after processing a Frame + // with FRAME_ATTR_EXCEPTION (e.g., FaultingExceptionFrame). When true, the managed + // frame reached via that Frame's return address was interrupted by an exception, + // and EnumGcRefs should use ExecutionAborted to skip live slot reporting at + // non-interruptible offsets. + public bool IsInterrupted { get; set; } + + // The frame type of the last SW_FRAME processed by Next(). + // Used by UpdateState to detect exception frames (FRAME_ATTR_EXCEPTION) and + // set IsInterrupted when transitioning to a managed frame. + public FrameIterator.FrameType? LastProcessedFrameType { get; set; } + public bool IsCurrentFrameResumable() { if (State is not (StackWalkState.SW_FRAME or StackWalkState.SW_SKIPPED_FRAME)) @@ -71,9 +84,10 @@ public bool IsCurrentFrameResumable() // Only frame types with FRAME_ATTR_RESUMABLE set isFirst=true. // FaultingExceptionFrame has FRAME_ATTR_FAULTED (sets hasFaulted) // but NOT FRAME_ATTR_RESUMABLE, so it must not be included here. - // TODO: HijackFrame only has FRAME_ATTR_RESUMABLE on non-x86 platforms. - // When x86 stack walking is supported, this should be conditioned on - // the target architecture. + // Note: HijackFrame only has FRAME_ATTR_RESUMABLE on non-x86 platforms + // (see frames.h). On x86 it uses GcScanRoots_Impl instead of the + // resumable frame pattern. When x86 cDAC stack walking is supported, + // HijackFrame should be conditioned on the target architecture. return ft is FrameIterator.FrameType.ResumableFrame or FrameIterator.FrameType.RedirectedThreadFrame or FrameIterator.FrameType.HijackFrame; @@ -81,9 +95,11 @@ or FrameIterator.FrameType.RedirectedThreadFrame /// /// Update the IsFirst state for the NEXT frame, matching native stackwalk.cpp: - /// - After a frameless frame: isFirst = false (line 2202) - /// - After a ResumableFrame: isFirst = true (line 2235) - /// - After other Frames: isFirst = false (implicit in line 2235 assignment) + /// - After a frameless frame: isFirst = false + /// - After a ResumableFrame: isFirst = true + /// - After other Frames: isFirst = false + /// - After a skipped frame: isFirst unchanged (native never modifies isFirst + /// in the SFITER_SKIPPED_FRAME_FUNCTION path — it keeps the value from Init) /// public void AdvanceIsFirst() { @@ -91,6 +107,14 @@ public void AdvanceIsFirst() { IsFirst = false; } + else if (State == StackWalkState.SW_SKIPPED_FRAME) + { + // Native SFITER_SKIPPED_FRAME_FUNCTION (stackwalk.cpp:2086-2128) does NOT + // modify isFirst. It stays true from Init() so the subsequent managed frame + // gets IsActiveFunc()=true. This is important because skipped frames are + // explicit Frames embedded within the active managed frame (e.g. InlinedCallFrame + // from PInvoke), and the managed frame should still be treated as the leaf. + } else { IsFirst = IsCurrentFrameResumable(); @@ -106,47 +130,12 @@ public StackDataFrameHandle ToDataFrame() } IEnumerable IStackWalk.CreateStackWalk(ThreadData threadData) - => CreateStackWalkCore(threadData, skipInitialFrames: false); - - /// - /// Core stack walk implementation. - /// - /// Thread to walk. - /// - /// When true, pre-advances the FrameIterator past explicit Frames below the initial - /// managed frame's caller SP. This matches the native DacStackReferenceWalker behavior - /// for GC reference enumeration, where these frames are within the current managed - /// frame's stack range and don't contribute additional GC roots. - /// - /// Must be false for ClrDataStackWalk, which advances the cDAC and legacy DAC in - /// lockstep and must yield the same frame sequence (including initial skipped frames). - /// - private IEnumerable CreateStackWalkCore(ThreadData threadData, bool skipInitialFrames) { IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); FillContextFromThread(context, threadData); StackWalkState state = IsManaged(context.InstructionPointer, out _) ? StackWalkState.SW_FRAMELESS : StackWalkState.SW_FRAME; FrameIterator frameIterator = new(_target, threadData); - if (skipInitialFrames) - { - TargetPointer skipBelowSP; - if (state == StackWalkState.SW_FRAMELESS) - { - IPlatformAgnosticContext callerCtx = context.Clone(); - callerCtx.Unwind(_target); - skipBelowSP = callerCtx.StackPointer; - } - else - { - skipBelowSP = context.StackPointer; - } - while (frameIterator.IsValid() && frameIterator.CurrentFrameAddress.Value < skipBelowSP.Value) - { - frameIterator.Next(); - } - } - // if the next Frame is not valid and we are not in managed code, there is nothing to return if (state == StackWalkState.SW_FRAME && !frameIterator.IsValid()) { @@ -158,7 +147,7 @@ private IEnumerable CreateStackWalkCore(ThreadData thread // Mirror native Init() -> ProcessCurrentFrame() -> CheckForSkippedFrames(): // When the initial frame is managed (SW_FRAMELESS), check if there are explicit // Frames below the caller SP that should be reported first. The native walker - // yields skipped frames BEFORE the containing managed frame on non-x86. + // yields skipped frames BEFORE the containing managed frame. if (state == StackWalkState.SW_FRAMELESS && CheckForSkippedFrames(stackWalkData)) { stackWalkData.State = StackWalkState.SW_SKIPPED_FRAME; @@ -176,18 +165,32 @@ private IEnumerable CreateStackWalkCore(ThreadData thread IReadOnlyList IStackWalk.WalkStackReferences(ThreadData threadData) { - IEnumerable stackFrames = CreateStackWalkCore(threadData, skipInitialFrames: true); - IEnumerable frames = stackFrames.Select(AssertCorrectHandle); - IEnumerable gcFrames = Filter(frames); + // Initialize the walk data directly + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); + FillContextFromThread(context, threadData); + StackWalkState state = IsManaged(context.InstructionPointer, out _) ? StackWalkState.SW_FRAMELESS : StackWalkState.SW_FRAME; + FrameIterator frameIterator = new(_target, threadData); + + if (state == StackWalkState.SW_FRAME && !frameIterator.IsValid()) + return []; + + StackWalkData walkData = new(context, state, frameIterator, threadData); + + // Mirror native Init() -> ProcessCurrentFrame() -> CheckForSkippedFrames(): + // When the initial frame is managed (SW_FRAMELESS), check if there are explicit + // Frames below the caller SP that should be reported first. The native walker + // yields skipped frames BEFORE the containing managed frame. + if (walkData.State == StackWalkState.SW_FRAMELESS && CheckForSkippedFrames(walkData)) + walkData.State = StackWalkState.SW_SKIPPED_FRAME; GcScanContext scanContext = new(_target, resolveInteriorPointers: false); - foreach (GCFrameData gcFrame in gcFrames) + // Filter drives Next() directly, matching native Filter()+NextRaw() integration. + // This prevents funclet-to-parent transitions from re-visiting already-walked frames. + foreach (GCFrameData gcFrame in Filter(walkData)) { try { - _ = ((IStackWalk)this).GetMethodDescPtr(gcFrame.Frame); - bool reportGcReferences = gcFrame.ShouldCrawlFrameReportGCReferences; TargetPointer pFrame = ((IStackWalk)this).GetFrameAddress(gcFrame.Frame); @@ -207,22 +210,45 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre ? CodeManagerFlags.ActiveStackFrame : 0; + // If the frame was interrupted by an exception (reached via a + // FaultingExceptionFrame), set ExecutionAborted so the GcInfoDecoder + // skips live slot reporting at non-interruptible offsets. This matches + // native CrawlFrame::GetCodeManagerFlags (stackwalk.h). + if (gcFrame.IsInterrupted) + codeManagerFlags |= CodeManagerFlags.ExecutionAborted; + if (gcFrame.ShouldParentToFuncletSkipReportingGCReferences) codeManagerFlags |= CodeManagerFlags.ParentOfFuncletStackFrame; - // TODO: When ShouldParentFrameUseUnwindTargetPCforGCReporting is set, - // use FindFirstInterruptiblePoint on the catch handler clause range - // to override the relOffset for GC liveness lookup. This mirrors - // native gcenv.ee.common.cpp behavior for catch-handler resumption. + uint? relOffsetOverride = null; + if (gcFrame.ShouldParentFrameUseUnwindTargetPCforGCReporting) + { + _eman.GetGCInfo(cbh.Value, out TargetPointer gcInfoAddr, out uint gcVersion); + IGCInfoHandle gcHandle = _target.Contracts.GCInfo.DecodePlatformSpecificGCInfo(gcInfoAddr, gcVersion); + uint startPC = gcFrame.ClauseForCatchHandlerStartPC; + uint endPC = gcFrame.ClauseForCatchHandlerEndPC; + foreach (var range in _target.Contracts.GCInfo.GetInterruptibleRanges(gcHandle)) + { + if (range.EndOffset <= startPC) + continue; + if (startPC >= range.StartOffset && startPC < range.EndOffset) + { + relOffsetOverride = startPC; + break; + } + if (range.StartOffset < endPC) + { + relOffsetOverride = range.StartOffset; + break; + } + } + } - GcScanner gcScanner = new(_target); - gcScanner.EnumGcRefs(gcFrame.Frame.Context, cbh.Value, codeManagerFlags, scanContext); + EnumGcRefsForManagedFrame(gcFrame.Frame.Context, cbh.Value, codeManagerFlags, scanContext, relOffsetOverride); } else { - // TODO: Frame-based GC root scanning (ScanFrameRoots) not yet implemented. - // Frames that call PromoteCallerStack (StubDispatchFrame, ExternalMethodFrame, - // DynamicHelperFrame, etc.) will be handled in a follow-up PR. + walkData.FrameIter.GcScanRoots(gcFrame.Frame.FrameAddress, scanContext); } } } @@ -260,11 +286,25 @@ public GCFrameData(StackDataFrameHandle frame) public bool ShouldParentToFuncletSkipReportingGCReferences { get; set; } public bool ShouldCrawlFrameReportGCReferences { get; set; } // required public bool ShouldParentFrameUseUnwindTargetPCforGCReporting { get; set; } + public uint ClauseForCatchHandlerStartPC { get; set; } + public uint ClauseForCatchHandlerEndPC { get; set; } + // Set when the frame was reached via an exception Frame (FRAME_ATTR_EXCEPTION). + // Causes ExecutionAborted to be passed to EnumGcRefs. + public bool IsInterrupted { get; set; } } - private IEnumerable Filter(IEnumerable handles) + /// + /// Port of native StackFrameIterator::Filter (GC_FUNCLET_REFERENCE_REPORTING mode). + /// Unlike the previous implementation that passively consumed pre-generated frames, + /// this version drives Next() directly — matching native Filter() which calls NextRaw() + /// internally to skip frames. This prevents funclet-to-parent transitions from + /// re-visiting already-walked frames. + /// +#pragma warning disable IDE0059 // Unnecessary assignment — false positives from goto case + do/while pattern + private IEnumerable Filter(StackWalkData walkData) { - // StackFrameIterator::Filter assuming GC_FUNCLET_REFERENCE_REPORTING is defined + // Process the initial frame, then loop calling Next() for subsequent frames. + // This matches native: Init() produces the first frame, then Filter()+NextRaw() loop. // global tracking variables bool processNonFilterFunclet = false; @@ -272,11 +312,19 @@ private IEnumerable Filter(IEnumerable handle bool didFuncletReportGCReferences = true; TargetPointer parentStackFrame = TargetPointer.Null; TargetPointer funcletParentStackFrame = TargetPointer.Null; - TargetPointer intermediaryFuncletParentStackFrame; + TargetPointer intermediaryFuncletParentStackFrame = TargetPointer.Null; - foreach (StackDataFrameHandle handle in handles) + // Process the initial frame, then advance with Next() + bool isValid = walkData.State is not (StackWalkState.SW_ERROR or StackWalkState.SW_COMPLETE); + while (isValid) { - GCFrameData gcFrame = new(handle); + StackDataFrameHandle handle = walkData.ToDataFrame(); + walkData.AdvanceIsFirst(); + + GCFrameData gcFrame = new(handle) + { + IsInterrupted = walkData.IsInterrupted, + }; // per-frame tracking variables bool stop = false; @@ -494,6 +542,9 @@ private IEnumerable Filter(IEnumerable handle didFuncletReportGCReferences = true; gcFrame.ShouldParentFrameUseUnwindTargetPCforGCReporting = true; + + gcFrame.ClauseForCatchHandlerStartPC = exInfo.ClauseForCatchHandlerStartPC; + gcFrame.ClauseForCatchHandlerEndPC = exInfo.ClauseForCatchHandlerEndPC; } else if (!IsFunclet(handle)) { @@ -568,8 +619,14 @@ private IEnumerable Filter(IEnumerable handle if (stop) yield return gcFrame; + + // Advance the iterator - matching native Filter() calling NextRaw() + // When a frame was skipped (stop=false), this advances past it. + // When a frame was yielded (stop=true), this advances to the next frame. + isValid = Next(walkData); } } +#pragma warning restore IDE0059 private bool IsUnwoundToTargetParentFrame(StackDataFrameHandle handle, TargetPointer targetParentFrame) { @@ -586,6 +643,18 @@ private bool Next(StackWalkData handle) switch (handle.State) { case StackWalkState.SW_FRAMELESS: + // Native assertion (stackwalk.cpp): current SP must be below the next Frame. + // FaultingExceptionFrame is a special case where it gets pushed after the frame is running. + Debug.Assert( + !handle.FrameIter.IsValid() || + handle.Context.StackPointer.Value < handle.FrameIter.CurrentFrameAddress.Value || + handle.FrameIter.GetCurrentFrameType() == FrameIterator.FrameType.FaultingExceptionFrame, + $"SP (0x{handle.Context.StackPointer:X}) should be below next Frame (0x{handle.FrameIter.CurrentFrameAddress:X})"); + + // Reset interrupted state after processing a managed frame. + // Native stackwalk.cpp:2203-2205: isInterrupted = false; hasFaulted = false; + handle.IsInterrupted = false; + try { handle.Context.Unwind(_target); @@ -597,13 +666,33 @@ private bool Next(StackWalkData handle) } break; case StackWalkState.SW_SKIPPED_FRAME: + // Advance past the skipped frame, then let UpdateState detect + // whether there are more skipped frames or we've reached the managed method. handle.FrameIter.Next(); break; case StackWalkState.SW_FRAME: - handle.FrameIter.UpdateContextFromFrame(handle.Context); - if (!handle.FrameIter.IsInlineCallFrameWithActiveCall()) + // Native SFITER_FRAME_FUNCTION gates ProcessIp + UpdateRegDisplay on + // GetReturnAddress() != 0, and gates GotoNextFrame on !pInlinedFrame. + // pInlinedFrame is set only for active InlinedCallFrames. { - handle.FrameIter.Next(); + var frameType = handle.FrameIter.GetCurrentFrameType(); + + TargetPointer returnAddress = handle.FrameIter.GetReturnAddress(); + bool isActiveICF = frameType == FrameIterator.FrameType.InlinedCallFrame + && returnAddress != TargetPointer.Null; + + // Record the frame type so UpdateState can detect exception frames + // and set IsInterrupted when transitioning to the managed frame. + handle.LastProcessedFrameType = frameType; + + if (returnAddress != TargetPointer.Null) + { + handle.FrameIter.UpdateContextFromFrame(handle.Context); + } + if (!isActiveICF) + { + handle.FrameIter.Next(); + } } break; case StackWalkState.SW_ERROR: @@ -629,6 +718,18 @@ private void UpdateState(StackWalkData handle) if (isManaged) { handle.State = StackWalkState.SW_FRAMELESS; + + // Detect exception frames (FRAME_ATTR_EXCEPTION) when transitioning to managed. + // Both FaultingExceptionFrame (hardware) and SoftwareExceptionFrame (managed throw) + // have FRAME_ATTR_EXCEPTION set. The resulting managed frame gets ExecutionAborted, + // causing GcInfoDecoder to skip live slot reporting at non-interruptible offsets. + if (handle.LastProcessedFrameType is FrameIterator.FrameType.FaultingExceptionFrame + or FrameIterator.FrameType.SoftwareExceptionFrame) + { + handle.IsInterrupted = true; + } + handle.LastProcessedFrameType = null; + if (CheckForSkippedFrames(handle)) { handle.State = StackWalkState.SW_SKIPPED_FRAME; @@ -707,15 +808,17 @@ TargetPointer IStackWalk.GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHa // 4) the return address method has a MDContext arg bool reportInteropMD = false; - if (FrameIterator.IsInlinedCallFrame(_target, framePtr) && + Data.Frame frameData = _target.ProcessedData.GetOrAdd(framePtr); + FrameIterator.FrameType frameType = FrameIterator.GetFrameType(_target, frameData.Identifier); + + if (frameType == FrameIterator.FrameType.InlinedCallFrame && handle.State == StackWalkState.SW_SKIPPED_FRAME) { IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; - // FrameIterator.GetReturnAddress is currently only implemented for InlinedCallFrame - // This is fine as this check is only needed for that frame type - TargetPointer returnAddress = FrameIterator.GetReturnAddress(_target, framePtr); - if (_eman.GetCodeBlockHandle(returnAddress.Value) is CodeBlockHandle cbh) + Data.InlinedCallFrame icf = _target.ProcessedData.GetOrAdd(framePtr); + TargetPointer returnAddress = icf.CallerReturnAddress; + if (returnAddress != TargetPointer.Null && _eman.GetCodeBlockHandle(returnAddress.Value) is CodeBlockHandle cbh) { MethodDescHandle returnMethodDesc = rts.GetMethodDescHandle(_eman.GetMethodDesc(cbh)); reportInteropMD = rts.HasMDContextArg(returnMethodDesc); @@ -802,4 +905,85 @@ private static StackDataFrameHandle AssertCorrectHandle(IStackDataFrameHandle st return handle; } + + /// + /// Enumerates live GC slots for a managed (frameless) code frame. + /// Port of native EECodeManager::EnumGcRefs (eetwain.cpp). + /// + private void EnumGcRefsForManagedFrame( + IPlatformAgnosticContext context, + CodeBlockHandle cbh, + CodeManagerFlags flags, + GcScanContext scanContext, + uint? relOffsetOverride = null) + { + TargetNUInt relativeOffset = _eman.GetRelativeOffset(cbh); + _eman.GetGCInfo(cbh, out TargetPointer gcInfoAddr, out uint gcVersion); + + if (_eman.IsFilterFunclet(cbh)) + flags |= CodeManagerFlags.NoReportUntracked; + + IGCInfoHandle handle = _target.Contracts.GCInfo.DecodePlatformSpecificGCInfo(gcInfoAddr, gcVersion); + if (handle is not IGCInfoDecoder decoder) + return; + + uint stackBaseRegister = decoder.StackBaseRegister; + TargetPointer? callerSP = null; + uint offsetToUse = relOffsetOverride ?? (uint)relativeOffset.Value; + + decoder.EnumerateLiveSlots( + offsetToUse, + flags, + (bool isRegister, uint registerNumber, int spOffset, uint spBase, uint gcFlags) => + { + GcScanFlags scanFlags = GcScanFlags.None; + if ((gcFlags & 0x1) != 0) + scanFlags |= GcScanFlags.GC_CALL_INTERIOR; + if ((gcFlags & 0x2) != 0) + scanFlags |= GcScanFlags.GC_CALL_PINNED; + + if (isRegister) + { + if (!context.TryReadRegister((int)registerNumber, out TargetNUInt regValue)) + return; + GcScanSlotLocation loc = new((int)registerNumber, 0, false); + scanContext.GCEnumCallback(new TargetPointer(regValue.Value), scanFlags, loc); + } + else + { + int spReg = context.StackPointerRegister; + int reg = spBase switch + { + 1 => spReg, + 2 => (int)stackBaseRegister, + 0 => -(spReg + 1), + _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), + }; + TargetPointer baseAddr = spBase switch + { + 1 => context.StackPointer, + 2 => context.TryReadRegister((int)stackBaseRegister, out TargetNUInt val) + ? new TargetPointer(val.Value) + : throw new InvalidOperationException($"Failed to read register {stackBaseRegister}"), + 0 => GetCallerSP(context, ref callerSP), + _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), + }; + + TargetPointer addr = new(baseAddr.Value + (ulong)(long)spOffset); + GcScanSlotLocation loc = new(reg, spOffset, true); + scanContext.GCEnumCallback(addr, scanFlags, loc); + } + }); + } + + private TargetPointer GetCallerSP(IPlatformAgnosticContext context, ref TargetPointer? cached) + { + if (cached is null) + { + IPlatformAgnosticContext callerContext = context.Clone(); + callerContext.Unwind(_target); + cached = callerContext.StackPointer; + } + return cached.Value; + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs index c8d30ded52e678..d582523b5159ec 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs @@ -23,6 +23,8 @@ public ExceptionInfo(Target target, TargetPointer address) CSFEHClause = target.ReadPointerField(address, type, nameof(CSFEHClause)); CSFEnclosingClause = target.ReadPointerField(address, type, nameof(CSFEnclosingClause)); CallerOfActualHandlerFrame = target.ReadPointerField(address, type, nameof(CallerOfActualHandlerFrame)); + ClauseForCatchHandlerStartPC = target.ReadField(address, type, nameof(ClauseForCatchHandlerStartPC)); + ClauseForCatchHandlerEndPC = target.ReadField(address, type, nameof(ClauseForCatchHandlerEndPC)); } public TargetPointer PreviousNestedInfo { get; } @@ -35,4 +37,6 @@ public ExceptionInfo(Target target, TargetPointer address) public TargetPointer CSFEHClause { get; } public TargetPointer CSFEnclosingClause { get; } public TargetPointer CallerOfActualHandlerFrame { get; } + public uint ClauseForCatchHandlerStartPC { get; } + public uint ClauseForCatchHandlerEndPC { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DynamicHelperFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DynamicHelperFrame.cs new file mode 100644 index 00000000000000..625c616d42616e --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DynamicHelperFrame.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal class DynamicHelperFrame : IData +{ + static DynamicHelperFrame IData.Create(Target target, TargetPointer address) + => new DynamicHelperFrame(target, address); + + public DynamicHelperFrame(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.DynamicHelperFrame); + DynamicHelperFrameFlags = target.ReadField(address, type, nameof(DynamicHelperFrameFlags)); + } + + public int DynamicHelperFrameFlags { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs new file mode 100644 index 00000000000000..cfc3e92be93297 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal class ExternalMethodFrame : IData +{ + static ExternalMethodFrame IData.Create(Target target, TargetPointer address) + => new ExternalMethodFrame(target, address); + + public ExternalMethodFrame(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.ExternalMethodFrame); + GCRefMap = target.ReadPointerField(address, type, nameof(GCRefMap)); + Indirection = target.ReadPointerField(address, type, nameof(Indirection)); + ZapModule = target.ReadPointerField(address, type, nameof(ZapModule)); + } + + public TargetPointer GCRefMap { get; } + public TargetPointer Indirection { get; } + public TargetPointer ZapModule { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs index c49f6919353255..da2e1a493f602b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs @@ -14,6 +14,9 @@ public StubDispatchFrame(Target target, TargetPointer address) MethodDescPtr = target.ReadPointerField(address, type, nameof(MethodDescPtr)); RepresentativeMTPtr = target.ReadPointerField(address, type, nameof(RepresentativeMTPtr)); RepresentativeSlot = target.ReadField(address, type, nameof(RepresentativeSlot)); + GCRefMap = target.ReadPointerField(address, type, nameof(GCRefMap)); + ZapModule = target.ReadPointerField(address, type, nameof(ZapModule)); + Indirection = target.ReadPointerField(address, type, nameof(Indirection)); Address = address; } @@ -21,4 +24,7 @@ public StubDispatchFrame(Target target, TargetPointer address) public TargetPointer MethodDescPtr { get; } public TargetPointer RepresentativeMTPtr { get; } public uint RepresentativeSlot { get; } + public TargetPointer GCRefMap { get; } + public TargetPointer ZapModule { get; } + public TargetPointer Indirection { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs index b2c0c71cb47ef9..e727286cc7de06 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs @@ -18,6 +18,12 @@ public TransitionBlock(Target target, TargetPointer address) { ArgumentRegisters = address + (ulong)type.Fields[nameof(ArgumentRegisters)].Offset; } + + // These are offsets relative to the TransitionBlock pointer, stored as field "offsets" + // in the data descriptor. They represent computed layout positions, not actual memory reads. + FirstGCRefMapSlot = (uint)type.Fields[nameof(FirstGCRefMapSlot)].Offset; + ArgumentRegistersOffset = (uint)type.Fields[nameof(ArgumentRegistersOffset)].Offset; + OffsetOfFloatArgumentRegisters = type.Fields[nameof(OffsetOfFloatArgumentRegisters)].Offset; } public TargetPointer ReturnAddress { get; } @@ -27,4 +33,21 @@ public TransitionBlock(Target target, TargetPointer address) /// Only available on ARM targets. /// public TargetPointer? ArgumentRegisters { get; } + + /// + /// Offset to the first slot covered by the GCRefMap, relative to the TransitionBlock pointer. + /// + public uint FirstGCRefMapSlot { get; } + + /// + /// Offset to the argument registers area, relative to the TransitionBlock pointer. + /// + public uint ArgumentRegistersOffset { get; } + + /// + /// Offset to the float argument registers area, relative to the TransitionBlock pointer. + /// Negative on most platforms (float regs are stored before the TransitionBlock). + /// Zero on platforms without float argument registers (x86, Windows x64). + /// + public int OffsetOfFloatArgumentRegisters { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs index 3241fa45b965a0..6557ee7aa99a1c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs @@ -33,6 +33,11 @@ public ReadyToRunInfo(Target target, TargetPointer address) DebugInfoSection = target.ReadPointerField(address, type, nameof(DebugInfoSection)); ExceptionInfoSection = target.ReadPointerField(address, type, nameof(ExceptionInfoSection)); + NumImportSections = target.Read(address + (ulong)type.Fields[nameof(NumImportSections)].Offset); + ImportSections = NumImportSections > 0 + ? target.ReadPointer(address + (ulong)type.Fields[nameof(ImportSections)].Offset) + : TargetPointer.Null; + // Map is from the composite info pointer (set to itself for non-multi-assembly composite images) EntryPointToMethodDescMap = CompositeInfo + (ulong)type.Fields[nameof(EntryPointToMethodDescMap)].Offset; LoadedImageBase = target.ReadPointerField(address, type, nameof(LoadedImageBase)); @@ -55,4 +60,6 @@ public ReadyToRunInfo(Target target, TargetPointer address) public TargetPointer EntryPointToMethodDescMap { get; } public TargetPointer LoadedImageBase { get; } public TargetPointer Composite { get; } + public uint NumImportSections { get; } + public TargetPointer ImportSections { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs index d67adff3ab3489..3eb7727d3d3fc2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -4034,13 +4034,69 @@ int ISOSEnum.GetCount(uint* pCount) int ISOSDacInterface.GetStackReferences(int osThreadID, DacComNullableByRef ppEnum) { - // Stack reference enumeration is not yet complete in the cDAC — capital-F Frame - // GC root scanning (ScanFrameRoots) is still pending. Fall through to the legacy - // DAC so that consumers (dump tests, SOS) continue to work while the implementation - // is in progress. - return _legacyImpl is not null - ? _legacyImpl.GetStackReferences(osThreadID, ppEnum) - : HResults.E_NOTIMPL; + int hr = HResults.S_OK; + try + { + IThread threadContract = _target.Contracts.Thread; + IStackWalk stackWalkContract = _target.Contracts.StackWalk; + ThreadData? matchingThread = null; + + ThreadStoreData threadStore = threadContract.GetThreadStoreData(); + TargetPointer threadAddr = threadStore.FirstThread; + while (threadAddr != TargetPointer.Null) + { + ThreadData td = threadContract.GetThreadData(threadAddr); + if (td.OSId.Value == (ulong)osThreadID) + { + matchingThread = td; + break; + } + threadAddr = td.NextThread; + } + + if (matchingThread is null) + { + return HResults.E_INVALIDARG; + } + + IReadOnlyList refs = stackWalkContract.WalkStackReferences(matchingThread.Value); + + SOSStackRefData[] sosRefs = new SOSStackRefData[refs.Count]; + for (int i = 0; i < refs.Count; i++) + { + sosRefs[i] = new SOSStackRefData + { + HasRegisterInformation = refs[i].HasRegisterInformation ? 1 : 0, + Register = refs[i].Register, + Offset = refs[i].Offset, + Address = refs[i].Address.Value, + Object = refs[i].Object.Value, + Flags = refs[i].Flags, + Source = refs[i].Source.Value, + SourceType = refs[i].IsStackSourceFrame + ? SOSStackSourceType.SOS_StackSourceFrame + : SOSStackSourceType.SOS_StackSourceIP, + StackPointer = refs[i].StackPointer.Value, + }; + } + + ppEnum.Interface = new SOSStackRefEnum(sosRefs); + } + catch (System.Exception) + { + hr = HResults.E_FAIL; + } +#if DEBUG + if (_legacyImpl is not null) + { + // Validate that the legacy DAC produces the same HResult. + // We pass isNullRef: false to request actual enumeration, but we don't + // compare individual refs — that's done by cdacstress.cpp at runtime. + int hrLocal = _legacyImpl.GetStackReferences(osThreadID, new DacComNullableByRef(isNullRef: false)); + Debug.ValidateHResult(hr, hrLocal); + } +#endif + return hr; } int ISOSDacInterface.GetStressLogAddress(ClrDataAddress* stressLog) diff --git a/src/native/managed/cdac/cdac.slnx b/src/native/managed/cdac/cdac.slnx index 7449d30624ec2d..3243195e0855eb 100644 --- a/src/native/managed/cdac/cdac.slnx +++ b/src/native/managed/cdac/cdac.slnx @@ -14,5 +14,6 @@ + diff --git a/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj b/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj index 5b33a365154275..a3951ba48e1a21 100644 --- a/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj +++ b/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs index c72126d962f939..d054e97d09de27 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs @@ -331,6 +331,9 @@ internal sealed class MockReadyToRunInfo : TypedView private const string LoadedImageBaseFieldName = "LoadedImageBase"; private const string CompositeFieldName = "Composite"; + private const string ImportSectionsFieldName = "ImportSections"; + private const string NumImportSectionsFieldName = "NumImportSections"; + public static Layout CreateLayout(MockTarget.Architecture architecture, int hashMapStride) => new SequentialLayoutBuilder("ReadyToRunInfo", architecture) .AddPointerField(ReadyToRunHeaderFieldName) @@ -342,6 +345,8 @@ public static Layout CreateLayout(MockTarget.Architecture ar .AddPointerField(DelayLoadMethodCallThunksFieldName) .AddPointerField(DebugInfoSectionFieldName) .AddPointerField(ExceptionInfoSectionFieldName) + .AddPointerField(ImportSectionsFieldName) + .AddUInt32Field(NumImportSectionsFieldName) .AddField(EntryPointToMethodDescMapFieldName, hashMapStride) .AddPointerField(LoadedImageBaseFieldName) .AddPointerField(CompositeFieldName) diff --git a/src/native/managed/cdac/tests/StressTests/BasicCdacStressTests.cs b/src/native/managed/cdac/tests/StressTests/BasicCdacStressTests.cs new file mode 100644 index 00000000000000..5da773f7828f7b --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/BasicCdacStressTests.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Microsoft.DotNet.XUnitExtensions; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.DataContractReader.Tests.GCStress; + +/// +/// Runs each debuggee app under corerun with DOTNET_CdacStress=0x51 and asserts +/// that the cDAC stack reference verification achieves 100% pass rate. +/// +/// +/// Prerequisites: +/// - Build CoreCLR native + cDAC: build.cmd -subset clr.native+tools.cdac -c Debug -rc Checked -lc Release +/// - Generate core_root: src\tests\build.cmd Checked generatelayoutonly /p:LibrariesConfiguration=Release +/// - Build debuggees: dotnet build this test project +/// +/// The tests use CORE_ROOT env var if set, otherwise default to the standard artifacts path. +/// +public class BasicStressTests : CdacStressTestBase +{ + public BasicStressTests(ITestOutputHelper output) : base(output) { } + + public static IEnumerable Debuggees => + [ + ["BasicAlloc"], + ["DeepStack"], + ["Generics"], + ["MultiThread"], + ["Comprehensive"], + ["ExceptionHandling"], + ["StructScenarios"], + ["DynamicMethods"], + ]; + + public static IEnumerable WindowsOnlyDebuggees => + [ + ["PInvoke"], + ]; + + [Theory] + [MemberData(nameof(Debuggees))] + public void GCStress_AllVerificationsPass(string debuggeeName) + { + CdacStressResults results = RunGCStress(debuggeeName); + AssertAllPassed(results, debuggeeName); + } + + [Theory] + [MemberData(nameof(WindowsOnlyDebuggees))] + public void GCStress_WindowsOnly_AllVerificationsPass(string debuggeeName) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + throw new SkipTestException("P/Invoke debuggee uses kernel32.dll (Windows only)"); + + CdacStressResults results = RunGCStress(debuggeeName); + AssertAllPassed(results, debuggeeName); + } +} diff --git a/src/native/managed/cdac/tests/StressTests/CdacStressResults.cs b/src/native/managed/cdac/tests/StressTests/CdacStressResults.cs new file mode 100644 index 00000000000000..dd2222741ebed3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/CdacStressResults.cs @@ -0,0 +1,275 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Microsoft.Diagnostics.DataContractReader.Tests.GCStress; + +/// +/// Parses the cdac stress results log file written by the native cdacstress.cpp hook. +/// +internal sealed partial class CdacStressResults +{ + public int TotalVerifications { get; private set; } + public int Passed { get; private set; } + public int Failed { get; private set; } + public int Skipped { get; private set; } + public int RtDiffs { get; private set; } + public string LogFilePath { get; private set; } = string.Empty; + public List FailureDetails { get; } = []; + public List SkipDetails { get; } = []; + public List FailedVerifications { get; } = []; + + [GeneratedRegex(@"^\[PASS\]")] + private static partial Regex PassPattern(); + + [GeneratedRegex(@"^\[FAIL\]")] + private static partial Regex FailPattern(); + + [GeneratedRegex(@"^\[SKIP\]")] + private static partial Regex SkipPattern(); + + [GeneratedRegex(@"^Total verifications:\s*(\d+)")] + private static partial Regex TotalPattern(); + + [GeneratedRegex(@"\[RT_DIFF\]")] + private static partial Regex RtDiffPattern(); + + [GeneratedRegex(@"\[FRAME_DIFF\]\s+Source=0x(\w+)\s+\(([^)]+)\):\s+(\w+)=(\d+)\s+(\w+)=(\d+)")] + private static partial Regex FrameDiffPattern(); + + [GeneratedRegex(@"\[FRAME_(\w+)_ONLY\]\s+Source=0x(\w+)\s+\(([^)]+)\):\s+\w+=(\d+)")] + private static partial Regex FrameOnlyPattern(); + + [GeneratedRegex(@"\[(cDAC|DAC|RT)_ONLY\]\s+Addr=0x(\w+)\s+Obj=0x(\w+)\s+Flags=0x(\w+)")] + private static partial Regex RefOnlyPattern(); + + [GeneratedRegex(@"cDAC \[\d+\]:\s+Addr=0x(\w+)\s+Obj=0x(\w+)\s+Flags=0x(\w+)\s+Src=(.+)")] + private static partial Regex CdacRefPattern(); + + [GeneratedRegex(@"RT\s+\[\d+\]:\s+Addr=0x(\w+)\s+Obj=0x(\w+)\s+Flags=0x(\w+)")] + private static partial Regex RtRefPattern(); + + public static CdacStressResults Parse(string logFilePath) + { + if (!File.Exists(logFilePath)) + throw new FileNotFoundException($"GC stress results log not found: {logFilePath}"); + + var results = new CdacStressResults { LogFilePath = logFilePath }; + FailedVerification? currentFailure = null; + FrameDiff? currentFrame = null; + + foreach (string line in File.ReadLines(logFilePath)) + { + string trimmed = line.Trim(); + + if (PassPattern().IsMatch(trimmed)) + { + currentFailure = null; + currentFrame = null; + results.Passed++; + } + else if (FailPattern().IsMatch(trimmed)) + { + results.Failed++; + results.FailureDetails.Add(trimmed); + currentFailure = new FailedVerification { Header = trimmed }; + results.FailedVerifications.Add(currentFailure); + currentFrame = null; + } + else if (SkipPattern().IsMatch(trimmed)) + { + currentFailure = null; + currentFrame = null; + results.Skipped++; + results.SkipDetails.Add(trimmed); + } + else if (RtDiffPattern().IsMatch(trimmed)) + { + results.RtDiffs++; + } + else if (currentFailure is not null) + { + // Parse structured per-frame output + Match frameDiff = FrameDiffPattern().Match(trimmed); + if (frameDiff.Success) + { + currentFrame = new FrameDiff + { + Source = ulong.Parse(frameDiff.Groups[1].Value, System.Globalization.NumberStyles.HexNumber), + MethodName = frameDiff.Groups[2].Value, + CdacCount = int.Parse(frameDiff.Groups[4].Value), + DacCount = int.Parse(frameDiff.Groups[6].Value), + Kind = FrameDiffKind.Different, + }; + currentFailure.FrameDiffs.Add(currentFrame); + continue; + } + + Match frameOnly = FrameOnlyPattern().Match(trimmed); + if (frameOnly.Success) + { + string ownerLabel = frameOnly.Groups[1].Value; + currentFrame = new FrameDiff + { + Source = ulong.Parse(frameOnly.Groups[2].Value, System.Globalization.NumberStyles.HexNumber), + MethodName = frameOnly.Groups[3].Value, + Kind = ownerLabel == "cDAC" ? FrameDiffKind.CdacOnly : FrameDiffKind.DacOnly, + }; + int count = int.Parse(frameOnly.Groups[4].Value); + if (currentFrame.Kind == FrameDiffKind.CdacOnly) + currentFrame.CdacCount = count; + else + currentFrame.DacCount = count; + currentFailure.FrameDiffs.Add(currentFrame); + continue; + } + + Match refOnly = RefOnlyPattern().Match(trimmed); + if (refOnly.Success && currentFrame is not null) + { + var r = new StackRef + { + Address = ulong.Parse(refOnly.Groups[2].Value, System.Globalization.NumberStyles.HexNumber), + Object = ulong.Parse(refOnly.Groups[3].Value, System.Globalization.NumberStyles.HexNumber), + Flags = uint.Parse(refOnly.Groups[4].Value, System.Globalization.NumberStyles.HexNumber), + }; + currentFrame.UnmatchedRefs.Add(($"{refOnly.Groups[1].Value}_ONLY", r)); + continue; + } + + // Parse flat cDAC/RT ref lines (for cDAC-vs-RT comparison) + Match cdacRef = CdacRefPattern().Match(trimmed); + if (cdacRef.Success) + { + currentFailure.CdacRefs.Add(new StackRef + { + Address = ulong.Parse(cdacRef.Groups[1].Value, System.Globalization.NumberStyles.HexNumber), + Object = ulong.Parse(cdacRef.Groups[2].Value, System.Globalization.NumberStyles.HexNumber), + Flags = uint.Parse(cdacRef.Groups[3].Value, System.Globalization.NumberStyles.HexNumber), + }); + continue; + } + + Match rtRef = RtRefPattern().Match(trimmed); + if (rtRef.Success) + { + currentFailure.RtRefs.Add(new StackRef + { + Address = ulong.Parse(rtRef.Groups[1].Value, System.Globalization.NumberStyles.HexNumber), + Object = ulong.Parse(rtRef.Groups[2].Value, System.Globalization.NumberStyles.HexNumber), + Flags = uint.Parse(rtRef.Groups[3].Value, System.Globalization.NumberStyles.HexNumber), + }); + continue; + } + + // Parse [STACK_TRACE] frame lines: #N MethodName (cDAC=X DAC=Y) + if (trimmed.StartsWith("#") && trimmed.Contains("(cDAC=")) + { + currentFailure.StackTrace.Add(trimmed); + } + } + + Match totalMatch = TotalPattern().Match(trimmed); + if (totalMatch.Success) + { + results.TotalVerifications = int.Parse(totalMatch.Groups[1].Value); + } + } + + if (results.TotalVerifications == 0) + { + results.TotalVerifications = results.Passed + results.Failed + results.Skipped; + } + + return results; + } + + public override string ToString() => + $"Total={TotalVerifications}, Passed={Passed}, Failed={Failed}, Skipped={Skipped}, RtDiffs={RtDiffs}"; + + /// + /// Formats the first N failed verifications using the structured per-frame data + /// logged by the native code. No re-analysis needed — just presents what was logged. + /// + public string AnalyzeFailures(int maxFailures = 3) + { + var sb = new System.Text.StringBuilder(); + + foreach (var failure in FailedVerifications.Take(maxFailures)) + { + sb.AppendLine(failure.Header); + + if (failure.FrameDiffs.Count > 0) + { + sb.AppendLine(" Per-frame diff (cDAC vs DAC):"); + foreach (var frame in failure.FrameDiffs) + { + string kindLabel = frame.Kind switch + { + FrameDiffKind.Different => $"cDAC={frame.CdacCount} DAC={frame.DacCount}", + FrameDiffKind.CdacOnly => $"cDAC={frame.CdacCount} (cDAC-only frame)", + FrameDiffKind.DacOnly => $"DAC={frame.DacCount} (DAC-only frame)", + _ => "unknown", + }; + sb.AppendLine($" {frame.MethodName}: {kindLabel}"); + foreach (var (label, r) in frame.UnmatchedRefs) + sb.AppendLine($" [{label}] Addr=0x{r.Address:X} Obj=0x{r.Object:X} Flags=0x{r.Flags:X}"); + } + } + + if (failure.CdacRefs.Count > 0 || failure.RtRefs.Count > 0) + { + sb.AppendLine($" cDAC vs RT: cDAC={failure.CdacRefs.Count} RT={failure.RtRefs.Count}"); + } + + if (failure.StackTrace.Count > 0) + { + sb.AppendLine(" Stack trace:"); + foreach (string frame in failure.StackTrace) + sb.AppendLine($" {frame}"); + } + + sb.AppendLine(); + } + + return sb.ToString(); + } +} + +internal struct StackRef +{ + public ulong Address; + public ulong Object; + public uint Flags; +} + +internal enum FrameDiffKind +{ + Different, + CdacOnly, + DacOnly, +} + +internal sealed class FrameDiff +{ + public ulong Source { get; set; } + public string MethodName { get; set; } = ""; + public int CdacCount { get; set; } + public int DacCount { get; set; } + public FrameDiffKind Kind { get; set; } + public List<(string Label, StackRef Ref)> UnmatchedRefs { get; } = []; +} + +internal sealed class FailedVerification +{ + public string Header { get; set; } = ""; + public List FrameDiffs { get; } = []; + public List CdacRefs { get; } = []; + public List RtRefs { get; } = []; + public List StackTrace { get; } = []; +} diff --git a/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs b/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs new file mode 100644 index 00000000000000..8a3eb52219ba38 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs @@ -0,0 +1,237 @@ +// 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; +using System.IO; +using System.Runtime.InteropServices; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.DataContractReader.Tests.GCStress; + +/// +/// Base class for cDAC stress tests. Runs a debuggee app under corerun +/// with DOTNET_CdacStress=0x51 and parses the verification results. +/// +public abstract class CdacStressTestBase +{ + private readonly ITestOutputHelper _output; + + protected CdacStressTestBase(ITestOutputHelper output) + { + _output = output; + } + + /// + /// Runs the named debuggee under GC stress and returns the parsed results. + /// + internal CdacStressResults RunGCStress(string debuggeeName, int timeoutSeconds = 300) + { + string coreRoot = GetCoreRoot(); + string corerun = GetCoreRunPath(coreRoot); + string debuggeeDll = GetDebuggeePath(debuggeeName); + string logFile = Path.Combine(Path.GetTempPath(), $"cdac-gcstress-{debuggeeName}-{Guid.NewGuid():N}.txt"); + + _output.WriteLine($"Running GC stress: {debuggeeName}"); + _output.WriteLine($" corerun: {corerun}"); + _output.WriteLine($" debuggee: {debuggeeDll}"); + _output.WriteLine($" log: {logFile}"); + + var psi = new ProcessStartInfo + { + FileName = corerun, + Arguments = $"\"{debuggeeDll}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + psi.Environment["CORE_ROOT"] = coreRoot; + // Default to 0x51 (ALLOC + REFS + USE_DAC) for three-way comparison. + // Override via outer DOTNET_CdacStress env var if needed. + psi.Environment["DOTNET_CdacStress"] = + Environment.GetEnvironmentVariable("DOTNET_CdacStress") ?? "0x51"; + psi.Environment["DOTNET_CdacStressFailFast"] = "0"; + psi.Environment["DOTNET_CdacStressLogFile"] = logFile; + psi.Environment["DOTNET_CdacStressStep"] = "1"; + psi.Environment["DOTNET_ContinueOnAssert"] = "1"; + + using var process = Process.Start(psi)!; + + // Read both stdout and stderr asynchronously to avoid deadlock + // when pipe buffers fill, and to allow WaitForExit timeout to work. + string stderr = ""; + string stdout = ""; + process.ErrorDataReceived += (_, e) => + { + if (e.Data is not null) + stderr += e.Data + Environment.NewLine; + }; + process.OutputDataReceived += (_, e) => + { + if (e.Data is not null) + stdout += e.Data + Environment.NewLine; + }; + process.BeginErrorReadLine(); + process.BeginOutputReadLine(); + + bool exited = process.WaitForExit(timeoutSeconds * 1000); + if (!exited) + { + process.Kill(entireProcessTree: true); + Assert.Fail($"GC stress test '{debuggeeName}' timed out after {timeoutSeconds}s"); + } + + _output.WriteLine($" exit code: {process.ExitCode}"); + if (!string.IsNullOrWhiteSpace(stdout)) + _output.WriteLine($" stdout: {stdout.TrimEnd()}"); + if (!string.IsNullOrWhiteSpace(stderr)) + _output.WriteLine($" stderr: {stderr.TrimEnd()}"); + + Assert.True(process.ExitCode == 100, + $"GC stress test '{debuggeeName}' exited with {process.ExitCode} (expected 100).\nstdout: {stdout}\nstderr: {stderr}"); + + Assert.True(File.Exists(logFile), + $"GC stress results log not created: {logFile}"); + + CdacStressResults results = CdacStressResults.Parse(logFile); + + _output.WriteLine($" results: {results}"); + + return results; + } + + /// + /// Asserts that GC stress verification produced 100% pass rate with no failures or skips. + /// + internal static void AssertAllPassed(CdacStressResults results, string debuggeeName) + { + Assert.True(results.TotalVerifications > 0, + $"GC stress test '{debuggeeName}' produced zero verifications — " + + "GCStress may not have triggered or cDAC may not be loaded."); + + if (results.Failed > 0) + { + string analysis = results.AnalyzeFailures(maxFailures: 3); + Assert.Fail( + $"GC stress test '{debuggeeName}' had {results.Failed} failure(s) " + + $"out of {results.TotalVerifications} verifications.\n" + + $"Log: {results.LogFilePath}\n\n{analysis}"); + } + + if (results.Skipped > 0) + { + string details = string.Join("\n", results.SkipDetails); + Assert.Fail( + $"GC stress test '{debuggeeName}' had {results.Skipped} skip(s) " + + $"out of {results.TotalVerifications} verifications.\n" + + $"Log: {results.LogFilePath}\n{details}"); + } + } + + /// + /// Asserts that GC stress verification produced a pass rate at or above the given threshold. + /// Useful for instruction-level stress where a small number of failures may occur + /// due to known limitations. + /// + internal static void AssertHighPassRate(CdacStressResults results, string debuggeeName, double minPassRate) + { + Assert.True(results.TotalVerifications > 0, + $"GC stress test '{debuggeeName}' produced zero verifications — " + + "GCStress may not have triggered or cDAC may not be loaded."); + + double passRate = (double)results.Passed / results.TotalVerifications; + if (passRate < minPassRate) + { + string details = string.Join("\n", results.FailureDetails); + Assert.Fail( + $"GC stress test '{debuggeeName}' pass rate {passRate:P2} is below " + + $"{minPassRate:P1} threshold. {results.Failed} failure(s) out of " + + $"{results.TotalVerifications} verifications.\n{details}"); + } + } + + private static string GetCoreRoot() + { + // Check environment variable first + string? coreRoot = Environment.GetEnvironmentVariable("CORE_ROOT"); + if (!string.IsNullOrEmpty(coreRoot) && Directory.Exists(coreRoot)) + return coreRoot; + + // Default path based on repo layout + string repoRoot = FindRepoRoot(); + string rid = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "windows" + : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "osx" + : "linux"; + string arch = RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(); + coreRoot = Path.Combine(repoRoot, "artifacts", "tests", "coreclr", $"{rid}.{arch}.Checked", "Tests", "Core_Root"); + + if (!Directory.Exists(coreRoot)) + throw new DirectoryNotFoundException( + $"Core_Root not found at '{coreRoot}'. " + + "Set the CORE_ROOT environment variable or run 'src/tests/build.cmd Checked generatelayoutonly'."); + + return coreRoot; + } + + private static string GetCoreRunPath(string coreRoot) + { + string exe = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "corerun.exe" : "corerun"; + string path = Path.Combine(coreRoot, exe); + Assert.True(File.Exists(path), $"corerun not found at '{path}'"); + + return path; + } + + private static string GetDebuggeePath(string debuggeeName) + { + // On Helix, debuggees are in the work item payload's debuggees/ directory. + // The test assembly is in /tests/, so AppContext.BaseDirectory is there. + // The debuggees are siblings at /debuggees//. + string? helixPayload = Environment.GetEnvironmentVariable("HELIX_WORKITEM_PAYLOAD"); + if (!string.IsNullOrEmpty(helixPayload)) + { + string helixDebuggeesDir = Path.Combine(helixPayload, "debuggees", debuggeeName); + if (Directory.Exists(helixDebuggeesDir)) + { + foreach (string dir in Directory.GetDirectories(helixDebuggeesDir, "*", SearchOption.AllDirectories)) + { + string dll = Path.Combine(dir, $"{debuggeeName}.dll"); + if (File.Exists(dll)) + return dll; + } + } + } + + // Local development: debuggees are built to artifacts/bin/StressTests// + string repoRoot = FindRepoRoot(); + string binDir = Path.Combine(repoRoot, "artifacts", "bin", "StressTests", debuggeeName); + + if (!Directory.Exists(binDir)) + throw new DirectoryNotFoundException( + $"Debuggee '{debuggeeName}' not found at '{binDir}'. Build the StressTests project first."); + + // Find the dll in any Release/ subdirectory + foreach (string dir in Directory.GetDirectories(binDir, "*", SearchOption.AllDirectories)) + { + string dll = Path.Combine(dir, $"{debuggeeName}.dll"); + if (File.Exists(dll)) + return dll; + } + + throw new FileNotFoundException($"Could not find {debuggeeName}.dll under '{binDir}'"); + } + + private static string FindRepoRoot() + { + string? dir = AppContext.BaseDirectory; + while (dir is not null) + { + if (File.Exists(Path.Combine(dir, "global.json"))) + return dir; + dir = Path.GetDirectoryName(dir); + } + + throw new InvalidOperationException("Could not find repo root (global.json)"); + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/DeepStack/DeepStack.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/DeepStack/DeepStack.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/DeepStack/DeepStack.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/DeepStack/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/DeepStack/Program.cs new file mode 100644 index 00000000000000..c98679aea54ac2 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/DeepStack/Program.cs @@ -0,0 +1,43 @@ +// 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.Runtime.CompilerServices; + +/// +/// Exercises deep recursion with live GC references at each frame level. +/// +internal static class Program +{ + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedCall(int depth) + { + object o = new object(); + if (depth > 0) + NestedCall(depth - 1); + GC.KeepAlive(o); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedWithMultipleRefs(int depth) + { + object a = new object(); + string b = $"depth-{depth}"; + int[] c = new int[depth + 1]; + if (depth > 0) + NestedWithMultipleRefs(depth - 1); + GC.KeepAlive(a); + GC.KeepAlive(b); + GC.KeepAlive(c); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + NestedCall(10); + NestedWithMultipleRefs(8); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/DynamicMethods/DynamicMethods.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/DynamicMethods/DynamicMethods.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/DynamicMethods/DynamicMethods.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/DynamicMethods/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/DynamicMethods/Program.cs new file mode 100644 index 00000000000000..865d338e1e935f --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/DynamicMethods/Program.cs @@ -0,0 +1,149 @@ +// 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.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +/// +/// Exercises the MetaSig (non-GCRefMap) path by creating and invoking +/// DynamicMethod (LCG) methods. These methods use StoredSigMethodDesc +/// and don't have pre-computed GCRefMaps, forcing PromoteCallerStack +/// to walk the signature via MetaSig. +/// +/// Scenarios: +/// - Simple object parameter (GcTypeKind.Ref) +/// - Multiple object parameters +/// - Byref parameter (GcTypeKind.Interior) +/// - Mixed ref and primitive parameters +/// - Method with 'this' (instance delegate) +/// - Method returning object (tests return type parsing) +/// +internal static class Program +{ + static int Main() + { + for (int i = 0; i < 50; i++) + { + SimpleObjectParam(); + MultipleObjectParams(); + MixedParams(); + ObjectReturn(); + KeepAliveInDynamic(); + } + return 100; + } + + // ===== Scenario 1: Single object parameter ===== + [MethodImpl(MethodImplOptions.NoInlining)] + static void SimpleObjectParam() + { + // Create: void DynMethod(object o) + DynamicMethod dm = new("DynSimple", typeof(void), new[] { typeof(object) }); + ILGenerator il = dm.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ret); + + Action del = dm.CreateDelegate>(); + object live = new object(); + del(live); + GC.KeepAlive(live); + } + + // ===== Scenario 2: Multiple object parameters ===== + [MethodImpl(MethodImplOptions.NoInlining)] + static void MultipleObjectParams() + { + // Create: void DynMulti(object a, string b, int[] c) + DynamicMethod dm = new("DynMulti", typeof(void), + new[] { typeof(object), typeof(string), typeof(int[]) }); + ILGenerator il = dm.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ret); + + var del = dm.CreateDelegate>(); + object a = new object(); + string b = "hello"; + int[] c = new[] { 1, 2, 3 }; + del(a, b, c); + GC.KeepAlive(a); + GC.KeepAlive(b); + GC.KeepAlive(c); + } + + // ===== Scenario 3: Mixed ref and primitive parameters ===== + [MethodImpl(MethodImplOptions.NoInlining)] + static void MixedParams() + { + // Create: void DynMixed(object o, int x, string s, long y) + DynamicMethod dm = new("DynMixed", typeof(void), + new[] { typeof(object), typeof(int), typeof(string), typeof(long) }); + ILGenerator il = dm.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ret); + + var del = dm.CreateDelegate>(); + object o = new object(); + string s = "world"; + del(o, 42, s, 999L); + GC.KeepAlive(o); + GC.KeepAlive(s); + } + + // ===== Scenario 4: Object return type ===== + [MethodImpl(MethodImplOptions.NoInlining)] + static void ObjectReturn() + { + // Create: object DynReturn(object o) + DynamicMethod dm = new("DynReturn", typeof(object), new[] { typeof(object) }); + ILGenerator il = dm.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ret); + + var del = dm.CreateDelegate>(); + object input = new object(); + object result = del(input); + GC.KeepAlive(result); + GC.KeepAlive(input); + } + + // ===== Scenario 5: Multiple allocations inside dynamic method ===== + [MethodImpl(MethodImplOptions.NoInlining)] + static void KeepAliveInDynamic() + { + // Create: void DynAlloc(object a, object b, object c, object d) + DynamicMethod dm = new("DynAlloc", typeof(void), + new[] { typeof(object), typeof(object), typeof(object), typeof(object) }); + ILGenerator il = dm.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ldarg_3); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ret); + + var del = dm.CreateDelegate>(); + object a = new object(); + object b = "str"; + object c = new int[] { 1 }; + object d = new byte[16]; + del(a, b, c, d); + GC.KeepAlive(a); + GC.KeepAlive(b); + GC.KeepAlive(c); + GC.KeepAlive(d); + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/ExceptionHandling/ExceptionHandling.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/ExceptionHandling/ExceptionHandling.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/ExceptionHandling/ExceptionHandling.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/ExceptionHandling/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/ExceptionHandling/Program.cs new file mode 100644 index 00000000000000..4bd0a12fe6d145 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/ExceptionHandling/Program.cs @@ -0,0 +1,143 @@ +// 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.Runtime.CompilerServices; + +/// +/// Exercises exception handling: try/catch/finally funclets, nested exceptions, +/// filter funclets, and rethrow. +/// +internal static class Program +{ + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryCatchScenario() + { + object before = new object(); + try + { + object inside = new object(); + ThrowHelper(); + GC.KeepAlive(inside); + } + catch (InvalidOperationException ex) + { + object inCatch = new object(); + GC.KeepAlive(ex); + GC.KeepAlive(inCatch); + } + GC.KeepAlive(before); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThrowHelper() + { + throw new InvalidOperationException("test exception"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryFinallyScenario() + { + object outerRef = new object(); + try + { + object innerRef = new object(); + GC.KeepAlive(innerRef); + } + finally + { + object finallyRef = new object(); + GC.KeepAlive(finallyRef); + } + GC.KeepAlive(outerRef); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedExceptionScenario() + { + object a = new object(); + try + { + try + { + object c = new object(); + throw new ArgumentException("inner"); + } + catch (ArgumentException ex1) + { + GC.KeepAlive(ex1); + throw new InvalidOperationException("outer", ex1); + } + finally + { + object d = new object(); + GC.KeepAlive(d); + } + } + catch (InvalidOperationException ex2) + { + GC.KeepAlive(ex2); + } + GC.KeepAlive(a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void FilterExceptionScenario() + { + object holder = new object(); + try + { + throw new ArgumentException("filter-test"); + } + catch (ArgumentException ex) when (FilterCheck(ex)) + { + GC.KeepAlive(ex); + } + GC.KeepAlive(holder); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static bool FilterCheck(Exception ex) + { + object filterLocal = new object(); + GC.KeepAlive(filterLocal); + return ex.Message.Contains("filter"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void RethrowScenario() + { + object outerRef = new object(); + try + { + try + { + throw new ApplicationException("rethrow-test"); + } + catch (ApplicationException) + { + object catchRef = new object(); + GC.KeepAlive(catchRef); + throw; + } + } + catch (ApplicationException ex) + { + GC.KeepAlive(ex); + } + GC.KeepAlive(outerRef); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + TryCatchScenario(); + TryFinallyScenario(); + NestedExceptionScenario(); + FilterExceptionScenario(); + RethrowScenario(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/Generics/Generics.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/Generics/Generics.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/Generics/Generics.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/Generics/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/Generics/Program.cs new file mode 100644 index 00000000000000..54b7060c040f5a --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/Generics/Program.cs @@ -0,0 +1,81 @@ +// 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.Collections.Generic; +using System.Runtime.CompilerServices; + +/// +/// Exercises generic method instantiations and interface dispatch. +/// +internal static class Program +{ + interface IKeepAlive + { + object GetRef(); + } + + class BoxHolder : IKeepAlive + { + object _value; + public BoxHolder() { _value = new object(); } + public BoxHolder(object v) { _value = v; } + + [MethodImpl(MethodImplOptions.NoInlining)] + public object GetRef() => _value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static T GenericAlloc() where T : new() + { + T val = new T(); + object marker = new object(); + GC.KeepAlive(marker); + return val; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void GenericScenario() + { + var o = GenericAlloc(); + var l = GenericAlloc>(); + var s = GenericAlloc(); + GC.KeepAlive(o); + GC.KeepAlive(l); + GC.KeepAlive(s); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void InterfaceDispatchScenario() + { + IKeepAlive holder = new BoxHolder(new int[] { 42, 43 }); + object r = holder.GetRef(); + GC.KeepAlive(holder); + GC.KeepAlive(r); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void DelegateScenario() + { + object captured = new object(); + Func fn = () => + { + GC.KeepAlive(captured); + return new object(); + }; + object result = fn(); + GC.KeepAlive(result); + GC.KeepAlive(fn); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + GenericScenario(); + InterfaceDispatchScenario(); + DelegateScenario(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/MultiThread/MultiThread.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/MultiThread/MultiThread.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/MultiThread/MultiThread.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/MultiThread/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/MultiThread/Program.cs new file mode 100644 index 00000000000000..0eea731a6bd313 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/MultiThread/Program.cs @@ -0,0 +1,53 @@ +// 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.Runtime.CompilerServices; +using System.Threading; + +/// +/// Exercises concurrent threads with GC references, exercising multi-threaded +/// stack walks and GC ref enumeration. +/// +internal static class Program +{ + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedCall(int depth) + { + object o = new object(); + if (depth > 0) + NestedCall(depth - 1); + GC.KeepAlive(o); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThreadWork(int id) + { + object threadLocal = new object(); + string threadName = $"thread-{id}"; + NestedCall(5); + GC.KeepAlive(threadLocal); + GC.KeepAlive(threadName); + } + + static int Main() + { + for (int iteration = 0; iteration < 2; iteration++) + { + ManualResetEventSlim ready = new ManualResetEventSlim(false); + ManualResetEventSlim go = new ManualResetEventSlim(false); + Thread t = new Thread(() => + { + ready.Set(); + go.Wait(); + ThreadWork(1); + }); + t.Start(); + ready.Wait(); + go.Set(); + ThreadWork(0); + t.Join(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/PInvoke/PInvoke.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/PInvoke/PInvoke.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/PInvoke/PInvoke.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/PInvoke/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/PInvoke/Program.cs new file mode 100644 index 00000000000000..83aece921baaea --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/PInvoke/Program.cs @@ -0,0 +1,74 @@ +// 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.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +/// +/// Exercises P/Invoke transitions with GC references before and after native calls, +/// and pinned GC handles. +/// +internal static class Program +{ + [DllImport("kernel32.dll")] + static extern uint GetCurrentThreadId(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static void PInvokeScenario() + { + object before = new object(); + uint tid = GetCurrentThreadId(); + object after = new object(); + GC.KeepAlive(before); + GC.KeepAlive(after); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void PinnedScenario() + { + byte[] buffer = new byte[64]; + GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned); + try + { + object other = new object(); + GC.KeepAlive(other); + GC.KeepAlive(buffer); + } + finally + { + pin.Free(); + } + } + + struct LargeStruct + { + public object A, B, C, D; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void StructWithRefsScenario() + { + LargeStruct ls; + ls.A = new object(); + ls.B = "struct-string"; + ls.C = new int[] { 10, 20 }; + ls.D = new object(); + GC.KeepAlive(ls.A); + GC.KeepAlive(ls.B); + GC.KeepAlive(ls.C); + GC.KeepAlive(ls.D); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + PInvokeScenario(); + PinnedScenario(); + StructWithRefsScenario(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/Program.cs new file mode 100644 index 00000000000000..9067337495def2 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/Program.cs @@ -0,0 +1,157 @@ +// 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.Runtime.CompilerServices; + +/// +/// Exercises struct-related GC scanning scenarios that stress the MetaSig path: +/// - Value type 'this' (interior pointer for struct instance methods) +/// - Small struct returns (retbuf detection precision) +/// - Struct parameters containing embedded GC references +/// +internal static class Program +{ + static int Main() + { + for (int i = 0; i < 100; i++) + { + ValueTypeThisScenario(); + SmallStructReturnScenario(); + StructWithRefsScenario(); + InterfaceDispatchScenario(); + } + return 100; + } + + // ===== Scenario 1: Value type 'this' ===== + // When a struct instance method is called through interface dispatch, + // 'this' is an interior pointer (pointing into the boxed struct, past + // the MethodTable pointer). The GC needs GC_CALL_INTERIOR to handle it. + + interface IKeepAlive + { + object GetRef(); + } + + struct StructWithRef : IKeepAlive + { + public object Field; + + [MethodImpl(MethodImplOptions.NoInlining)] + public object GetRef() => Field; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ValueTypeThisScenario() + { + IKeepAlive s = new StructWithRef { Field = new object() }; + object r = s.GetRef(); + GC.KeepAlive(r); + GC.KeepAlive(s); + } + + // ===== Scenario 2: Small struct returns ===== + // Methods returning small structs (1/2/4/8 bytes, power-of-2) do NOT need + // a return buffer on AMD64 Windows — the value is returned in RAX. + // Conservative HasRetBuffArg=true shifts all parameter offsets by 1 slot. + + struct SmallResult + { + public int Value; + } + + struct TinyResult + { + public byte Value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static SmallResult MakeSmallResult(object keepAlive) + { + GC.KeepAlive(keepAlive); + return new SmallResult { Value = 42 }; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static TinyResult MakeTinyResult(object keepAlive) + { + GC.KeepAlive(keepAlive); + return new TinyResult { Value = 1 }; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void SmallStructReturnScenario() + { + object live = new object(); + SmallResult sr = MakeSmallResult(live); + TinyResult tr = MakeTinyResult(live); + GC.KeepAlive(sr); + GC.KeepAlive(tr); + GC.KeepAlive(live); + } + + // ===== Scenario 3: Struct parameters with embedded GC refs ===== + // Value type parameters containing object references require GCDesc + // scanning to find the embedded refs. Without this, the refs inside + // the struct are invisible to the GC. + + struct Holder + { + public object Ref1; + public string Ref2; + public int[] Array; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ProcessHolder(Holder h) + { + GC.KeepAlive(h.Ref1); + GC.KeepAlive(h.Ref2); + GC.KeepAlive(h.Array); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void StructWithRefsScenario() + { + Holder h = new Holder + { + Ref1 = new object(), + Ref2 = "hello", + Array = new int[] { 1, 2, 3 }, + }; + ProcessHolder(h); + GC.KeepAlive(h.Ref1); + } + + // ===== Scenario 4: Interface dispatch with generics ===== + // Shared generic methods going through stub dispatch combine + // RequiresInstArg with value type 'this'. + + interface IGenericOp + { + T Get(); + } + + struct GenericStruct : IGenericOp + { + public T Value; + + [MethodImpl(MethodImplOptions.NoInlining)] + public T Get() => Value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void InterfaceDispatchScenario() + { + IGenericOp g = new GenericStruct { Value = new object() }; + object r = g.Get(); + GC.KeepAlive(r); + GC.KeepAlive(g); + + IGenericOp gs = new GenericStruct { Value = "test" }; + string s = gs.Get(); + GC.KeepAlive(s); + GC.KeepAlive(gs); + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/StructScenarios.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/StructScenarios.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/StructScenarios.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Microsoft.Diagnostics.DataContractReader.StressTests.csproj b/src/native/managed/cdac/tests/StressTests/Microsoft.Diagnostics.DataContractReader.StressTests.csproj new file mode 100644 index 00000000000000..d6bd3aa5a13459 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Microsoft.Diagnostics.DataContractReader.StressTests.csproj @@ -0,0 +1,21 @@ + + + true + $(NetCoreAppToolCurrent) + enable + true + + + + + + + + + + + + + + + diff --git a/src/native/managed/cdac/tests/StressTests/README.md b/src/native/managed/cdac/tests/StressTests/README.md index c5bcde5675b3f0..97a676edfa2892 100644 --- a/src/native/managed/cdac/tests/StressTests/README.md +++ b/src/native/managed/cdac/tests/StressTests/README.md @@ -1,18 +1,39 @@ # cDAC Stress Tests -This folder contains stress tests that verify the cDAC's stack reference -enumeration against the runtime's GC root scanning. The tests run managed -debuggee applications under `corerun` with cDAC stress flags enabled, -triggering verification at allocation points, GC points, or instruction-level -GC stress points. +Integration tests that verify the cDAC's stack reference enumeration matches the runtime's +GC root scanning under GC stress conditions. -## Quick Start +## How It Works + +Each test runs a debuggee console app under `corerun` with `DOTNET_CdacStress=0x51`, which enables: +- **0x01**: Allocation-point verification (triggers at every managed allocation) +- **0x10**: GC reference comparison (compares cDAC stack refs against runtime refs) +- **0x40**: Legacy DAC comparison (three-way: cDAC vs DAC vs runtime) + +The native `cdacstress.cpp` hook writes structured per-frame comparison results to a log file. +On failure, it shows per-frame diffs with resolved method names, making it easy to identify +which frame and method has mismatched GC references. + +Pass/fail semantics: +- **[PASS]**: cDAC matches DAC (may include `[RT_DIFF]` annotation if RT differs) +- **[FAIL]**: cDAC does NOT match DAC +- **[SKIP]**: cDAC GetStackReferences failed (e.g., during EH) + +## Prerequisites + +Build the runtime with the cDAC stress hook enabled: ```powershell -# Prerequisites: build CoreCLR Checked and generate core_root -# build.cmd clr+libs -rc Checked -lc Release -# src\tests\build.cmd Checked generatelayoutonly /p:LibrariesConfiguration=Release +# From repo root +.\build.cmd -subset clr.native+tools.cdac -c Debug -rc Checked -lc Release +.\src\tests\build.cmd Checked generatelayoutonly -SkipRestorePackages /p:LibrariesConfiguration=Release +``` +## Running Tests + +### Using RunStressTests.ps1 + +```powershell # Run all debuggees (allocation-point verification, no GCStress) ./RunStressTests.ps1 -SkipBuild @@ -21,88 +42,60 @@ GC stress points. # Run with instruction-level GCStress (slower, more thorough) ./RunStressTests.ps1 -SkipBuild -CdacStress 0x14 -GCStress 0x4 - -# Full comparison including walk parity and DAC cross-check -./RunStressTests.ps1 -SkipBuild -CdacStress 0x74 -GCStress 0x4 ``` -## How It Works - -### DOTNET_CdacStress Flags - -The `DOTNET_CdacStress` environment variable is a bitmask that controls -**where** and **what** the runtime verifies: +### Using dotnet test (xUnit) -| Bit | Flag | Description | -|-----|------|-------------| -| 0x1 | ALLOC | Verify at managed allocation points | -| 0x2 | GC | Verify at GC collection points | -| 0x4 | INSTR | Verify at instruction-level GC stress points (requires `DOTNET_GCStress`) | -| 0x10 | REFS | Compare GC stack references (cDAC vs runtime) | -| 0x20 | WALK | Compare stack walk frame ordering (cDAC vs DAC) | -| 0x40 | USE_DAC | Also compare GC refs against the legacy DAC | -| 0x100 | UNIQUE | Only verify each instruction pointer once | - -Common combinations: -- `0x11` — ALLOC + REFS (fast, default) -- `0x14` — INSTR + REFS (thorough, requires `DOTNET_GCStress=0x4`) -- `0x31` — ALLOC + REFS + WALK (fast with walk parity check) -- `0x74` — INSTR + REFS + WALK + USE_DAC (full comparison) - -### Verification Flow +```powershell +# Build and run all stress tests +.\.dotnet\dotnet.exe test src\native\managed\cdac\tests\StressTests -At each stress point, the native hook (`cdacstress.cpp`) in the runtime: +# Run a specific debuggee +.\.dotnet\dotnet.exe test src\native\managed\cdac\tests\StressTests --filter "FullyQualifiedName~BasicAlloc" -1. Suspends the current thread's context -2. Calls the cDAC's `GetStackReferences` to enumerate GC roots -3. Compares against the runtime's own GC root enumeration -4. Optionally compares against the legacy DAC's enumeration -5. Optionally compares stack walk frame ordering -6. Logs `[PASS]` or `[FAIL]` per verification point +# Set CORE_ROOT manually if needed +$env:CORE_ROOT = "path\to\Core_Root" +.\.dotnet\dotnet.exe test src\native\managed\cdac\tests\StressTests +``` -The script collects these results and reports aggregate pass/fail counts. +## Adding a New Debuggee -## Debuggees +1. Create a folder under `Debuggees/` with a `.csproj` and `Program.cs` +2. The `.csproj` just needs: `` + (inherits OutputType=Exe and TFM from `Directory.Build.props`) +3. `Main()` must return `100` on success +4. Use `[MethodImpl(MethodImplOptions.NoInlining)]` on methods to prevent inlining +5. Use `GC.KeepAlive()` to ensure objects are live at GC stress points +6. Add the debuggee name to `BasicStressTests.Debuggees` -Each debuggee is a standalone console application under `Debuggees/`: +## Debuggee Catalog | Debuggee | Scenarios | |----------|-----------| -| **BasicAlloc** | Object allocation, strings, arrays, many live refs | -| **Comprehensive** | All-in-one: allocations, deep stacks, exceptions, generics, P/Invoke, threading | - -All debuggees return exit code 100 on success. - -### Adding a New Debuggee - -1. Create a new folder under `Debuggees/` (e.g., `Debuggees/MyScenario/`) -2. Add a minimal `.csproj`: - ```xml - - ``` - The `Directory.Build.props` provides all common settings. -3. Add a `Program.cs` with a `Main()` that returns 100 -4. Use `[MethodImpl(MethodImplOptions.NoInlining)]` and `GC.KeepAlive()` - to prevent the JIT from optimizing away allocations and references +| **BasicAlloc** | Objects, strings, arrays, many live refs | +| **ExceptionHandling** | try/catch/finally funclets, nested exceptions, filter funclets, rethrow | +| **DeepStack** | Deep recursion with live refs at each frame | +| **Generics** | Generic method instantiations, interface dispatch, delegates | +| **PInvoke** | P/Invoke transitions, pinned GC handles, struct with object refs | +| **MultiThread** | Concurrent threads with synchronized GC stress | +| **Comprehensive** | All-in-one: every scenario in a single run | +| **StructScenarios** | Struct returns, by-ref params | +| **DynamicMethods** | DynamicMethod / IL emit | + +## Architecture -The script auto-discovers all debuggees by scanning for `.csproj` files. - -## Script Parameters - -| Parameter | Default | Description | -|-----------|---------|-------------| -| `-Configuration` | `Checked` | Runtime build configuration | -| `-CdacStress` | `0x11` | Hex bitmask for `DOTNET_CdacStress` | -| `-GCStress` | _(empty)_ | Hex value for `DOTNET_GCStress` (e.g., `0x4`) | -| `-Debuggee` | _(all)_ | Which debuggee(s) to run | -| `-SkipBuild` | off | Skip CoreCLR/cDAC build step | -| `-SkipBaseline` | off | Skip baseline (no-stress) verification | - -## Expected Results - -Most runs achieve >99.5% pass rate. A small number of failures (~0.2%) -are expected due to the ScanFrameRoots gap — the cDAC does not yet enumerate -GC roots from explicit frame stub data (e.g., `StubDispatchFrame`, -`PInvokeCalliFrame`). These are tracked in [known-issues.md](known-issues.md). - -Walk parity (`WALK` flag) should show 0 mismatches. +``` +CdacStressTestBase.RunGCStress(debuggeeName) + │ + ├── Locate core_root/corerun (CORE_ROOT env or default path) + ├── Locate debuggee DLL (artifacts/bin/StressTests//...) + ├── Start Process: corerun + │ Environment: + │ DOTNET_CdacStress=0x51 + │ DOTNET_CdacStressStep=1 + │ DOTNET_CdacStressLogFile= + │ DOTNET_ContinueOnAssert=1 + ├── Wait for exit (timeout: 300s) + ├── Parse results log → CdacStressResults + └── Assert: exit=100, zero failures +``` diff --git a/src/native/managed/cdac/tests/StressTests/StressTests.targets b/src/native/managed/cdac/tests/StressTests/StressTests.targets new file mode 100644 index 00000000000000..b88b4132e0d4a3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/StressTests.targets @@ -0,0 +1,70 @@ + + + + $(MSBuildThisFileDirectory)Debuggees\ + Release + + + + + + + + + + + + + + + + + + + + + <_HelixTestsDir>$([MSBuild]::NormalizeDirectory('$(HelixPayloadDir)', 'tests')) + <_HelixDebuggeesDir>$([MSBuild]::NormalizeDirectory('$(HelixPayloadDir)', 'debuggees')) + + + + + <_TestOutput Include="$(OutputPath)**\*" /> + + + + + + <_XunitConsoleFiles Include="$([System.IO.Path]::GetDirectoryName('$(XunitConsoleNetCoreAppPath)'))\*" /> + + + + + + + + + + + <_DebuggeeOutputDir>$([MSBuild]::NormalizeDirectory('$(RepoRoot)', 'artifacts', 'bin', 'StressTests', '$(DebuggeeName)', '$(DebuggeeConfiguration)')) + + + <_DebuggeeFiles Include="$(_DebuggeeOutputDir)**\*" /> + + + + diff --git a/src/native/managed/cdac/tests/StressTests/cdac-stress-helix.proj b/src/native/managed/cdac/tests/StressTests/cdac-stress-helix.proj new file mode 100644 index 00000000000000..9deecf71c49ca4 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/cdac-stress-helix.proj @@ -0,0 +1,76 @@ + + + + + + msbuild + true + true + true + $(_Creator) + true + $(BUILD_BUILDNUMBER) + test/cdac/stresstests/ + pr/dotnet/runtime/cdac-stress-tests + 00:30:00 + + + + + + %(Identity) + + + + + + + + + + + + + + + @(HelixPreCommand) + + + + + + <_StressTestCommand>%25HELIX_CORRELATION_PAYLOAD%25\dotnet.exe exec --runtimeconfig %25HELIX_WORKITEM_PAYLOAD%25\tests\Microsoft.Diagnostics.DataContractReader.StressTests.runtimeconfig.json --depsfile %25HELIX_WORKITEM_PAYLOAD%25\tests\Microsoft.Diagnostics.DataContractReader.StressTests.deps.json %25HELIX_WORKITEM_PAYLOAD%25\tests\xunit.console.dll %25HELIX_WORKITEM_PAYLOAD%25\tests\Microsoft.Diagnostics.DataContractReader.StressTests.dll -xml testResults.xml -nologo + + + <_StressTestCommand>$HELIX_CORRELATION_PAYLOAD/dotnet exec --runtimeconfig $HELIX_WORKITEM_PAYLOAD/tests/Microsoft.Diagnostics.DataContractReader.StressTests.runtimeconfig.json --depsfile $HELIX_WORKITEM_PAYLOAD/tests/Microsoft.Diagnostics.DataContractReader.StressTests.deps.json $HELIX_WORKITEM_PAYLOAD/tests/xunit.console.dll $HELIX_WORKITEM_PAYLOAD/tests/Microsoft.Diagnostics.DataContractReader.StressTests.dll -xml testResults.xml -nologo + + + + + $(StressTestsPayload) + $(_StressTestCommand) + $(WorkItemTimeout) + + + + + diff --git a/src/native/managed/cdac/tests/StressTests/known-issues.md b/src/native/managed/cdac/tests/StressTests/known-issues.md index 6445d255b67362..51815fca6a5100 100644 --- a/src/native/managed/cdac/tests/StressTests/known-issues.md +++ b/src/native/managed/cdac/tests/StressTests/known-issues.md @@ -1,57 +1,128 @@ # cDAC Stack Reference Walking — Known Issues This document tracks known gaps between the cDAC's stack reference enumeration -and the legacy DAC's `GetStackReferences`. +and the legacy DAC / runtime's GC stack scanning. ## Current Test Results -Using `DOTNET_CdacStress` with cDAC-vs-DAC comparison: - -| Mode | Non-EH debuggees (6) | ExceptionHandling | -|------|-----------------------|-------------------| -| INSTR (0x4 + GCStress=0x4, step=10) | 0 failures | 0-2 failures | -| ALLOC+UNIQUE (0x101) | 0 failures | 4 failures | -| Walk comparison (0x20, IP+SP) | 0 mismatches | N/A | - -## Known Issue: cDAC Cannot Unwind Through Native Frames - -**Severity**: Low — only affects live-process stress testing during active -exception first-pass dispatch. Does not affect dump analysis where the thread -is suspended with a consistent Frame chain. - -**Pattern**: `cDAC < DAC` (cDAC reports 4 refs, DAC reports 10-13). -ExceptionHandling debuggee only, 4 deterministic occurrences per run. - -**Root cause**: The cDAC's `AMD64Unwinder.Unwind` (and equivalents for other -architectures) can only unwind **managed** frames — it checks -`ExecutionManager.GetCodeBlockHandle(IP)` first and returns false if the IP -is not in a managed code range. This means it cannot unwind through native -runtime frames (allocation helpers, EH dispatch code, etc.). - -When the allocation stress point fires during exception first-pass dispatch: - -1. The thread's `m_pFrame` is `FRAME_TOP` (no explicit Frames in the chain - because the InlinedCallFrame/SoftwareExceptionFrame have been popped or - not yet pushed at that point in the EH dispatch sequence) -2. The initial IP is in native code (allocation helper) -3. The cDAC attempts to unwind through native frames but - `GetCodeBlockHandle` returns null for native IPs → unwind fails -4. With no Frames and no ability to unwind, the walk stops early - -The legacy DAC's `DacStackReferenceWalker::WalkStack` succeeds because -`StackWalkFrames` calls `VirtualUnwindToFirstManagedCallFrame` which uses -OS-level unwind (`RtlVirtualUnwind` on Windows, `PAL_VirtualUnwind` on Unix) -that can unwind ANY native frame using PE `.pdata`/`.xdata` sections. - -**Possible fixes**: -1. **Ensure Frames are always available** — change the runtime to keep - an explicit Frame pushed during allocation points within EH dispatch. - The cDAC cannot do OS-level native unwind (it operates on dumps where - `RtlVirtualUnwind` is not available). The Frame chain is the only - mechanism the cDAC has for transitioning through native code to reach - managed frames. If `m_pFrame = FRAME_TOP` when the IP is native, the - cDAC cannot proceed. -2. **Accept as known limitation** — these failures only occur during - live-process stress testing at a narrow window during EH first-pass - dispatch. In dumps, the exception state is frozen and the Frame chain - is consistent. +### Unit tests: 1374/1374 pass + +### ALLOC+WALK+USE_DAC (0x61) — Stack walk frame comparison +**7/7 debuggees: 100% clean (zero WALK_FAIL) when tested** + +### ALLOC+REFS+USE_DAC (0x51) — Three-way GC ref comparison + +| Debuggee | Result | Notes | +|----------|--------|-------| +| BasicAlloc | 0 failures | | +| Comprehensive | 0 failures | | +| DeepStack | 0 failures | | +| Generics | 0 failures | | +| MultiThread | 0 failures | | +| PInvoke | 0 failures | Windows only | +| DynamicMethods | 0 failures | | +| StructScenarios | 0 failures | | +| ExceptionHandling | 0 failures | Fixed via ExecutionAborted | + +## Issue 1: ELEMENT_TYPE_INTERNAL in PromoteCallerStack (instruction-level stress only) + +**Affected**: Explicit Frames whose method signature contains `ELEMENT_TYPE_INTERNAL` (0x21) +**Frequency**: ~3 per 25K verifications (0.01%) +**Root cause**: IDENTIFIED — follow-up fix needed + +**Where it happens**: `FrameIterator.PromoteCallerStack()` in +`src/native/managed/cdac/.../Contracts/StackWalk/FrameHandling/FrameIterator.cs` +(around line 604). This is the fallback path used when a Frame's GCRefMap is +unavailable and we must decode the method signature to determine which caller +arguments are GC references. + +**Pattern**: The DAC reports 1 ref from an explicit Frame that the cDAC fails to scan. +The `PromoteCallerStack` fallback decodes the method signature using +`System.Reflection.Metadata.SignatureDecoder`, which only handles standard ECMA-335 +type codes. Runtime-internal signatures (generated for IL stubs, marshalling stubs, +unsafe accessors, etc.) may contain `ELEMENT_TYPE_INTERNAL` (0x21), which encodes a +raw pointer-sized `TypeHandle` directly in the signature blob. The SRM decoder doesn't +recognize this type code and throws `BadImageFormatException`. + +``` +System.BadImageFormatException: Unexpected SignatureTypeCode: (0x21). + at SignatureDecoder`2.DecodeType(BlobReader&, Boolean, Int32) + at SignatureDecoder`2.DecodeGenericTypeInstance(BlobReader&) + at FrameIterator.PromoteCallerStack(...) + at FrameIterator.GcScanRoots(...) +``` + +The exception is caught by the per-frame exception handler in `WalkStackReferences()` +(`StackWalk_1.cs`, around line 245), which silently swallows it and continues the +walk — causing the Frame's GC refs to be unreported. + +**How the DAC handles it**: The native DAC uses `MetaSig` + `ArgIterator` +(`frames.cpp:1520-1596`) instead of the SRM decoder. `MetaSig` natively understands +`ELEMENT_TYPE_INTERNAL` — it reads the embedded TypeHandle pointer and follows it to +determine the actual type for GC classification. + +**How the Legacy cDAC handles it**: `SigFormat.cs` (line 157-175) already handles +`ELEMENT_TYPE_INTERNAL` by reading the pointer-sized TypeHandle, resolving it via +`RuntimeTypeSystem.GetTypeHandle()`, and checking `GetSignatureCorElementType()`. + +**Current workaround**: A `catch (BadImageFormatException)` in `PromoteCallerStack` +returns without reporting refs for the frame. + +**Follow-up fix**: Replace the SRM `SignatureDecoder` usage with a custom signature +walker that: +1. Pre-processes the signature bytes, handling `ELEMENT_TYPE_INTERNAL` (0x21) by + reading the pointer-sized TypeHandle and resolving through `RuntimeTypeSystem` + (following the pattern in `SigFormat.cs:157-175`) +2. Delegates standard ECMA-335 type codes to the existing `GcSignatureTypeProvider` +3. Handles `ELEMENT_TYPE_CMOD_INTERNAL` (0x22) similarly if encountered + +## IsFirst not preserved for skipped frames (FIXED) + +Previously ~4 per 25K failures at instruction-level stress. The cDAC's +`AdvanceIsFirst` was updating `IsFirst` for `SW_SKIPPED_FRAME` based on the +Frame's resumable attribute, but the native walker does NOT modify `isFirst` +in the `SFITER_SKIPPED_FRAME_FUNCTION` path (stackwalk.cpp:2086-2128). Fixed +by making `AdvanceIsFirst` skip the `IsFirst` update for `SW_SKIPPED_FRAME`. + +## EH ThrowHelper (FIXED) + +Previously 8-9 failures per run. Fixed by detecting `SoftwareExceptionFrame` +and `FaultingExceptionFrame` as interrupted frames and setting `ExecutionAborted` +flag, matching native `CrawlFrame::GetCodeManagerFlags`. + +## Allocation-level stress results + +At allocation-level stress (`DOTNET_CdacStress=0x51`, the default): +- All 9 debuggees pass 100% (0 failures across ~45K total verifications) + +## Instruction-level stress results + +At instruction-level stress (`DOTNET_GCStress=0x4 + DOTNET_CdacStress=0x54`): +- Comprehensive: 25,512 pass / 3 fail (99.988%) + - 0 FRAME_DIFF (fixed via IsFirst skipped-frame preservation) + - 3 FRAME_DAC_ONLY (ELEMENT_TYPE_INTERNAL in PromoteCallerStack — follow-up) + +## Future work + +- Investigate the GcInfo safe-point bitmap decoding difference for QCall frames +- Replace `fprintf`-based stress logging in `cdacstress.cpp` with a more + structured mechanism (e.g., ETW events or StressLog) for better tooling + integration and reduced I/O overhead during stress runs. + +## Log Format + +The stress log uses structured per-frame output with method name resolution: + +``` +[PASS] Thread=0x... IP=0x... cDAC=N DAC=N RT=M +[FAIL] Thread=0x... IP=0x... cDAC=N DAC=M RT=M + [COMPARE cDAC-vs-DAC] + [FRAME_DIFF] Source=0x... (MethodName): cDAC=X DAC=Y + [cDAC_ONLY] Addr=0x... Obj=0x... Flags=0x... + [DAC_ONLY] Addr=0x... Obj=0x... Flags=0x... + [FRAME_cDAC_ONLY] Source=0x... (MethodName): cDAC=X + [FRAME_DAC_ONLY] Source=0x... (): DAC=Y + [RT_DIFF] cDAC=N RT=M (cDAC matches DAC but differs from RT) + [STACK_TRACE] (cDAC=N DAC=M RT=M) + #i MethodName (cDAC=X DAC=Y) [<-- MISMATCH] +```