Подтвердить что ты не робот

Как работает Thread.Abort()?

Обычно мы генерируем исключение, когда неверный ввод передается методу или когда объект собирается ввести недопустимое состояние. Рассмотрим следующий пример

private void SomeMethod(string value)
{
    if(value == null)
        throw new ArgumentNullException("value");
    //Method logic goes here
}

В приведенном выше примере я вставил оператор throw, который выдает ArgumentNullException. Мой вопрос заключается в том, как runtime удается бросить ThreadAbortException. Очевидно, что во всех методах невозможно использовать оператор throw, даже время выполнения управляет тем, что бросает ThreadAbortException в наши собственные методы.

Мне было интересно, как они это делают? Мне было любопытно узнать, что происходит за кулисами, я открыл рефлектор, чтобы открыть Thread.Abort и в конечном итоге с этим

[MethodImplAttribute(MethodImplOptions.InternalCall)]
private extern void AbortInternal();//Implemented in CLR

Затем я googled и нашел это Как работает ThreadAbortException. Эта ссылка говорит, что runtime отправляет APC через QueueUserAPC функцию и то, как они делают трюк. Я не знал о методе QueueUserAPC, я просто попытался выяснить, возможно ли это с помощью некоторого кода. Следующий код показывает мою попытку.

[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
}

Если я делаю что-то неправильно, простите меня, я понятия не имею, как это сделать. Снова вернемся к вопросу. Может ли кто-нибудь, кто знает об этом или части команды CLR, объяснить, как он работает внутри страны? Если APC - это трюк, выполняющийся после того, что вы делаете неправильно здесь?

4b9b3361

Ответ 1

Вы уверены, что читаете страницу, на которую указываете? В конце концов это сводится к:

Вызов Thread.Abort сводится к тому, что .NET устанавливает флаг в потоке, который нужно прервать, и затем проверяет этот флаг в течение определенных точек в потоке жизни, бросая исключение, если флаг установлен.

Ответ 2

Чтобы получить обратный вызов APC для работы, вам понадобится дескриптор потока (который не совпадает с идентификатором потока). Я также обновил атрибуты на PInvokes.

Также имейте в виду, что поток должен находиться в состоянии ожидания "alert-able" для вызова APC (который нам предоставит Thread.Sleep). Поэтому, если поток занят работой, он не может быть вызван.

[DllImport("kernel32.dll", EntryPoint = "GetCurrentThread", CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr GetCurrentThread();

[DllImport("kernel32.dll", EntryPoint = "QueueUserAPC", CallingConvention = CallingConvention.StdCall, SetLastError = true)]
public static extern uint QueueUserAPC(ApcDelegate pfnAPC, IntPtr hThread, UIntPtr dwData);

[UnmanagedFunctionPointerAttribute(CallingConvention.StdCall)]
public delegate void ApcDelegate(UIntPtr dwParam);

[DllImport("kernel32.dll", EntryPoint = "DuplicateHandle", CallingConvention = CallingConvention.StdCall, SetLastError = true)]
public static extern bool DuplicateHandle([In] System.IntPtr hSourceProcessHandle, [In] System.IntPtr hSourceHandle, [In] System.IntPtr hTargetProcessHandle, out System.IntPtr lpTargetHandle, uint dwDesiredAccess, [MarshalAsAttribute(UnmanagedType.Bool)] bool bInheritHandle, uint dwOptions);

[DllImport("kernel32.dll", EntryPoint = "GetCurrentProcess", CallingConvention = CallingConvention.StdCall, SetLastError = true)]
public static extern IntPtr GetCurrentProcess();


static IntPtr hThread;
public static void SomeMethod(object value)
{
    DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), out hThread, 0, false, 2);

    while (true)
    {
        Console.WriteLine(".");
        Thread.Sleep(1000);
    }
}

private static void APC(UIntPtr data)
{
    Console.WriteLine("Callback invoked");
}

static void Main(string[] args)
{
    Console.WriteLine("in Main\n");

    Thread t = new Thread(Program.SomeMethod);
    t.Start();

    Thread.Sleep(1000); // wait until the thread fills out the hThread member -- don't do this at home, this isn't a good way to synchronize threads...
    uint result = QueueUserAPC(APC, hThread, (UIntPtr)0);

    Console.ReadLine();
}


Редактировать:
Как CLR вводит исключение
Учитывая этот цикл для функции потока:

