-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Update Stepping through GenericPInvokeCalli Stub under the Debugger #110677
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Update Stepping through GenericPInvokeCalli Stub under the Debugger #110677
Conversation
| UNINSTALL_UNWIND_AND_CONTINUE_HANDLER; | ||
| UNINSTALL_MANAGED_EXCEPTION_DISPATCHER; | ||
|
|
||
| if (g_genericPInvokeCalliHelperTraceActiveCount > 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the case where the IL Stub is already made, it does not go down this path even though the trigger is enabled. I need to figure out how to disable the trigger in that scenario.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the ILStub is made, the path of execution is through JIT_ReversePInvokeEnterRare2 and into TraceCall. Eventually, a managed patch is set and then DisableAll is called, so the GenericPInvokeCalli trigger will be disabled with that call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there some state we can assert here or there?
In the case where the IL Stub is already made
What is this path then? How is this path different from the other, with respect to the IL Stub state?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After returning from the exception that was the step into, we eventually make it to HCIMPL1_RAW(void, JIT_ReversePInvokeEnter, ReversePInvokeFrame* frame), which is some kind of jitHelper. This calls into JIT_ReversePInvokeEnterRare2, which calls into TraceCall. We then go into DispatchTraceCall, which calls TriggerTraceCall because we are working with a LEAF_MOST_FRAME. We go into an if statement with TraceStub, FollowTrace and PatchTrace. TraceStub decides we are managed code and followTrace agrees and a managed trace patch is set in PatchTrace. All three functions (TS, FT and PT) returned true, so inside of TriggerTraceCall, we DisableAll() which disables all triggers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit confused by the description here. In the code above it looks like we are creating an ILStub that facilitates a call from managed code to native code (a pinvoke). However in your description you say the code is executing JIT_ReversePInvokeEnter which is code I'd expect to run when a native function is calling a managed function, the reverse of a pinvoke. I'd like to understand better what does the test case you are running look like and what methods are calling one another. My worry is that you might have a test-case that is relying on the reverse pinvoke to be successful, but some other test case that doesn't include the reverse pinvoke might fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the test case I am using:
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
static int DoubleInt(int i) => i * 2;
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
static float MultiplyInt (float a, float b) => (a * b);
static unsafe void Function()
{
var fnPtr = (delegate* unmanaged[Cdecl]<int,int>) &DoubleInt;
var fnPtr2 = (delegate* unmanaged[Cdecl]<float,float, float>) &MultiplyInt;
fnPtr(21); // step into me
Console.WriteLine("We are in the middle");
fnPtr2(1.5f, 2.5f); //
fnPtr(34); // step into me
Console.WriteLine("We made it");
}
Function();
The case discussed above happens when we step into the following line: fnPtr(34);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AaronRobinsonMSFT - does it match your understanding that a calli to managed method marked UnmanagedCallersOnly would first do a pinvoke followed by a reverse p/invoke? That sounds plausible to me but I've never looked into the details of the implementation.
@mikelle-rogers - I'd suggest trying an alternate test case where you write the native function in a separate dll using C++ and call that. I'd expect you will find that the reverse pinvoke part disappears. @AaronRobinsonMSFT - do you know of any good examples of this? I poked around a little bit but of the few examples I saw it was always passing a managed callback to native code rather than passing a native callback to managed code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UnmanagedCallersOnly would first do a pinvoke followed by a reverse p/invoke?
@noahfalk When you say "do a pinvoke", what does that mean in this case? Meaning, what part of the machinery are you referring to as pinvoke?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, let me try to be more precise :) By 'Do a pinvoke' I was refering to the calli executing down the code path commented here GetILStubForCalli(...), followed by invoking the ILStub that is generated in this method. PInvoke may not have been accurate terminology for that part. Does that make more sense?
|
Tagging subscribers to this area: @tommcdon |
src/coreclr/debug/ee/controller.cpp
Outdated
| TraceDestination trace; | ||
| FramePointer fp = LEAF_MOST_FRAME; | ||
| trace.InitForStub(target); | ||
| g_pEEInterface->FollowTrace(&trace); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we check the return value here and conditionally execute PatchTrace?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, FollowTrace should always return True. Should we assert that that is the case? Then, if there is an issue (or a case that I do not know about) we will find it easily.
src/coreclr/debug/ee/controller.cpp
Outdated
| trace.InitForStub(target); | ||
| g_pEEInterface->FollowTrace(&trace); | ||
| //fStopInUnmanaged only matters for TRACE_UNMANAGED | ||
| PatchTrace(&trace, fp, /*fStopInUnmanaged*/false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as above, check the return value? If it fails, should we avoid calling DisableGenericPInvokeCalli?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same goes for this one. PatchTrace should return true always, so maybe we assert that it has? And if it hasn't then that possible provides us with an easy issue to track?
tommcdon
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left a couple comments for your review, but otherwise LGTM!
| FramePointer fp = LEAF_MOST_FRAME; | ||
| trace.InitForStub(target); | ||
| bool hasTraceType = g_pEEInterface->FollowTrace(&trace); | ||
| //fStopInUnmanaged only matters for TRACE_UNMANAGED |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: it seems a little odd to have an unrelated assert in between the comment and the line it is commenting on
| //fStopInUnmanaged only matters for TRACE_UNMANAGED | |
| _ASSERTE(hasTraceType); | |
| //fStopInUnmanaged only matters for TRACE_UNMANAGED | |
| bool setPatch = PatchTrace(&trace, fp, /*fStopInUnmanaged*/false); |
|
@tommcdon, what are the next steps for this PR? Thanks. |
@stephentoub I believe there was some testing that needs to done and if that looked good this was very close to ready. @noahfalk did you have any other concerns? |
Based on #110677 (comment) it wasn't clear that we've gotten calls into the correct runtime code paths. Mikelle's explanation sounded like the current code relied on a reverse p/invoke to occur during the operation but I wouldn't expect all scenarios involving calli to contain a reverse p/invoke. Testing a call to a pure C++ dll may reveal changes are needed. |
Create a trigger and new trace type for the GenericPInvokeCalli stub.