Пропихиваем параметры в небезопасные операции в безопасном коде

Всем привет. В этот раз продолжаем смеяться над нормальным вызовом методов. Предлагаю ознакомится с вызовом метода с параметрами без передачи параметров. Также попробуем преобразовать ссылочный тип в число — его адрес, без использования указателей и unsafe кода.

ua80z6gqkrpmrvr-y2-5k2u_h8a.jpeg


Дисклеймер


Прежде, чем приступить к повествованию, настоятельно рекомендую ознакомиться с предыдущим постом про StructLayout, т.к. вещи, оговоренные там, здесь повторяться не будут.

Также хотелось бы предупредить, что данная статья не содержит материал, который стоит применять в реальных проектах.

Немного начальных сведений


Прежде, чем приступить к практике, давайте вспомним, как преобразовывается код C#.
Разберем простой пример. Напомню, что для того, чтобы развлекаться со StructLayout, я использую только виртуальные методы.

public class Helper 
{
        public virtual void Foo(int param)
        {
        }
}

public class Program 
{
    public void Main() 
    {
        Helper helper = new Helper();
        var param = 5;
        helper.Foo(param);
    }
}


Данный код не содержит ничего сложного, однако инструкции, генерируемые JiTом, содержат несколько ключевых моментов. Предлагаю разобрать лишь небольшой фрагмент сгенерированного кода.

    1: mov dword [ebp-0x8], 0x5
    2: mov ecx, [ebp-0xc]
    3: mov edx, [ebp-0x8]
    4: mov eax, [ecx]
    5: mov eax, [eax+0x28]
    6: call dword [eax+0x10]


В этом небольшом примере можно наблюдать fastcall — соглашение по передаче параметров через регистры (первых двух параметров слева направо в регистрах ecx и edx), а остальные параметры передаются справа налево в стеке. Первый (неявный) параметр — адрес экземпляра класса, на котором метод вызван. Он передается первым неявным параметром для каждого экземплярного метода. Второй параметр — локальная переменная типа int (в нашем случае).

Итак, в первой строке мы видим локальную переменную 5, здесь ничего интересного нет.
Во второй строке мы копируем адрес экземпляра Helper в регистр ecx. Это адрес непосредственно таблицы методов.
Третяя строка содержит копирование локальной переменной 5 в регистр edx
Четвертая строка копирует адрес таблицы методов в регистр eax
Пятая строка содержит сдвиг регистра eax на 40 байт, на адрес начала методов в таблице методов. (Таблица методов содержит разную информацию, которая хранится до этого. к такой информации, например, относится адрес таблицы методов базового класса, адрес EEClass, различные флаги, в том числе и флаг сборщика мусора, и так далее). Соответсвенно теперь в регистре eax хранится адрес первого метода из таблицы методов.
В шестой строке вызывается метод по смещению 16 от начала, то бишь пятый в таблице методов. Почему наш единсвенный метод пятый? Напоминаю, что у object существуют 4 виртуальных метода (ToString, Equals, GetHashCode и Finalize), которые, соотвественно, будут у всех классов.

Переходим к практике


Пришло время приступить к небольшой демонстрации. Предлагаю вот такую заготовку (весьма похожую на заготовку из предыдущей статьи).

[StructLayout(LayoutKind.Explicit)]
    public class CustomStructWithLayout
    {
        [FieldOffset(0)]
        public Test1 Test1;
        [FieldOffset(0)]
        public Test2 Test2;
    }

    public class Test1
    {
        public virtual int Useless(int param)
        {
            Console.WriteLine(param);
            return param;
        }
    }

    public class Test2
    {
        public virtual int Useless()
        {
            return 888;
        }
    }

    public class Stub
    {
        public void Foo(int stub) { }
    }


И следующее наполение метода Main:

    class Program
    {
        static void Main(string[] args)
        {
            Test2 fake = new CustomStructWithLayout
            {
                Test2 = new Test2(),
                Test1 = new Test1()
            }.Test2;
            Stub bar = new Stub();
            int param = 55555;
            bar.Foo(param);
            fake.Useless();
            Console.Read();
        }
    }


Как вы догадываетесь, по опыту предыдущей статьи, будет вызван метод Useless (int j) типа Test1.

Но что будет выведено? Внимательный читатель, полагаю, уже ответил на этот вопрос. На консоль выведено »55555».

Но давайте все же глянем на фрагменты сгенерированного кода

     mov ecx, [ebp-0x20]
     mov edx, [ebp-0x10]
     cmp [ecx], ecx
     call Stub.Foo(Int32)
     nop
     mov ecx, [ebp-0x1c]
     mov eax, [ecx]
     mov eax, [eax+0x28]
     call dword [eax+0x10]


Думаю, вы узнали шаблон вызова виртуального метода, он начинается после L00cc: nop. Как мы видим, в ecx ожидаемо записывается адрес экзепляра, на котором вызвается метод. Но т.к. мы вызываем якобы метод типа Test2, который не имеет параметров, то в edx ничего не записывается. Однако до этого был вызван метод, которому передавался параметр как раз через регистр edx, соответсвенно, значение в нем и осталось. и его мы можем наблюдать в окне вывода.

Есть еще один интересный нюанс. Я специально использовал значимый тип. Предлагаю попробовать заменить тип параметра метода Foo типа Stub на любой ссылочный тип, например, строку. Но тип параметра метода Useless не изменять. Ниже можете увидеть результат на моей машине с некоторыми проясняющими элементами: WinDBG и Калькулятором :)

ljpt5isxjzejz0_kxhu0yyovyhw.jpeg
Картинка кликабельна

В окне вывода выводится адрес ссылочного типа в десятичной системе счисления

Итог


Освежили в памяти знания о вызове методов при помощи соглашения fastcall и тут же воспользовались чудным регистром edx для передачи параметра в 2 метода за раз. Также наплевали на все типы и вспомнив, что все есть лишь байты без труда получили адрес объекта без использования указателей и unsafe кода. Далее планирую использовать полученный адрес для еще более неприменимых целей!

Спасибо за внимание!

P.S. Код на C# можно найти по ссылке

© Habrahabr.ru