while (true)
{
    i = ((i + 7) * 3 ^ 0x73234) & 0xFFFF;
}

Затем я .Abort редактировал поток и смотрел на собственную трассировку стека

...
ntdll!KiUserExceptionDispatcher
KERNELBASE!RaiseException
clr!RaiseComPlusException
clr!RedirectForThrowControl2
clr!RedirectForThrowControl_RspAligned
clr!RedirectForThrowControl_FixRsp
csTest.Program.SomeMethod(System.Object)
...

Глядя на обратный адрес вызова RedirectForThrowControl_FixRsp, он указывает на середину моего цикла, для которого нет переходов или вызовов:

nop
mov     eax,dword ptr [rbp+8]
add     eax,7 // code flow would return to execute this line
lea     eax,[rax+rax*2]
xor     eax,73234h
and     eax,0FFFFh
mov     dword ptr [rbp+8],eax
nop
mov     byte ptr [rbp+18h],1
jmp     000007fe`95ba02da // loop back to the top

Таким образом, очевидно, что CLR фактически модифицирует указатель инструкции рассматриваемого потока, чтобы физически управлять yank из нормального потока. Очевидно, что им необходимо предоставить несколько оберток для исправления и восстановить все регистры стека, чтобы они работали корректно (таким образом, aptly назывались _FixRsp и _RspAligned API.


В отдельном тесте у меня просто были вызовы Console.Write() внутри цикла потока, и там было похоже, что CLR вводил тест непосредственно перед физическим вызовом WriteFile:

KERNELBASE!RaiseException
clr!RaiseTheExceptionInternalOnly
clr! ?? ::FNODOBFM::`string'
clr!HelperMethodFrame::PushSlowHelper
clr!JIT_RareDisableHelper
mscorlib_ni!DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
mscorlib_ni!System.IO.__ConsoleStream.WriteFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Boolean)

Ответ 3

Чтобы получить QueueUserAPC для работы, вам нужно сделать две вещи.

  • Приобретите целевой дескриптор потока. Обратите внимание, что это не то же самое, что и собственный идентификатор потока.
  • Разрешить целевой поток перейти в аварийное состояние.

Вот полная программа, которая демонстрирует это.

class Program
{
    [DllImport("kernel32.dll", EntryPoint = "DuplicateHandle", CallingConvention = CallingConvention.StdCall, SetLastError = true)]
    public static extern bool DuplicateHandle([In] System.IntPtr hSourceProcessHandle, [In] System.IntPtr hSourceHandle, [In] System.IntPtr hTargetProcessHandle, out System.IntPtr lpTargetHandle, uint dwDesiredAccess, [MarshalAsAttribute(UnmanagedType.Bool)] bool bInheritHandle, uint dwOptions);

    [DllImport("kernel32.dll", EntryPoint = "GetCurrentProcess", CallingConvention = CallingConvention.StdCall, SetLastError = true)]
    public static extern IntPtr GetCurrentProcess();

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetCurrentThread();

    [DllImport("kernel32.dll")]
    private static extern uint QueueUserAPC(ApcMethod pfnAPC, IntPtr hThread, UIntPtr dwData);

    private delegate void ApcMethod(UIntPtr dwParam);

    static void Main(string[] args)
    {
        Console.WriteLine("Main: " + Thread.CurrentThread.ManagedThreadId);
        IntPtr threadHandle = IntPtr.Zero;
        var threadHandleSet = new ManualResetEvent(false);
        var apcSet = new ManualResetEvent(false);
        var thread = new Thread(
            () =>
            {
                Console.WriteLine("thread started");
                threadHandle = GetCurrentThread();
                DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), out threadHandle, 0, false, 2);
                threadHandleSet.Set();
                apcSet.WaitOne();
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("thread waiting");
                    Thread.Sleep(1000);
                    Console.WriteLine("thread running");
                }
                Console.WriteLine("thread finished");
            });
        thread.Start();
        threadHandleSet.WaitOne();
        uint result = QueueUserAPC(DoApcCallback, threadHandle, UIntPtr.Zero);
        apcSet.Set();
        Console.ReadLine();
    }

    private static void DoApcCallback(UIntPtr dwParam)
    {
        Console.WriteLine("DoApcCallback: " + Thread.CurrentThread.ManagedThreadId);
    }

}

Это существенно позволяет разработчику внедрить выполнение метода в любой произвольный поток. Целевой поток не обязательно должен иметь насос сообщений, как это было бы необходимо для традиционного подхода. Одна из проблем с этим подходом состоит в том, что целевой поток должен находиться в аварийном состоянии. Поэтому в основном поток должен вызывать один из консервированных вызовов блокировки .NET, таких как Thread.Sleep, WaitHandle.WaitOne и т.д. Для очереди APC для выполнения.

Ответ 4

Я загрузил код SSCLI и начал соваться. Мне сложно выполнить код (в основном потому, что я не специалист по С++ или ASM), но я вижу много крючков, где прерывания вводятся полусинхронно.

  • попытка/улов/окончательная/обработка управления блоком блокировки
  • Активация GC (выделение памяти)
  • проксирование через мягкие прерывания (например, с Thread.Interrupt), когда в аварийном состоянии
  • перехватывание виртуальных вызовов
  • Подготовка к закрытию хвоста JIT
  • неуправляемые управляемые переходы

Это просто назвать несколько. Я хотел знать, как были введены асинхронные прерывания. Общая идея захвата указателя инструкций является частью того, как это происходит. Однако это намного сложнее, чем то, что я описал выше. Похоже, что идиома Suspend-Modify-Resume всегда используется. Из кода SSCLI я вижу, что он приостанавливает и возобновляет поток в определенных сценариях для подготовки к захвату, но это не всегда так. Мне кажется, что захват может произойти, когда поток также работает с полным отверстием.

В статье, на которой вы ссылались, упоминается, что в целевом потоке установлен флаг прерывания. Это технически правильно. Флаг называется TS_AbortRequested, и существует много логики, которая управляет установкой этого флага. Существуют проверки для определения того, существует ли ограниченная область выполнения и существует ли поток в настоящее время в блоке try-catch-finally-fault. Некоторые из этих работ включают сканирование стека, которое означает, что поток должен быть приостановлен и возобновлен. Однако, как обнаружено изменение флага, происходит реальная магия. В статье это не очень хорошо объясняется.

Я уже упомянул несколько полусинхронных точек впрыска в приведенном выше списке. Это должно быть довольно тривиально, чтобы понять. Но как происходит асинхронная инъекция? Ну, мне кажется, что JIT - это волшебник за шторкой здесь. В JIT/GC есть какой-то механизм опроса, который периодически определяет, должна ли собираться коллекция. Это также дает возможность проверить, изменилось ли какое-либо из управляемых потоков состояния (например, установлен флаг флага прерывания). Если TS_AbortRequested задано, тогда уловка происходит тогда и там.

Если вы смотрите на код SSCLI, здесь есть несколько хороших функций для просмотра.

  • HandleThreadAbort
  • CommonTripThread
  • JIT_PollGC
  • JIT_TailCallHelper
  • COMPlusCheckForAbort
  • ThrowForFlowControl
  • JIT_RareDisableHelper

Есть много других подсказок. Имейте в виду, что это SSCLI, поэтому имена методов могут не совпадать точно с стеками вызовов, наблюдаемыми в процессе производства (например, что Джош Поули обнаружил), но будут сходства. Кроме того, большая часть захвата потока выполняется с помощью ассемблерного кода, поэтому иногда трудно следовать. Я выделил JIT_PollGC, потому что я считаю, что это то, где происходит интересное. Я считаю, что JIT будет динамически и стратегически помещаться в исполняемый поток. Это в основном механизм того, как эти тесные петли все еще могут получать инъекции прерывания. Целевой поток действительно по существу опроса для запроса прерывания, но как часть более крупной стратегии для вызова GC 1

Так ясно, что JIT, GC и прерывания потоков тесно связаны. Это очевидно, когда вы смотрите на код SSCLI. В качестве примера метод, используемый для определения безопасных точек для прерываний потока, такой же, как тот, который используется для определения того, разрешено ли выполнение GC.


1Общий интерфейс CLI Essentials, Дэвид Штуц, 2003 г., стр. 249-250

Ответ 5

Это легко, базовая ОС делает это. Если поток находится в каком-либо состоянии, кроме "работает на другом ядре", нет проблем - в этом состоянии установлено значение "никогда не запускаться снова". Если поток работает на другом ядре, аппаратное обеспечение ОС прерывает другое ядро ​​через. это межпроцессорный драйвер и таким образом уничтожает поток.

Любое упоминание "временного фрагмента", "кванта" и т.д. просто.....