Никогда не приходил в голову вопрос: "а как сохранить/передать в другой поток ссылку на поле?"? Логичным предположением будет «передам 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)
Обратное преобразование производится аналогично. Сохранение же кадра стека производится с помощью мониторов:
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.
Автор: 6opoDuJIo