Недокументированные возможности недокументированных возможностей: Передача ref в другой поток

от автора

Никогда не приходил в голову вопрос: "а как сохранить/передать в другой поток ссылку на поле?". Логичным предположением будет «передам ref в метод и сохраню. Стоп, oh shi~». Да, ref не сохраняются (а ещё, нельзя использовать на них замыкания, так что создать функцию внутри такого метода и создать из её поток тоже не получится). Но зато можно превратить ref в TypedReferenceс помощью недокументированного ключевого слова __makeref. Увы, TypedReference нельзя напрямую сохранить в поле и не наследует от System.Object, так что каст привычными методами тоже невозможен (да и вообще на их использование наложена целая куча ограничений). Казалось бы, тупик. Но это ещё не всё — есть ещё RuntimeArgumentHandle, который обладает свойствами TypedReference, за одним исключением — после хитрого каста в System.Object его ещё можно использовать до тех пор, пока жив кадр стека, в котором он был создан. Об этом этот пост.

TypedReference

Сам TypedReference удивительнейшая вещица — в него можно завернуть ref и передать другой метод (т.е. из метода, один из параметров которого является ref‘ом). Однако, методы этой структуры не позволяют нам задавать значение — NotSupportedException ожидает нас на вызове соответствующих методов. Но не беда — имеется ключевое слово __refvalue, которое позволяет не только получить значение, но и задать его. Но выглядит это довольно странно:

