We usually throw exception when invalid input is passed to a method or when a object is about to enter invalid state. Let's consider the following example
private void SomeMethod(string value)
{
if(value == null)
throw new ArgumentNullException("value");
//Method logic goes here
}
In the above example I inserted a throw statement which throws ArgumentNullException
. My question is how does runtime manages to throw ThreadAbortException
. Obviously it is not possible to use a throw
statement in all the methods, even runtime manages to throw ThreadAbortException
in our custom methods too.
I was wondering how do they do it?
I was curious to know what is happening behind the scenes, I opened a reflector to open Thread.Abort
and end up with this
[MethodImplAttribute(MethodImplOptions.InternalCall)]
private extern void AbortInternal();//Implemented in CLR
Then I googled and found this How does ThreadAbortException really work. This link says that runtime posts APC through QueueUserAPC
function and that's how they do the trick.
I wasn't aware of QueueUserAPC
method I just gave a try to see whether it is possible with some code. Following code shows my try.
[DllImport("kernel32.dll")]
static extern uint QueueUserAPC(ApcDelegate pfnAPC, IntPtr hThread, UIntPtr dwData);
delegate void ApcDelegate(UIntPtr dwParam);
Thread t = new Thread(Threadproc);
t.Start();
//wait for thread to start
uint result = QueueUserAPC(APC, new IntPtr(nativeId), (UIntPtr)0);//returns zero(fails)
int error = Marshal.GetLastWin32Error();// error also zero
private static void APC(UIntPtr data)
{
Console.WriteLine("Callback invoked");
}
private static void Threadproc()
{
//some infinite loop with a sleep
}
If am doing something wrong forgive me, I have no idea how to do it. Again back to question, Can somebody with knowledge about this or part of CLR team explain how it works internally?
If APC
is the trick runtime follows what am doing wrong here?
Are you sure you read the page you were pointing to? In the end it boils down to:
To get your APC callback to work, you need a thread handle (which is not the same as the thread ID). I've also updated the attributes on the PInvokes.
Also keep in mind that the thread needs to be in an "alert-able" wait state in order for the APC to be called (which Thread.Sleep will give us). So if the thread is busy doing stuff, it may not be called.
Edit:
How the CLR injects the exception
Given this loop for the thread function:
I then
.Abort
ed the thread and looked at the native stack traceLooking at the return address of the
RedirectForThrowControl_FixRsp
call, it is pointing into the middle of my loop, for which there are no jumps or calls:So apparently the CLR is actually modifying the instruction pointer of the thread in question to physically yank control from the normal flow. They obviously needed to supply several wrappers to fixup and restore all the stack registers to make it work correctly (thus the aptly named
_FixRsp
and_RspAligned
APIs.In a separate test, I just had
Console.Write()
calls within my thread loop, and there it looked like the CLR injected a test just before the physical call out toWriteFile
:To get the QueueUserAPC to work you have to do two things.
Here is a complete program that demonstrates this.
This essentially allows a developer to inject the execution of a method into any arbitrary thread. The target thread does not have to have a message pump like would be necessary for the traditional approach. One problem with this approach though is that the target thread has to be in an alertable state. So basically the thread must call one of the canned .NET blocking calls like
Thread.Sleep
,WaitHandle.WaitOne
, etc. for the APC queue to execute.I downloaded the SSCLI code and started poking around. The code is difficult for me to follow (mostly because I am not a C++ or ASM expert), but I do see a lot of hooks where the aborts are injected semi-synchronously.
That is just to name a few. What I wanted to know was how asynchronous aborts were injected. The general idea of hijacking the instruction pointer is part of how it happens. However, it is far more complex than what I described above. It does not appear that a Suspend-Modify-Resume idiom is always used. From the SSCLI code I can see that it does suspend and resume the thread in certain scenarios to prepare for the hijack, but this is not always the case. It looks to me that the hijack can occur while the thread is running full bore as well.
The article you linked to mentions that an abort flag is set on the target thread. This is technically correct. The flag is called
TS_AbortRequested
and there is a lot of logic that controls how this flag is set. There are checks for determining if a constrained execution region exists and whether the thread is currently in a try-catch-finally-fault block. Some of this work involves a stack crawl which means the thread must be suspended and resumed. However, how the change of the flag is detected is where the real magic happens. The article does not explain that very well.I already mentioned several semi-synchronous injection points in the list above. Those should be pretty trivial to understand. But, how does the asynchronous injection happen exactly? Well, it appears to me that the JIT is the wizard behind by the curtain here. There is some kind of polling mechanism built into the JIT/GC that periodically determines if a collection should occur. This also provides an opportunity to check to see if any of the managed threads have changed state (like having the abort flag set). If
TS_AbortRequested
is set then the hijack happens then and there.If you are looking at the SSCLI code here are some good functions to look at.
There are many other clues. Keep in mind that this is the SSCLI so the method names may not match exactly with call stacks observed in production (like what Josh Poley discovered), but there will be similarities. Also, a lot of the thread hijacking is done with assembly code so it is hard to follow at times. I highlighted
JIT_PollGC
because I believe this is where the interesting stuff happens. This is the hook that I believe the JIT will dynamically and strategically place into the executing thread. This is basically the mechanism for how those tight loops can still receive the abort injections. The target thread really is essentially polling for the abort request, but as part of a larger strategy to invoke the GC1So clearly the JIT, GC, and thread aborts are intimately related. It is obvious when you look at the SSCLI code. As an example, the method used to determine the safe points for thread aborts is the same as the one used to determine if the GC is allowed to run.
1Shared Source CLI Essentials, David Stutz, 2003, pg. 249-250
It's easy, the underlying OS does it. If the thread is in any state except 'running on another core', there is no problem - it's state is set to 'never run again'. If the thread is runing on another core, the OS hardware-interrupts the other core via. it's interprocessor driver and so exterminates the thread.
Any mention of 'time-slice', 'quantum' etc. is just.....