«Угон» потоков.
Бывает так, что требуется выполнить свой код в контексте произвольного потока. Либо в своем потоке, но в то время когда поток выполняет чужой код. Например, сборщик мусора может хотеть перехватить управление, даже если поток крутит бесконечный цикл. Один из методов перехвата – использование функций GetThreadContext и SetThreadContext. Эти функции позволяют манипулировать контекстом потока – т.е. состоянием регистров процессора, в том числе и указателем на текущую выполняемую инструкцию. В простейшем случае перехватчик приостанавливает поток, сохраняет текущий контекст, модифицирует EIP, чтобы тот показывал на нужный код, и снова запускает поток. При обратном переключении просто восстанавливается ранее сохраненный контекст и все. Метод простой, эффективный и … неработающий.
Чтобы разобраться в чем тут дело, рассмотрим как работают функции GetThreadContext и SetThreadContext. Когда процессор переключается в режим ядра, операционная система сохраняет состояние регистров пользовательского режима в ядерном стеке. При обратном переключении, сохраненные значения вновь загружаются в регистры процессора. GetThreadContext и SetThreadContext принудительно переключает поток в режим ядра (посылают потоку kernel APC, фактически – прерывание) и читает или модифицирует пользовательский контекст прямо в стеке. Важное следствие такого подхода – поток может быть прерван в любой момент, за исключением немногочисленных участков ядерного кода, защищенных от kernel APC.
Проблема первая: при переходе в режим ядра не всегда сохраняются все регистры. С точки зрения NT существует три способа переключения в режим ядра: системный вызов, исключение (trap) и прерывание. При вызове системной функции в соответствии с соглашением о вызовах не сохраняются volatile регистры. Ну а раз они не сохраняются, то и восстанавливаться они не будут. Соответственно попытка установить с помощью SetThreadContext, скажем EAX, может и не увенчается успехом.
Проблема вторая: GetThreadContext и SetThreadContext – не единственные, кто модифицирует состояние регистров пользовательского режима. Существует функция NtContinue и диспетчер прерываний. Последний особенно интересен:
xor eax, eax ; eax = 0
mov [eax], eax ; [0] = 0 – Access Violation!
С точки зрения приложения, результат выполнения второй инструкции состоит в том, что состояние регистров процессора (CONTEXT) и информация об исключении (EXCEPTION_RECORD) сохраняется в стеке, а управление передается на код диспетчера прерываний в NTDLL. Этакая «мега» инструкция. Конечно, вся эта работа выполняется ядром OS, но проблема состоит в том, что эта «мега» инструкция – неатомарная. Поток может быть прерван в процессе обработки исключения в ядре. С точки зрения пользовательского кода – прямо в середине инструкции. Эффект от SetThreadContext в этот момент не предсказуем. Поток либо переключится на контекст, переданный в SetThreadContext, либо диспетчер исключений переключит поток на код диспетчера прерываний в NTDLL. Все зависит от того, кто установит пользовательский контекст последним.
Проблема третья: Wow64. Если совсем коротко, то точка входа в 64-х битный код сидит в ring 3. По аналогии с точками входа в ядро, этот код сохраняет 32-х битное состояние процессора. В отличие точек входа в ядро, код в Wow64 абсолютно не защищен от прерывания с помощью kernel APC. В результате в некоторых редких ситуациях GetThreadContext может просто возвращать неправильный 32-х битный контекст (хотя Wow64 очень старается, чтобы обойти такие ситуации).
Вот такая вот печальная история…
PS. А теперь давайте посчитаем количество комментариев «Windows – must die».
Long live Ubuntu!
Логичный вопрос – и что можно сделать, если все-таки надо?
Интересно, не задумывался об этом.
Получается, что совсем никак нельзя “попросить” нить выполнить код помимо QueueUserAPC? Конечно, хорошо бы, чтобы поток прервался не “прямо сейчас”, а когда это относительно безопасно – т.е., как позиксовый сигнал.
От неправильного 32-битного контекста в Wow64 как-нибудь можно защититься, чтобы можно было спокойно контекст читать?
Троянописатель врывается в тред: можно перехватить прерывание системного таймера и “дождавшись” нужного потока выполнить возврат из обработчика прерывания в свой код (естественно, сохранив оригинальные значения значения esp, cs:eip и остальных регистров).
очередной “архитектурный” чирий винды в попытке угодить всем подряд. уже забыли и про очереди и про сериализацию и про то, что через дверь ходить удобнее и правильнее, нежели лезть в окно. R.I.P. – закапывайте.
Попросить можно тысячей и одним способом. Но для этого поток должен или ждать запроса, или периодически проверять не пришел ли запрос. Принудительно прервать произвольный поток в подходящем месте гораздо сложнее.
Можно например делать SuspendThread и анализировать тип trap frame. В данный момент это работает на amd64 и ia64. Если написать свой драйвер, то и x86 будет тоже работать. Но не Wow64, по крайней мере не Wow64 на Itanium.
Пожалуй стоит написать пость с разбором разных вариантов.
На amd64 работает вариант с проверкой типа trap frame. На Itanium – контекст возвращается правильный, но узнать, что вот вот случиться прерывание нельзя.
Можно и так с оговоркой, что это не работает для Wow64 и требует проверки типа trap frame.
@Vital Zanko
@Gigabyte
3:2. Нормальные собеседники лидируют с минимальным отрывом.
Я не программист. Посему, админский вопрос. Вот, чегото я видимо не пойму. Если поток является единицей планирования, то что мешает запустить свой поток рядом и просто дождаться, когда планировщик передаст управление? Зачем вообще все эти прыжки в сторону?
Параллельный поток даст возможность выполнить свой код. Перехват чужого потока позволяет контролировать чужой код. Ну еще и скрытно выполнять свой код, но это в основном троянописателям интересно.
>Зачем вообще все эти прыжки в сторону?
Это надо для:
1) асинхронного взаимодействия нитей (т.е., чтобы нить отреагировала на что-то как можно скорее как только появится такая возможность)
2) корректного кооперативного завершения нитей (т.е., внутри нити возбуждается исключение, которое разворачивает стек и вызывает все обработчики). – Кстати, а как в .NET CLR реализован Thread.Abort: каким образом он кидает исключение в другой нити?
2) Корректной остановки нити для сборки мусора, т.к. сборщику мусора нельзя показывать частично проинициализированный объект.
@Алексей Пахунов
Благо, что с переходом на Fedora Linux копеек накопилось
Свои 2 копейки что ли подкинуть?
>Зачем вообще все эти прыжки в сторону?
Это нужно для асинхронного взаимодействие нитей: нити не надо активно ожидать события. В частности, это бывает важно при реализации виртуальных машин для языков программирования.