Не уважаю инкапсуляцию, или использование таблицы методов другого типа для быстрого вызова приватных методов

в 12:09, , рубрики: .net, C#, inside, memory, method table, structlayout, высокая производительность, ненормальное программирование, ооп

Всем привет. Хотелось бы поделиться примером использования StructLayout для чего-то более интересного, чем примеры с байтами, интами и прочими цифрами, в которых все происходит чуть более, чем ожидаемо.

Прежде, чем приступить к молниеносному нарушению инкапсуляции, стоит в двух словах напомнить, что такое StructLayout. Строго говоря, это даже StructLayoutAttribute, то бишь атрибут, который позволяет создавать структуры и классы, подобные union в С++. Если говорить еще более подробно, то данный атрибут позволяет взять управление размещением членов класса в памяти на себя. Соответсвенно, ставится он над классом. Обычно, если класс имеет 2 поля, мы ожидаем, что они будут располагаться последовательно, то бишь будут независимы друг от друга (не перекрывать). Однако, StructLayout дает возможность указать, что расположение полей будет задавать не среда, а пользователь. Для явного указания смещения полей следует использовать параметр LayoutKind.Explicit. Для указания, по какому смещению относительно начала класса/структуры (в дальнейшем класса) мы хотим разместить поле, над ним следует поставить атрибут FieldOffset, который принимает в качестве параметра количесво байт — отступ от начала класса. Отрицательное значение передать не получится, так что о том, чтобы испортить указатели на таблицу методов или индекс блока синхронизации, даже и не думайте, все будет немного сложнее.

Приступим к написанию кода. Для начала предлагаю начать с простого примера. Создадим класс следующего вида:

    public class CustomClass
    {
        public override string ToString()
        {
            return "CUSTOM";
        }

        public virtual object Field { get; } = new object();
    }

Далее используем вышеописанный механизм явного задания смещений полей.

[StructLayout(LayoutKind.Explicit)]
    public class CustomStructWithLayout
    {
        [FieldOffset(0)]
        public string Str;

        [FieldOffset(0)]
        public CustomClass SomeInstance;
    }

Пока отложу объяснения и воспользуюсь написанным классом следующим образом:

    class Program
    {
        static void Main(string[] args)
        {
            CustomStructWithLayout instance = new CustomStructWithLayout();
            instance.SomeInstance = new CustomClass();
            instance.Str = "4564";
            Console.WriteLine(instance.SomeInstance.GetType()); //System.String
            Console.WriteLine(instance.SomeInstance.ToString()); //4564
            Console.Read();
        }
    }

Итого. Вызов метода GetType() выдает string, метод ToString() шалит и дает нам строку «4564».
Разрядка для мозгов: Что будет выведено при вызове виртуального свойства CustomClass?

Как вы уже догадались, мы проинициализировали CustomStructWithLayout, обе ссылки равны null, далее инициализируем поле нашего типа, а после присваиваем строку полю Str. В итоге от CustomClass остается чуть больше, чем ничего. Поверх его была записана строка со всей ее внутренней структурой, включая таблицу методов и индекс блока синхронизации. Но компилятор видит поле все еще типа нашего класса.
Для доказательсва приведу небольшую вырезку из WinDbg:
Не уважаю инкапсуляцию, или использование таблицы методов другого типа для быстрого вызова приватных методов - 1
Здесь можно увидеть несколько необычных вещей. Первая — в объекте адреса на таблицы методов у полей класса разные, что и ожидаемо, но адрес значения поля один. Вторая — можно увидеть, что оба поля расположены по смещению 4. Думаю, большинсво поймет, но на всякий случай поясню, непосредсвенно по адресу объекта располагается ссылка на таблицу методов. Поля начинаются со смещением 4 байта (длz 32 бит), а индекс блока синхронизации расположен со смещением -4.

Теперь, когда разобрались, что происходит, можно попробовать использовать смещения для вызова того, что вызывать не следовало бы.
Для этого я повторил структуру класса string в одном из своих классов. Правда повторил я лишь начало, поскольку класс string весьма объемный.

    public class CustomClassLikeString
    {
        public const int FakeAlignConst = 3;
        public const int FakeCharPtrAlignConst = 3;
        public static readonly object FakeStringEmpty;
        public char FakeFirstChar;
        public int FakeLength = 3;
        public const int FakeTrimBoth = 3;
        public const int FakeTrimHead = 3;
        public const int FakeTrimTail = 3;

        public CustomClassLikeString(){}
        public CustomClassLikeString(int a){}
        public CustomClassLikeString(byte a){}
        public CustomClassLikeString(short a){}
        public CustomClassLikeString(string a){}
        public CustomClassLikeString(uint a){}
        public CustomClassLikeString(ushort a){}
        public CustomClassLikeString(long a){ }

        public void Stub1(){}
        public virtual int CompareTo(object value)
        {
            return 800;
        }
        public virtual int CompareTo(string value)
        {
            return 801;
        }
    }

Ну и немного меняется структура с Layout

    [StructLayout(LayoutKind.Explicit)]
    public class CustomStructWithLayout
    {
        [FieldOffset(0)]
        public string Str;

        [FieldOffset(0)]
        public CustomClassLikeString SomeInstance;
    }

Далее, при вызове FakeLength или метода CompareTo() благодаря идентичным смещением этих членов класса относительно адреса самого объекта будет вызван соответсвующий метод строки (в данном случае). Добираться до первого приватного метода в строке, который я могу использовать, было довольно долго, поэтому я остановился на публичном. Но поле приватное, все честно. Кстати, методы сделаны виртуальными для защиты от всяких оптимизаций, мешающий работе (например, встраивания), а также для того, чтоб метод вызывался по смещению в таблице методов.

Итак, производительность. Исное дело, что прямой конкурент в вызове того, что вызвать не надо и в нарушении инкапсуляции — рефлексия. Я думаю, что и так понятно, что мы быстрее этой вещи, все ж мы не анализируем метаданные. Точные значения:

Method Job Mean Error StdDev Median
StructLayoutField Clr 0.0597 ns 0.0344 ns 0.0396 ns 0.0498 ns
ReflectionField Clr 197.1257 ns 1.9148 ns 1.7911 ns 197.4787 ns
StructLayoutMethod Clr 3.5195 ns 0.0382 ns 0.0319 ns 3.5285 ns
ReflectionMethod Clr 743.9793 ns 13.7378 ns 12.8504 ns 743.8471 ns

Здесь длинный кусок кода с тем, как я измерял производительность (Если кому-то оно надо):

Код

    [ClrJob]
    [RPlotExporter, RankColumn]
    [InProcessAttribute]
    public class Benchmarking
    {
        private CustomStructWithLayout instance;
        private string str;
        [GlobalSetup]
        public void Setup()
        {
            instance = new CustomStructWithLayout();
            instance.SomeInstance = new CustomClassLikeString();
            instance.Str = "4564";
            str = "4564";
        }

        [Benchmark]
        public int StructLayoutField()
        {
            return instance.SomeInstance.FakeLength;
        }

        [Benchmark]
        public int ReflectionField()
        {
            return (int)typeof(string).GetField("m_stringLength", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(str);
        }

        [Benchmark]
        public int StructLayoutMethod()
        {
            return instance.SomeInstance.CompareTo("4564");
        }

        [Benchmark]
        public int ReflectionMethod()
        {
            return (int)typeof(string).GetMethod("CompareTo", new[] { typeof(string) }).Invoke(str, new[] { "4564" });
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<Benchmarking>();
        }
    }

Автор: ZloyChert

Источник

Поделиться

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