void Out(ref int someInt) {       Input(__makeref(someInt)); }  void Input(TypedReference @ref) {       int val = __refvalue(@ref, int);//Получаем значение       __refvalue(@ref, int) = 0;//Задаём значение в someInt } 

При том, что тип задаётся ручками, скастить, например, int в string, не получится — проверка принадлежности типу все-таки проводится.
При этом всём, TypedReference тоже нельзя использовать в замыканиях — так что для того, чтобы создать с замыканием на TypedReference тоже не получится.

RuntimeArgumentHandle

Является ничем иным, как params, только в профиль. По сути, представляет из себя некий список TypedReference‘ов (доступ к которым производитс конструированием ArgIterator‘а), а создаётся тоже… даже и не знаю как это описывать:

void Out(int something) {       Input(__arglist(something)); }  void Input(__arglist) {        new ArgIterator(__arglist); } 

При этом, ключевое слово __arglist нельзя использовать в делегатах при их обьявлении. Но RuntimeArgumentHandle можно (но только как параметры, TypedReference и RuntimeArgumentHandle нельзя возвращать из методов). __arglist() также нельзя использовать как аргумент для вызова делегата, но зато __arglist можно. Смысл этой несколько расплывчатой формулировки лучше подкрепить примером:

delegate void ArgWarrior(RuntimeArgumentHandle argh);  void Out(int something) {      (new ArgWarrior(u => { } ))(__arglist(someting));//не скомпилируется      Input(new ArgWarrior(u => { } ), __arglist(someting); }  void Input(ArgWarrior argh, __arglist) {        argh(__arglist);//а так можно } 

И вот я подобрался к ключевому моменту этого марлезонского балета: делегатам.

Манипуляции над _methodPtrAux как способ изысканных издевательств над делегатами

_methodPtrAux — это четвёртое поле в любом делегируемом типе, которое сыграет тут ключевую роль. В чём суть? Суть в том, что _methodPtrAux хранит в себе указатель на уже jit’енный метод. Записав произвольный неуправляемый код по тому указателю, можно таким образом этот неуправляемый код выполнить. Но это тут не главное. Делегат остаётся пригодным к использованию даже после подмены значения _methodPtrAux, и при вызове его, управление перейдёт именно туда, куда указывает значение этого поля. Т.о., имея два делегата с разными входными параметрами, я могу заменить указатель из делегата a на указатель из делегата b. Даже если у них разный набор аргументов, всё сработает. Ключевым моментом будет так-же и то, что даже если различаются типы соответствующих аргументов, clr не забьёт тревогу — int будет скастен в string все желания будут исполнены, никто не уйдёт обиженным, или… RuntimeArgumentHandle будет преобразован в System.Object:

        delegate void Encast(RuntimeArgumentHandle @ref);         delegate void Uncast(object @object);                 static void UseWith(Encast en, __arglist)         {             en(__arglist);         }                 static object m_storedRef;                   static void Engage(ref object @object)         {              Encast en = new Encast(@ref => { });              Uncast un = new Uncast(o =>                 {                     m_storedRef = o;//сюда перейдёт управление после вызова <b>en</b>.                 });                 typeof(Encast).GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)[3].SetValue(en, typeof(Uncast).GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)[3].GetValue(un));//меняем указатель у en                 UseWith(en, __arglist(__makeref(@object))); //вызываем         } 

Как видно по лямбде, я сразу уже сохраняю полученное значение в статическое поле. Да, тут есть одно непонятное ограничение — если сохранять o в не-статическое поле то можно уронить clr (чтение-запись защищённой памяти). Даже если целевым полем будет не поле, а поле обьекта, что хранится в статическом поле (например, Dictionary) всё должно пройти гладко. Несколько чудно при этом аргумент лямбды выглядит в отладчике: при просмотре можно увидеть только "{object}" (без кавычек) и ничего более. Попытка извлечь тип или привести к String при этом ничего хорошего не сулит (можно уронить clr)

Скрытый текст

Зато если такой фокус провернуть с TypedReference, можно увидеть ещё кое-что интересное (это я оставляю на самостоятельное изучение читателям. Можно попробовать скастить в int, тоже любопытно).

Обратное преобразование производится аналогично. Сохранение же кадра стека производится с помощью мониторов:

      static object m_locker = new object();       //...      Monitor.Enter(m_locker);      Monitor.Exit(m_locker); 

m_locker уже заранее заблокирован из другого потока, так что выполнение приостанавливается, т.о. RuntimeArgumentHandle так и остаётся в стеке, не разрушаясь.
Полный код программы выглядит так:

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading;  namespace ThreadJiggler {     class Program     {         delegate void Encast(RuntimeArgumentHandle @ref);         delegate void Uncast(object @object);         static object m_storedRef;         static object m_locker = new object();         static bool m_useFlag;                   static void Main(string[] args)         {             object @v = "means \"vendetta\"";             Victim1(ref @v);             Console.WriteLine(@v);         }          static void UseWith(Encast en, __arglist)         {             en(__arglist);         }          static Thread m_someThread;          static void Victim1(ref object @object)         {             Thread t = new Thread(() =>             {                 Monitor.Enter(m_locker);                 {                     for (; !m_useFlag; )                     {                         Thread.Sleep(10);                     }                      Encast en = new Encast(@ref =>                     {                         TypedReference tr = new ArgIterator(@ref).GetNextArg();                         __refvalue( tr, object) = 0;                     });                      Uncast un = new Uncast(o => { m_storedRef = o; });                     typeof(Uncast).GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)[3].SetValue(un, typeof(Encast).GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)[3].GetValue(en));                     un(m_storedRef);                 }                 Monitor.Exit(m_locker);             });             t.IsBackground = false;             t.Start();              {                 Encast en = new Encast(@ref => { });                 Uncast un = new Uncast(o =>                 {                     m_storedRef = o; m_useFlag = true;                     Monitor.Enter(m_locker);                     Monitor.Exit(m_locker);                 });                 typeof(Encast).GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)[3].SetValue(en, typeof(Uncast).GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)[3].GetValue(un));                 UseWith(en, __arglist(__makeref(@object)));             }         }     } } 

В конце Main можно увидеть, что значение @v сменилось на 0.

ссылка на оригинал статьи http://habrahabr.ru/post/192140/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *