C#: Этюды, часть 4

    Продолжение, предыдущая часть здесь

    Итак, сегодня мы с Вами с помощью молотка и лопаты попытаемся сделать трепанацию черепа влезть во внутренности .NET.

    Имеется простейший класс: class X { public int Val; }. При создании объекта этого класса какой реальный размер он будет занимать? И какие значения будут содержать дополнительные поля? Предъявите доказательство в виде кода на C# ;)

    Разумеется, это только первая загадка из серии, но этого должно хватить по крайней мере на выходные. Выяснение внутренностей .NET — достаточно большая тема.
    Примечание: поскольку речь идет о системно-зависимых вещах, я буду полагать, что код выполняется на 32-битной ОС.

    UPD Итак, было получено два схожих решения: от lam0x86: iaroshenko.habrahabr.ru/blog/77275/#comment_2250121 и от crjd: iaroshenko.habrahabr.ru/blog/77275/#comment_2252317. Я сначала классифицировал их как неправильные, но позже увидел свою ошибку.

    Тем не менее, оба решения основаны на том, что в классе есть открытое поле int. Если же это будет не поле, а свойство, или не будет совсем ни полей ни свойств, то такое решение не сработает.
    Поэтому я решил продемонстрировать «авторский вариант»: iaroshenko.habrahabr.ru/blog/77275/#comment_2269072. Естественно, он будет работать только в этих ограниченных рамках: простейшая программа, где нет нужды создавать другие объекты или собирать мусор.

    Кстати, если убрать поля совсем, то размер все равно остается тем же, только дополнительное поле равно нулю.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 41

      –1
      System.IO.MemoryStream stream = new System.IO.MemoryStream();
      BinaryFormatter objFormatter = new BinaryFormatter();
      objFormatter.Serialize(stream, new X());
      stream.Length — будет его размером?
      зы:
      [Serializable]
      class X { public int Val; }
        0
        в stream.Length вы получите намного больше
          +1
          да, у меня получилось 111, что никак не вяжется с размером такого мелкого объекта )
          тип в этом случае сериализуется в строку, а еще я полагаю, не все скрытые поля будут сериализованы
        0
        Сильно подозреваю, что неправильно, но:
        повесить на класс атрибут [StructLayout(LayoutKind.Sequential)] и:
        int size = Marshal.SizeOf(new X());
        А вообще такие вещи профайлер считает :)
          0
          Marshal.SizeOf вернет в данном случае объем, занимаемый полями, т.е. 4 байта
          профайлер я пытался вчера дернуть из шарпа, но не очень получилось, да и неточные он данные дает
            0
            или тут надо получать ссылку на обьект IntPtr через:
            GCHandle gch = GCHandle.Alloc(new X());
            IntPtr unmanagedPtr = GCHandle.ToIntPtr(gch);
            потом передать её unmanaged коду на подсчет?
              0
              тоже нет, при передаче в неуправляемый код никакие внутренние структуры дотнета просто так не передаются, нет в этом смысла
              да и это нарушает ограничение этого этюда, ведь на шарпе неуправляемы код не напишешь (насколько я помню ))
            +1
            Эта тема вроде хорошо раскрыта в CLR via C#.
            А вообще правильный ответ 12?
              +1
              да, правильный ответ 12, и тему я взял именно у Рихтера )
              но теория теорией, а практическое подтверждение тоже требуется
              для Вас еще последующие части будут )
              0
              long aaa = GC.GetTotalMemory(true);
              X t = new X();
              t.Val = 5;
              long bbb = GC.GetTotalMemory(true);
              long ccc = bbb — aaa;
              System.Windows.Forms.MessageBox.Show(ccc.ToString());

              ответ: 24?
                0
                нет, поскольку GetTotalMemory дает лишь приблизительную оценку, а с большим количеством объектов ответы будут еще сильнее отличаться
                собственно, правильный ответ в комментариях уже есть, но нет доказательства
                  0
                  Спасиб =) Понял и посмотрел описания… Было интересно.
                +1
                Экземпляр этого класса занимает 12 байт. 4 байта отводится на ссылку на элемент в таблице синхронизации (если объект не участвует в блокирующих операциях, эти 4 байта равны 0), 4 на ссылку на тип объекта, последние 4 на значение Val.
                Как это проверить не знаю =)
                  +2
                  правильно, и Вы даже почти знаете всю теорию для следующего этюда, но код все равно нужен )
                    0
                    правильнее сказать: знаете почти всю теорию, сорри )
                  –1
                  вы не могли бы перенести этюд в блог .net? топики из личных блогов не выходят на главную страницу Хабра
                    +2
                    мне как раз и не нужно, чтоб он выходил на главную страницу, потому что тогда меня минусуют те, кому дотнет по барабану — похоже, я зачастил своими этюдами )
                    а делать закрытым тоже не хочу, так как он скорее предназначен для тех, у кого нет инвайта на хабр, нежели для тех, у кого есть
                      0
                      а вы на gotdotnet.ru участвуете? туда бы тоже кросспостить подобные этюды, аудитория соответствующая
                        0
                        нет, не участвую
                        но если там такие монстры, как на rsdn.ru, то я пока воздержусь
                        там у них такие этюды, которые с решением не сразу-то поймешь
                          –1
                          монстры никогда не составляют всю аудиторию
                          есть там и начинающие и всякие, а читателей, думаю, вообще полный зоопарк
                          просто сейчас ресурсу нужны интересные материалы, он переживает реинкарнацию и новое воплощение :-)
                    +1
                    unsafe
                    {
                    object obj = new X();
                    RuntimeTypeHandle tHandle = obj.GetType().TypeHandle;
                    int size = *(*(int**)&tHandle + 1);
                    }

                    для работы надо разрешить запуск небезопасного кода в проекте.
                      –1
                      и да и нет
                      да — потому что Вы первый догадались применить ансейф код, и Ваш метод даже дает правильные резулататы
                      нет — потому что Вы используете то, что истинный размер указан где-то в типе. это специфическая деталь реализации, и она может менятся, а может и вообще врать.
                      ну и вдобавок требуется указать содержимое полей
                        0
                        А что должно быть на выходе — размер экземпляра, или значения полей?
                          0
                          и то и другое
                            +1
                            Я знаю как можно «не совсем честно» добраться до экземпляра класса:

                            X obj = new X();

                            fixed (int* ptr = &obj.Val)
                            {
                            int val = ptr[0];
                            int typeHandle = ptr[-1];
                            int syncBlock = ptr[-2];
                            }

                            сойдёт за ответ? =)
                              0
                              нет, по нескольким причинам
                              во-1, Вы полагаете, что объект внутри содержит три 32битных поля, а это еще надо доказать
                              во-2, Вы почему-то решили, что дополнительные поля находятся перед «явным» полем, а это весьма сомнительное утверждение

                              но направление выбрано правильное, продолжайте думать )
                                0
                                совершенно не понятно, каким образом я смогу доказать, что syncBlock — это именно 4-хбайтовое значение. Имхо, об этом можно только прочитать, либо догадаться, проведя некоторое количество экспериментов. Или где-то в глубине typehandle-ов всё-таки есть полные метаданные о class-layout-е, в т.ч. и о вспомогательных полях?
                                  0
                                  нет, нужно доказать, что их именно три, а не четыре и не пять
                                  ну и вывести их значение
                                  их смысл будет проверятся в следующем этюде
                                0
                                похоже, Вы правы, а я ошибся
                                действительно, ptr[-1] и ptr[-2] будут искомыми полями
                                я опирался не на адрес поля &obj.Val, а на значение, содержаееся непосредственно в ссылке obj. но похоже, она указывает в «середину» объекта, а именно — в ptr[-1] в Вашей нотации
                                размер реальной памяти Вы не сосчитали, но это уже сделано в комментариях ниже.

                                но Вы оба опирались на адрес переменной Val. если бы переменная была приватной и имела геттер/сеттер, или если бы даже ее вообще не было, Вы бы смогли тогда достучаться до этих полей?
                            +1
                            Таксс =)) ясно (правда так и не пришла мысль, как же верно реализовать)… Жалко мне пора уходить и нет времени дальше эксперементировать. Надеюсь все-таки увидеть решение, когда вернусь. Ещё раз спасибо за этюды, в день раз их просматривать-самое оно =)
                          0
                          Думаю нужно сделать 2 new и вычесть ссылки. Желательно сделать это много раз и взять минимум из-за возможности сборки мусора и создания объектов в других потоках.
                            0
                            код давайте, код! ))
                              +1
                              unsafe
                              {
                                  X x1 = new X() { Val = 1 };
                                  X x2 = new X() { Val = 2 };
                              
                                  fixed (int *p1 = &x1.Val)
                                  fixed (int *p2 = &x2.Val)
                                  {
                                      long fieldCount = p2 - p1;
                                      long size = fieldCount * sizeof(int);
                                      Console.WriteLine(size);
                                      int *pointer = p1 + 1;
                                      while (pointer <= p2)
                                      {
                                          Console.WriteLine(*pointer);
                                          pointer++;
                                      }
                                  }
                              }
                              


                              Правда тут ограничения есть.
                              1. Предполагается, что объекты будут расположены подряд (если запускать этот код отдельно, не создавая ни чего до него и во время него, то так оно и будет).
                              2. По поводу количества полей. Опять же предполагается, что они int.

                              А так, размер и содержимое памяти выводятся.
                                0
                                очень хорошая попытка, но к сожалению, не засчитывается

                                размер Вы посчитали верно (т.е. такой способ тоже годится — допущение, что два объекта одного типа имеют одинаковую внутреннюю структуру — достаточно логичное)
                                необходимое условие — запускать код в отдельной программе, не создавая других объектов — тоже верно

                                а вот содержимое выведено неправильно
                                см. iaroshenko.habrahabr.ru/blog/77275/#comment_2250134
                                  0
                                  да, смотрите мое дополнение: iaroshenko.habrahabr.ru/blog/77275/#comment_2253737
                                  похоже, Вы тоже выведите искомые поля объекта x2
                                0
                                Казалось бы, никто не гарантирует, что объекты в памяти будут расположены последовательно, разве нет?
                                  0
                                  а давайте подумаем, откуда могут появится дырки между объектами? только от удаленных объектов, не так ли?
                                  кроме того, разве сборщик мусора не перемещает объекты в начало кучи?
                                    0
                                    Т.е. аллокатор памяти выделяет её всегда последовательно? Буду знать.
                                      0
                                      вопрос динамического выделения памяти — отдельная большая тема, в двух словах ее никак не опишешь
                                      если вкратце, основная проблема — утилизация уже освобожденной памяти (т.е. повторное использование участков, которые были ранее выделены и потом освободились), поэтому объекты желательно держать как можно плотнее. в неуправляемых языках, типа с или с++ (ну также и собственно на уровне ос происходит, хотя с++ дублирует этот механизм) выделяемая память имеет указатель размера, занимаемого объектом. при освобождении указатель вместе с размером добавляется в т.н. список свободных областей. есть хорошая статья джоэля, затрагивающая эту тему: russian.joelonsoftware.com/Articles/BacktoBasics.html. она будет полезна любому, на мой взгляд
                                      в управляемой среде .NET ситуация принципиально другая. поскольку инфраструктура .NET контролирует каждую ссылку на любой объект, имеется возможность перемещать объекты из одной области памяти в другую прозрачно для пользователя, обновляя при этом все ссылки. сборщик мусора действует именно так, уплотняя объекты как можно сильнее — чтобы свободная память была участком максимального размера.
                                      кроме того, следует упомянуть о поколениях сборщика мусора (объекты в пределах поколения «группируются»), о выравнивании памяти (для эффективности границы объектов должны быть выровнены по некоторому размеру, обычно размеру указателя), о больших объектах (в .NET большие объекты не переносятся с места на место, ибо это накладно, а располагаются в отдельной области управляемой кучи).
                                      все это очень большая тема )
                                        0
                                        пролистал Рихтера (CLS via C#). он прямым текстом пишет «в управляемой куче последовательно созданные объекты гарантированно будут расположены друг за другом»
                                        конечно, ремарки насчет выравнивания, поколений и больших объектов остаются в силе
                                  0
                                  using System;
                                  using System.Runtime.InteropServices;

                                  public static class ObjectExtensions
                                  {
                                    [StructLayout(LayoutKind.Explicit)]
                                    internal struct MyStruct
                                    {
                                      [FieldOffset(0)]
                                      public Int32 Value1;

                                      [FieldOffset(4)]
                                      public object Value2;
                                    }

                                    public static unsafe int GetAddr(this object o)
                                    {
                                      MyStruct ms = new MyStruct();
                                      ms.Value2 = o;
                                      int* ptr = &ms.Value1;
                                      return ptr[1];
                                    }

                                    public static unsafe void PrintObject(this int ptr, int offset, int count, string descr)
                                    {
                                      Console.WriteLine("Printing {0} object at addr : {1}", descr, ptr);
                                      int* ptr2 = (int*)ptr;
                                      for (int i = 0; i < count; ++i)
                                      {
                                        Console.WriteLine("-- {0}", ptr2[offset + i]);
                                      }
                                    }
                                  }

                                  class X
                                  {
                                    public int Val {get; set; }
                                  }

                                  class App
                                  {
                                    static unsafe void Main()
                                    {
                                      const int INT_BYTES = 4;
                                      object x = new X() { Val = 101 };
                                      {
                                        object y = new X() { Val = 257 };
                                        int size = y.GetAddr() - x.GetAddr();
                                        Console.WriteLine("Size is {0} bytes", size);
                                        x.GetAddr().PrintObject(-1, size/INT_BYTES, "x");
                                        y.GetAddr().PrintObject(-1, size/INT_BYTES, "y");
                                      }
                                    }
                                  }

                                  * This source code was highlighted with Source Code Highlighter.

                                  Only users with full accounts can post comments. Log in, please.