Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1e21c17
Including generic instantiations in stack frames
hez2010 Jan 3, 2024
44f2275
Fix another test
hez2010 Jan 3, 2024
250472a
Always keep generic context alive
hez2010 Jan 3, 2024
dbc1445
Only keep generic context when optimization is disabled
hez2010 Jan 3, 2024
a804a43
Add support for exception stacktrace as well
hez2010 Jan 3, 2024
dc44a0d
Update tests
hez2010 Jan 3, 2024
869f439
Try to fix the GC hole
hez2010 Jan 4, 2024
fbe823e
GC hole fixes take 2
hez2010 Jan 4, 2024
2048e41
Protect oRef
hez2010 Jan 4, 2024
9ec2532
Add some TODO comments
hez2010 Jan 4, 2024
504c3fe
Objects are not verifiable
hez2010 Jan 11, 2024
69ca3eb
Prettify stacktrace: strip assembly info for generic arguments
hez2010 Feb 21, 2024
fa307d5
Cleanup
hez2010 Feb 21, 2024
93ee9a7
Fix build failure
hez2010 Feb 22, 2024
60d2c0c
oops
hez2010 Feb 22, 2024
a1598bb
Fix AV
hez2010 Feb 23, 2024
f6e0743
oops
hez2010 Feb 23, 2024
e6cc527
Update tests
hez2010 Feb 23, 2024
a6d49b0
Move exact pFunc init to a later phase
hez2010 Feb 23, 2024
37502b1
Fix tests
hez2010 Feb 23, 2024
c2742cc
Apply NoOptimization to test stackframe
hez2010 Feb 23, 2024
359e396
More tests fixes
hez2010 Feb 23, 2024
07d86bb
Prevent bad MethodDesc being returned back for reflection.
hez2010 Feb 25, 2024
f3118e1
Introduce a switch
hez2010 Feb 25, 2024
70059c5
Update tests
hez2010 Feb 25, 2024
54790f7
Flip the switch
hez2010 Feb 25, 2024
8e9c556
Code cleanup
hez2010 Feb 26, 2024
6cc5a6a
Merge branch 'main' into feature/generic-stacktrace
hez2010 Mar 29, 2024
bd83c8a
Fix unmatched braces
hez2010 Mar 29, 2024
5465ff4
Try fix GC hole
hez2010 Apr 6, 2024
1a6c019
Fix contract
hez2010 Apr 6, 2024
58ec764
Fix debug assert
hez2010 Apr 6, 2024
657e2b3
Don't create new MT
hez2010 Apr 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,10 @@ internal void InitializeSourceInfo(int iSkip, bool fNeedFileInfo, Exception? exc
if (mh == IntPtr.Zero)
return null;

IRuntimeMethodInfo? mhReal = RuntimeMethodHandle.GetTypicalMethodDefinition(new RuntimeMethodInfoStub(mh, this));
IRuntimeMethodInfo? mhReal =
LocalAppContextSwitches.ShowGenericInstantiations
? RuntimeMethodHandle.FromIntPtr(mh).GetMethodInfo()
: RuntimeMethodHandle.GetTypicalMethodDefinition(new RuntimeMethodInfoStub(mh, this));

return RuntimeType.GetMethodBase(mhReal);
}
Expand Down
10 changes: 6 additions & 4 deletions src/coreclr/vm/debugdebugger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -391,13 +391,15 @@ FCIMPL4(void, DebugStackTrace::GetStackFramesInternal,
int iNumValidFrames = 0;
for (int i = 0; i < data.cElements; i++)
{
// The managed stacktrace classes always returns typical method definition, so we don't need to bother providing exact instantiation.
// Generics::GetExactInstantiationsOfMethodAndItsClassFromCallInformation(data.pElements[i].pFunc, data.pElements[i].pExactGenericArgsToken, &pExactMethod, &thExactType);
MethodDesc* pFunc = data.pElements[i].pFunc;

// Strip the instantiation to make sure that the reflection never gets a bad method desc back.
if (pFunc->HasMethodInstantiation())
// We need to strip the instantiations if the method desc is shared by generic instantiations
// to make sure the reflection never gets a bad method desc back.
// i.e. a method desc has System.__Canon in its instantiations
if (pFunc->HasClassOrMethodInstantiation() && pFunc->IsSharedByGenericInstantiations())
{
pFunc = pFunc->StripMethodInstantiation();
}
_ASSERTE(pFunc->IsRuntimeMethodHandle());

// Method handle
Expand Down
4 changes: 2 additions & 2 deletions src/coreclr/vm/exceptionhandling.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1659,7 +1659,7 @@ void ExceptionTracker::InitializeCrawlFrame(CrawlFrame* pcfThisFrame, Thread* pT
{
pcfThisFrame->isFrameless = true;
pcfThisFrame->pFunc = pcfThisFrame->codeInfo.GetMethodDesc();

pcfThisFrame->InitializeExactGenericInstantiations();
*puMethodStartPC = pcfThisFrame->codeInfo.GetStartAddress();
}
else
Expand Down Expand Up @@ -7583,7 +7583,7 @@ extern "C" void QCALLTYPE AppendExceptionStackFrame(QCall::ObjectHandleOnStack e
#if _DEBUG
EECodeInfo codeInfo(ip);
_ASSERTE(codeInfo.IsValid());
_ASSERTE(pMD == codeInfo.GetMethodDesc());
_ASSERTE(pMD->GetMemberDef() == codeInfo.GetMethodDesc()->GetMemberDef());
#endif // _DEBUG

pExInfo->m_StackTraceInfo.AppendElement(canAllocateMemory, ip, sp, pMD, &pExInfo->m_frameIter.m_crawl);
Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/vm/genmeth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ InstantiatedMethodDesc::FindLoadedInstantiatedMethodDesc(MethodTable *pExactOrRe
{
CONTRACT(InstantiatedMethodDesc *)
{
THROWS;
NOTHROW;
GC_NOTRIGGER;
FORBID_FAULT;
PRECONDITION(CheckPointer(pExactOrRepMT));
Expand Down
29 changes: 18 additions & 11 deletions src/coreclr/vm/jitinterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7779,21 +7779,28 @@ static void getMethodInfoHelper(

if (methInfo->options & CORINFO_GENERICS_CTXT_MASK)
{
#if defined(PROFILING_SUPPORTED)
BOOL fProfilerRequiresGenericsContextForEnterLeave = FALSE;
{
BEGIN_PROFILER_CALLBACK(CORProfilerPresent());
if ((&g_profControlBlock)->RequiresGenericsContextForEnterLeave())
{
fProfilerRequiresGenericsContextForEnterLeave = TRUE;
}
END_PROFILER_CALLBACK();
}
if (fProfilerRequiresGenericsContextForEnterLeave)
if (ftn->IsJitOptimizationDisabled())
{
methInfo->options = CorInfoOptions(methInfo->options|CORINFO_GENERICS_CTXT_KEEP_ALIVE);
Copy link
Member

Choose a reason for hiding this comment

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

@AndyAyersMS - Do you know if the JIT is currently preserving the generic context when optimizations are disabled somehow? I expected this would be something we already do but looking around at how CORINFO_GENERICS_CTXT_KEEP_ALIVE is used now that didn't seem to be the case.

Copy link
Member

Choose a reason for hiding this comment

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

It is not.

Generally speaking the generic context is only kept alive if we see the CORINFO_GENERICS_CTXT_KEEP_ALIVE flag from the jithost, or if there is a runtime lookup that uses the context.

}
else
{
#if defined(PROFILING_SUPPORTED)
BOOL fProfilerRequiresGenericsContextForEnterLeave = FALSE;
{
BEGIN_PROFILER_CALLBACK(CORProfilerPresent());
if ((&g_profControlBlock)->RequiresGenericsContextForEnterLeave())
{
fProfilerRequiresGenericsContextForEnterLeave = TRUE;
}
END_PROFILER_CALLBACK();
}
if (fProfilerRequiresGenericsContextForEnterLeave)
{
methInfo->options = CorInfoOptions(methInfo->options|CORINFO_GENERICS_CTXT_KEEP_ALIVE);
}
#endif // defined(PROFILING_SUPPORTED)
}
}

PCCOR_SIGNATURE pSig = NULL;
Expand Down
40 changes: 40 additions & 0 deletions src/coreclr/vm/stackwalk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,45 @@ PTR_VOID CrawlFrame::GetExactGenericArgsToken()
}
}

void CrawlFrame::InitializeExactGenericInstantiations()
{
CONTRACTL {
SUPPORTS_DAC;
NOTHROW;
GC_NOTRIGGER;
} CONTRACTL_END;

// Turn into cooperative GC mode
GCX_COOP();

if (pFunc != NULL && pFunc->HasClassOrMethodInstantiation() && pFunc->IsSharedByGenericInstantiations())
{
// Get exact instantiations for shared generics where possible
PTR_VOID pExactGenericArgsToken = GetExactGenericArgsToken();

if (pExactGenericArgsToken != NULL)
{
TypeHandle th;
MethodDesc* pConstructedFunc = NULL;
if (Generics::GetExactInstantiationsOfMethodAndItsClassFromCallInformation(pFunc, pExactGenericArgsToken, &th, &pConstructedFunc))
{
#ifndef DACCESS_COMPILE
if (pConstructedFunc->IsSharedByGenericInstantiations())
{
InstantiatedMethodDesc *pInstMD = InstantiatedMethodDesc::FindLoadedInstantiatedMethodDesc(th.GetMethodTable(), pConstructedFunc->GetMemberDef(), Instantiation(), false);

if (pInstMD != NULL)
{
pConstructedFunc = pInstMD;
}
Comment on lines +318 to +323
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It should really be

pConstructedFunc = InstantiatedMethodDesc::FindOrCreateExactClassMethod(th.GetMethodTable(), pConstructedFunc);

but this would possibly throw or trigger a GC.

}
#endif // !DACCESS_COMPILE
pFunc = pConstructedFunc;
}
}
}
}

/* Is this frame at a safe spot for GC?
*/
bool CrawlFrame::IsGcSafe()
Expand Down Expand Up @@ -3025,6 +3064,7 @@ void StackFrameIterator::ProcessCurrentFrame(void)
#endif // FEATURE_EH_FUNCLETS

m_crawl.pFunc = m_crawl.codeInfo.GetMethodDesc();
m_crawl.InitializeExactGenericInstantiations();

// Cache values which may be updated by CheckForSkippedFrames()
m_cachedCodeInfo = m_crawl.codeInfo;
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/vm/stackwalk.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ class CrawlFrame
These together carry the exact instantiation information.
*/
PTR_VOID GetExactGenericArgsToken();
void InitializeExactGenericInstantiations();

inline CodeManState * GetCodeManState() { LIMITED_METHOD_DAC_CONTRACT; return & codeManState; }
/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,16 @@ public static IEnumerable<object[]> ToString_TestData()
yield return new object[] { new StackFrame(), "MoveNext at offset {offset} in file:line:column {fileName}:{lineNumber}:{column}" + Environment.NewLine };
yield return new object[] { new StackFrame("FileName", 1, 2), "MoveNext at offset {offset} in file:line:column FileName:1:2" + Environment.NewLine };
yield return new object[] { new StackFrame(int.MaxValue), "<null>" + Environment.NewLine };
yield return new object[] { GenericMethod<string>(), "GenericMethod<T> at offset {offset} in file:line:column {fileName}:{lineNumber}:{column}" + Environment.NewLine };
yield return new object[] { GenericMethod<string, int>(), "GenericMethod<T,U> at offset {offset} in file:line:column {fileName}:{lineNumber}:{column}" + Environment.NewLine };
if (AppContext.TryGetSwitch("Switch.System.Diagnostics.StackTrace.ShowGenericInstantiations", out var showGenericInstantiations) && showGenericInstantiations)
Copy link
Member

Choose a reason for hiding this comment

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

I see this code that checks for the switch, but is there something which is setting the switch? I want to ensure the test is explicitly validating the behavior both with the switch enabled and disabled.

{
yield return new object[] { GenericMethod<string>(), "GenericMethod<System.String> at offset {offset} in file:line:column {fileName}:{lineNumber}:{column}" + Environment.NewLine };
yield return new object[] { GenericMethod<string, int>(), "GenericMethod<System.String,System.Int32> at offset {offset} in file:line:column {fileName}:{lineNumber}:{column}" + Environment.NewLine };
}
else
{
yield return new object[] { GenericMethod<string>(), "GenericMethod<T> at offset {offset} in file:line:column {fileName}:{lineNumber}:{column}" + Environment.NewLine };
yield return new object[] { GenericMethod<string, int>(), "GenericMethod<T,U> at offset {offset} in file:line:column {fileName}:{lineNumber}:{column}" + Environment.NewLine };
}
yield return new object[] { new ClassWithConstructor().StackFrame, ".ctor at offset {offset} in file:line:column {fileName}:{lineNumber}:{column}" + Environment.NewLine };
}

Expand All @@ -133,17 +141,17 @@ public void ToString_Invoke_ReturnsExpected(StackFrame stackFrame, string expect
Assert.Equal(expectedToString, stackFrame.ToString());
}

[MethodImpl(MethodImplOptions.NoInlining)]
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
private static StackFrame GenericMethod<T>() => new StackFrame();

[MethodImpl(MethodImplOptions.NoInlining)]
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
private static StackFrame GenericMethod<T, U>() => new StackFrame();

private class ClassWithConstructor
{
public StackFrame StackFrame { get; }

[MethodImpl(MethodImplOptions.NoInlining)]
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
public ClassWithConstructor() => StackFrame = new StackFrame();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,16 @@ public static IEnumerable<object[]> ToString_TestData()
yield return new object[] { NoParameters(), "System.Diagnostics.Tests.StackTraceTests.NoParameters()" };
yield return new object[] { OneParameter(1), "System.Diagnostics.Tests.StackTraceTests.OneParameter(Int32 x)" };
yield return new object[] { TwoParameters(1, null), "System.Diagnostics.Tests.StackTraceTests.TwoParameters(Int32 x, String y)" };
yield return new object[] { Generic<int>(), "System.Diagnostics.Tests.StackTraceTests.Generic[T]()" };
yield return new object[] { Generic<int, string>(), "System.Diagnostics.Tests.StackTraceTests.Generic[T,U]()" };
if (AppContext.TryGetSwitch("Switch.System.Diagnostics.StackTrace.ShowGenericInstantiations", out var showGenericInstantiations) && showGenericInstantiations)
{
yield return new object[] { Generic<int>(), "System.Diagnostics.Tests.StackTraceTests.Generic[System.Int32]()" };
yield return new object[] { Generic<int, string>(), "System.Diagnostics.Tests.StackTraceTests.Generic[System.Int32,System.String]()" };
}
else
{
yield return new object[] { Generic<int>(), "System.Diagnostics.Tests.StackTraceTests.Generic[T]()" };
yield return new object[] { Generic<int, string>(), "System.Diagnostics.Tests.StackTraceTests.Generic[T,U]()" };
}
yield return new object[] { new ClassWithConstructor().StackTrace, "System.Diagnostics.Tests.StackTraceTests.ClassWithConstructor..ctor()" };

// Methods belonging to the System.Diagnostics namespace are ignored.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,12 @@ public override string ToString()
else
fFirstTyParam = false;

sb.Append(typars[k].Name);
string typeName = typars[k].ToString();
for (int i = 0; i < typeName.Length; i++)
{
char ch = typeName[i];
sb.Append(ch == '+' ? '.' : ch);
}
k++;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,10 +249,10 @@ internal void ToString(TraceFormat traceFormat, StringBuilder sb)
if (declaringType != null)
{
// Append t.FullName, replacing '+' with '.'
string fullName = declaringType.FullName!;
for (int i = 0; i < fullName.Length; i++)
string fullNameWithoutAssemblyInfo = declaringType.ToString();
for (int i = 0; i < fullNameWithoutAssemblyInfo.Length; i++)
{
char ch = fullName[i];
char ch = fullNameWithoutAssemblyInfo[i];
sb.Append(ch == '+' ? '.' : ch);
}
sb.Append('.');
Expand All @@ -273,7 +273,12 @@ internal void ToString(TraceFormat traceFormat, StringBuilder sb)
else
fFirstTyParam = false;

sb.Append(typars[k].Name);
string typeName = typars[k].ToString();
for (int i = 0; i < typeName.Length; i++)
{
char ch = typeName[i];
sb.Append(ch == '+' ? '.' : ch);
}
k++;
}
sb.Append(']');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,24 @@ public static bool ShowILOffsets
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => GetDefaultShowILOffsetSetting();
}

private static int s_showGenericInstantiations;
private static bool GetDefaultShowGenericInstantiationsSetting()
{
if (s_showGenericInstantiations < 0) return false;
if (s_showGenericInstantiations > 0) return true;

// Disabled by default.
bool isSwitchEnabled = AppContextConfigHelper.GetBooleanConfig("Switch.System.Diagnostics.StackTrace.ShowGenericInstantiations", false);
s_showGenericInstantiations = isSwitchEnabled ? 1 : -1;

return isSwitchEnabled;
}

public static bool ShowGenericInstantiations
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => GetDefaultShowGenericInstantiationsSetting();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ public void StackTraceTest()
{
"System.Tests.EnvironmentStackTrace.StaticFrame(Object obj)",
"System.Tests.EnvironmentStackTrace.TestClass..ctor()",
"System.Tests.EnvironmentStackTrace.GenericFrame[T1,T2](T1 t1, T2 t2)",
AppContext.TryGetSwitch("Switch.System.Diagnostics.StackTrace.ShowGenericInstantiations", out var showGenericInstantiations)
?
showGenericInstantiations
? "System.Tests.EnvironmentStackTrace.GenericFrame[System.DateTime,System.Text.StringBuilder](DateTime t1, StringBuilder t2)"
: "System.Tests.EnvironmentStackTrace.GenericFrame[T1,T2](T1 t1, T2 t2)"
: "System.Tests.EnvironmentStackTrace.GenericFrame[T1,T2](T1 t1, T2 t2)"
,
"System.Tests.EnvironmentStackTrace.TestFrame()"
};

Expand All @@ -40,27 +46,27 @@ public void StackTraceTest()
}
}

[MethodImpl(MethodImplOptions.NoInlining)]
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
Copy link
Member

Choose a reason for hiding this comment

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

I don't want to weaken the existing stackwalking test by only validating it against unoptimized code. You could create some alternate frames that are unoptimized and call those instead if the switch is set.

(The same thing applies in the other tests)

Copy link
Member

Choose a reason for hiding this comment

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

I also think we should explicitly have a test that demonstrates the behavior of a stack trace that occurs in optimized shared generic code where the shared generics context is no longer in scope. Even if it doesn't have exact type naming we still want to ensure it degrades gracefully rather than doing anything bad (like crashing).

Was there ever an agreed upon behavior for what text gets shown for the generic parameters in an optimized shared generic frame? I believe the text "System.__Canon" has already leaked out in a few other places such as the error handling text @hez2010 mentioned or it has shown up in VS debugger callstacks for many years when debugging optimized generic code:
image

I feel like the cat is already out of the bag in terms of the revealing that type name so using it here is aligned with precedent elsewhere. I'd also be fine reverting to use the type parameter name if you think its worth trying to get the cat back into the bag @jkotas?

Copy link
Member

Choose a reason for hiding this comment

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

We do not leak __Canon through public APIs. We have always treated leaking __Canon through public APIs as bug.

I agree with #96440 (comment) .

private void TestFrame()
{
GenericFrame<DateTime, StringBuilder>(DateTime.Now, null);
}

[MethodImpl(MethodImplOptions.NoInlining)]
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
private void GenericFrame<T1, T2>(T1 t1, T2 t2)
{
new TestClass();
}

[MethodImpl(MethodImplOptions.NoInlining)]
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
private static void StaticFrame(object obj)
{
s_stackTrace = Environment.StackTrace;
}

private class TestClass
{
[MethodImpl(MethodImplOptions.NoInlining)]
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
public TestClass()
{
StaticFrame(null);
Expand Down