Получение указателя на объект .Net

в 12:36, , рубрики: .net, memory management, Блог компании Luxoft, ненормальное программирование, метки: ,

Получение указателя на объект .Net
Сразу отвечу на вопрос «а зачем?». Просто интересно получить указатель на объект и потом подумать, что с ним особенного можно сделать :) Ведь если получить указатель, то дальше можно сделать все что угодно. Например, становится возможным изучить поведение SyncBlockIndex, или взять MethodTable и изучив, где что находится, изменить его содержимое. Можно мастерить собственные типы данных, не прибегая к Reflection. В общем можно делать много странных вещей, которые относятся больше к спортивному программированию и к саморазвитию. Однако, приступим.

Способ №1

После проведенной исследовательской работы было выяснено что можно внутрь структуры положить два ссылочных типа и при этом задать их точное выравнивание относительно начала структуры, наложив их друг на друга. Итак, что мы в данном случае имеем?

  • Любой экземпляр ссылочного типа имеет одиноковый заголовок с системной информацией.
  • Это означает что если мы разместим в нашем классе какое-то поле, и если мы в другом классе также определим единственное поле, они будут располагаться по одному смещению относительно начала каждого из классов
  • Если мы имеем два указателя на экземпляры ссылочных типов в двух полях с единым смещением (`[StructLayout(LayoutKind.Explicit)]` на описании типа структуры и `[FieldOffset(0)]` при описании полей), и при этом записав в один из них указатель на какой-то класс, то обращаясь по этому же указателю (смещение полей едино) по второму полю, тип которого при этом имеет другое наполнение собственными полями, мы интерпретируем эти же данные по-своему. Другими словами, если мы имеем два класса: `A` и `B`, и при этом у `A` есть поле `object _field`, а у класса `B` есть поле `int _field`, то на 32-х разрядной системе имея структуру `C`, в которой по единому смещению находятся поля с типами `A` и `B`, можно записав в `C.A._field = new object()` считать адрес этого объекта: `var address = C.B._field`.

Для демонстрации выше сказанного был реализована структура `SafePtr`, у которой как раз есть два внутренних типа: `ReferenceType` (1) и `IntPtrWrapper` (2) и два свойства для их установки и получения — (5) и (6), которые читают и получают значения полей, выровненных по единому смещению 0 (3) и (4):

    [StructLayout(LayoutKind.Explicit)]
    public struct SafePtr
    {
        // (1)
        public class ReferenceType
        {
            public object Reference;
        }
        
        // (2)
        public class IntPtrWrapper
        {
            public IntPtr IntPtr;
        }

        // (3)
        [FieldOffset(0)]
        private ReferenceType Obj;

        // (4)
        [FieldOffset(0)]
        private IntPtrWrapper Pointer;

        public static SafePtr Create(object obj)
        {
            return new SafePtr { Obj = new ReferenceType { Reference = obj } };
        }

        public static SafePtr Create(IntPtr rIntPtr)
        {
            return new SafePtr { Pointer = new IntPtrWrapper { IntPtr = rIntPtr } };
        }

        // (5)
        public IntPtr IntPtr
        {
            get { return Pointer.IntPtr; }
            set { Pointer.IntPtr = value; }
        }

        // (6)
        public Object Object
        {
            get { return Obj.Reference; }
            set { Obj.Reference = value; }
        }

        public void SetPointer(SafePtr another)
        {
            Pointer.IntPtr = another.Pointer.IntPtr;
        }
    }

Ну и напоследок, использование:

    var safeptr = SafePtr.Create(new object());
    Console.WriteLine("Address of object is: {0}", safeptr.IntPtr.ToInt32());

Вот и все :) Также вы сможете использовать как чистый указатель:

    var safeptr = SafePtr.Create(new object());
    unsafe {
        ((int *)safeptr.IntPtr)* = 0;
    }
    Console.WriteLine("Address of object is null: {0}", safeptr.Object == null);

Способ №2

Есть еще один, более удобный способ. Для того чтобы скастить строго типизированный указатель .Net к обычному указателю, необходимо найти место, где теряется информация о типе. Где становится все равно что хранится. И такое место есть: стек потока. .Net использует обычный стек потока для вызова методов (в общем-то зачем ему использовать что-то еще?), а это значит что если туда помещается указатель на .Net объект, размер которого (например), на 32-х разрядной машине равен 4 байтам, то можно оттуда просто считать 4 байта и все. Главное чтобы JITter не проверял типы при компиляции.

C# не позволяет нам записать в стек данные одного типа и достать уже другого. Т.е., другими словами, конструкция:

   int point
   var pointer = new Object(); 

запрещена. Однако она не запрещена MSIL'ом. На нем и напишем методы конвертации (EntityPtr.il):

// Metadata version: v2.0.50215
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .zV.4..
  .ver 2:0:0:0
}
.assembly EntityPtr

{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) 
  .hash algorithm 0x00008004
  .ver 1:0:0:0
}


.module sample.exe
// MVID: {A224F460-A049-4A03-9E71-80A36DBBBCD3}
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00000001    //  ILONLY

// =============== CLASS MEMBERS DECLARATION ===================

.class public auto ansi abstract sealed beforefieldinit System.Runtime.CLR.EntityPtr
	extends [mscorlib]System.Object
{             
	.method public hidebysig static 
		native int ToPointer<TType> (
			!!TType input
		) cil managed 
	{
		// Method begins at RVA 0x2254
		// Code size 7 (0x7)
		.maxstack 2
		.locals init (
			[0] native int CSS
		)

		ldarg.0
		conv.i4
		ldc.i4 4
		sub
		conv.i
		ret
	} // end of method Converter::ToPointer

	.method public hidebysig static 
		native int ToPointerWithOffset<TType> (
			!!TType input
		) cil managed 
	{
		// Method begins at RVA 0x2254
		// Code size 7 (0x7)
		.maxstack 2
		.locals init (
			[0] native int CSS
		)

		ldarg.0
		ret
	} // end of method Converter::ToPointer

	// Methods
	.method public hidebysig static 
		!!TType ToInstance<TType> (
			native int input
		) cil managed 
	{
		// Method begins at RVA 0x2254
		// Code size 7 (0x7)
		.maxstack 2
		.locals init (
			[0] native int CSS
		)

		ldarg.0
		conv.i4
		ldc.i4 4
		add
		conv.i
		ret
	} // end of method Converter::ToInstance

	.method public hidebysig static 
		!!TType ToInstanceWithOffset<TType> (
			native int input
		) cil managed 
	{
		// Method begins at RVA 0x2254
		// Code size 7 (0x7)
		.maxstack 2
		.locals init (
			[0] native int CSS
		)

		ldarg.0		
		ret
	} // end of method Converter::ToInstance

} // end of class sandbox.Converter

Этот код по сути содержит класс `System.Runtime.CLR.EntityPtr` с четыремя методами:

  • `IntPtr ToPointer(T object)` — возвращает указатель на объект, смещенный на размер SyncBlockIndex (управляемые указатели в .Net указывают не на начало объекта, а берутся со смещением, равным SyncBlockIndex, т.е. равному размеру — слова процессора)
    `IntPtr ToPointerWithOffset(T object)` — возвращает указатель на объект без смещения на раз -мер SyncBlockIndex
    `T ToInstance(IntPtr pointer)` — возвращает объект, на который указывает указатель, смещенный на размер слова назад (SyncBlockIndex)
    `T ToPointerWithOffset(IntPtr pointer)` — возвращает объект, на который указывает указатель без смещения на размер слова назад (SyncBlockIndex)

    А его использование еще проще чем в методе 1:

        var pointer = EntityPtr.ToPointer("Hello");
    

    вот и все!

    Выводы

    Из проделанной работы можно сделать несколько выводов:

    • Указатель на объект .Net получить можно
    • На самом деле обе методики работают только в FullTrust. Если вы настроите Sandbox, вылетит исключение.
    • Если во время манипуляций сработает GC, то память, понятное дело, сожмется. При этом наш указатель автоматически перестанет быть валидным.

    Тем не менее это не отменяет того факта что это крайне интересно и открывает тонны новых возможностей.

Автор: sidristij

Источник

Поделиться

* - обязательные к заполнению поля