Небезопасная версия выходного параметра C#

Задача состоит в том, чтобы написать метод, который инициализирует объявленную переменную, как с использованием параметра out, но с использованием небезопасного контекста. Приведенный ниже небезопасный код работает на C++, но печатает 0 на C#. Кто-нибудь может помочь?

UPD. Код работает в режиме Release.

    static void InitBox(out Box box)
    {
        box = new Box(100);
    }

    unsafe static void InitBoxUnsafe(Box** box)
    {
        Box b = new Box(500);
        (*box) = &b;
    }

    static void Main(string[] args)
    {
        //// The task is to write a code that does the same
        // Box box;
        // InitBox(out box);
        // box.Print();
        //// but using unsafe context

        //// I wrote this code, but it's not working (output is 0), can anyone tell why?
        unsafe
        {
            Box* pBox;
            Box** ppBox = &pBox;
            InitBoxUnsafe(ppBox);
            pBox->Print();
        }
    } 

    struct Box
    {
         private readonly int num;
         public Box(int number)
         {
              num = numberl
         }
         public void Print()
         {
              Console.WriteLine(num);
         }
    }

person Community    schedule 15.10.2014    source источник
comment
Это похоже, что вы захватываете адрес в стеке в середине цепочки вызовов; это удивительно опасно, даже если это работает, не так ли?   -  person Marc Gravell    schedule 15.10.2014
comment
используйте out вместо unsafe , unsafe тут вообще не надо   -  person Mgetz    schedule 15.10.2014
comment
Согласен с @Mgetz. unsafe предназначен для очень специфических приложений (таких как пиксельные функции на растровых изображениях). Это совершенно странный вариант использования unsafe, который, безусловно, создаст гораздо больше ошибок, чем вы ожидаете.   -  person Evan L    schedule 15.10.2014
comment
Что здесь на самом деле весело; для меня это работает (и я использую это слово вольно) в режиме выпуска и не работает в режиме отладки; это работает только случайно ;p   -  person Marc Gravell    schedule 15.10.2014
comment
Спасибо, ребята, за вызов! Было действительно интересно, сможет ли C# справиться с таким низкоуровневым кодом, не так ли?   -  person    schedule 15.10.2014
comment
@Marc Gravell♦ - Это действительно работает в Релизе!!! Собираюсь накидать ассемблерный код...   -  person    schedule 15.10.2014
comment
@IvanZ ваш недавний комментарий беспокоит меня тем, что вы не до конца понимаете проблему. Да, C# прекрасно с этим справится. Проблема в том, что вы просите его сделать что-то действительно опасное и глупое.   -  person Marc Gravell    schedule 15.10.2014
comment
@MarcGravell Я предположил, что JIT отладки обнуляет стек по указателю стека, но, похоже, это не так. Фактически, на моем компьютере, даже в режиме отладки (с отладчиком или без), я получаю распечатку случайных чисел. Я предполагаю, что это вызвано тем, что метод Print не встроен, поэтому вызов самого Print перезаписывает Box, который раньше был в стеке. В режиме выпуска метод становится встроенным, поэтому указатель по-прежнему указывает на те же данные, что и раньше. Весело.   -  person Luaan    schedule 16.10.2014
comment
@Luaan среда выполнения обнуляет пространство стека при входе, но не при выходе.   -  person Marc Gravell    schedule 16.10.2014
comment
@MarcGravell Я пытался пройтись по коду при дизассемблировании, и кажется, что по какой-то причине в некоторых случаях код (в выпуске, с присоединенным отладчиком после того, как код уже JIT-компилирован) фактически обнуляет стек перед извлечением. И после нажатия тоже (push eax, xor eax, eax, mov [esp], eax и наконец mov [esp], value). Это может быть какая-то оптимизация компилятора или мера безопасности, но это довольно странно. В любом случае, на моем компьютере он действительно встраивает метод Print, а указатель по-прежнему ссылается на значение 500 в памяти (очевидно, в недопустимой части стека).   -  person Luaan    schedule 16.10.2014
comment
@Luaan звучит весело. Я не собираюсь слишком беспокоиться об этом лично: неопределенное поведение не определено - интересно из-за любопытства, но не более того   -  person Marc Gravell    schedule 16.10.2014


Ответы (2)


Код работает только на C++, потому что вы пробуете его только с очень простым приложением.

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

Безопасная версия C# работает, потому что на самом деле она не захватывает адрес локальной переменной, а копирует ее значение в кадр выше.

Теперь вы сможете это сделать, если явно выделите неуправляемую память для структуры. Конечно, если вы надеетесь сделать это из соображений производительности, это, вероятно, значительно ухудшит вашу производительность и обязательно вызовет массу проблем, связанных с работой со все менее и менее безопасным кодом :)

Также может быть возможно просто скопировать значение, а не распространять адрес из небезопасного метода, но я бы тоже не стал возиться с этим.

Итак, главный вопрос: что не так с out и ref? Почему вы пытаетесь использовать код unsafe, чтобы сделать что-то, что безопасный код может сделать легко и чисто?

ИЗМЕНИТЬ:

При работе с небезопасным кодом, возможно, стоит взглянуть на фактически сгенерированный ассемблерный код. Это не дает никаких гарантий, поскольку и компилятор C#, и JIT-компилятор могут давать разные результаты на разных компьютерах (или, возможно, даже на одном и том же компьютере в разное время), но может дать некоторое представление. В этом случае код на ассемблере можно упростить до такого:

Главный:

  • Установите [ebp - 8] (в настоящее время вершина стека) в ноль (это в основном строка Box *pBox;). Итак, в [ebp - 8] у нас есть pBox.
  • Передайте адрес pBox в InitBoxUnsafe, в моем случае через ecx (поэтому ecx теперь имеет значение ebp - 8).

InitBoxUnsafe (очевидно, но все же интересно — это не встроено):

  • Мы создаем новую структуру Box на вершине стека (в основном сводится к mov [esp], 0, mov [esp], 500 — размещение типов значений в стеке очень просто).
  • Сохраните значение esp в eax, так что eax теперь имеет адрес нашего нового "экземпляра" Box.
  • И, наконец, сохраните eax в [ecx], чтобы наша переменная pBox в области видимости Main теперь имела адрес локальной переменной в области видимости InitBoxUnsafe. В этот момент указатель все еще действителен, и коробка все еще находится в стеке.
  • Когда мы возвращаемся из метода, весь его стек выталкивается.

Вернуться к основному:

  • Поскольку мы извлекли стек InitBoxUnsafe, esp теперь вернулся туда, где он был до вызова, а pBox теперь имеет адрес выше текущего указателя стека. Значение может все еще быть там, но указатель теперь недопустим. Конечно, мы в коде unsafe, так что некому шлепнуть нас по запястью...
  • There's now a different path for the release version, and the debug version:
    • In the debug version, the Print call is executed as usual. This involves putting a few values on the stack, most importantly the return address. However, this overwrites the value of *pBox, because it points to the same point in the stack. So when the Console.WriteLine is actually called, it will print out the return address, rather than 500. I assume that on 64-bit, this will usually mean 0, because that part of the return address will usually be zero, while on 32-bit, it will usually be junk.
    • В выпуске (и без подключенного отладчика) вызов Print встроен. Это означает, что стек фактически не перезаписывается до самого вызова Console.WriteLine, а значение *pBox фиксируется задолго до выполнения вызова. Однако любые действия, которые помещаются в стек между InitBoxUnsafe и pBox->Print(), также уничтожают значение. На самом деле достаточно просто вызвать pBox->Print() два раза подряд - первый выведет 500, а второй выведет, например, адрес потока Console. Или, если вы вызываете только метод, который не имеет локальных переменных или аргументов, он может распечатать адрес возврата.

Как видите, код на самом деле не сильно отличается от того, что C++ делает с эквивалентным кодом. Различия между выводом двух (или, на самом деле, запуском двух на разных компьютерах или их компиляцией в разных компиляторах) связаны с тем, что то, что вы делаете, является незаконным и неопределенным - поведение, на которое вы не должны рассчитывать. . Всегда.

Теперь, если вы взглянете на свой второй вариант, где вы передали адрес локальной переменной box Main вместо указателя на указатель, вся эта чепуха с указателем почти полностью пропущена - InitBoxUnsafe теперь просто делает mov [&box], 500 напрямую - вполне законная операция. На самом деле, в режиме выпуска без отладчика теперь можно безопасно встроить InitBoxUnsafe, полностью избавившись от вызова - теперь все это в основном компилируется в Box box = (Box)500;. Вызов Print для box теперь также полностью безопасен, потому что значение теперь прочно находится в области видимости (непосредственно в [ebp - 8], а не в [[ebp - 8]], так сказать).

person Luaan    schedule 15.10.2014
comment
Спасибо за подробный ответ! Мне просто было интересно, будет ли работать эта реализация и интересно, почему она не работает. Насколько я понимаю, нет способа реализовать такое поведение в небезопасном контексте? - person ; 15.10.2014
comment
@IvanZ проблема не в том, что это невозможно, а в том, что это не означает то, что вы думаете. Это вполне возможно, и работает просто отлично, если он делает то, что вы просите. Проблема в том, что то, о чем вы просите, бессмысленно. - person Marc Gravell; 15.10.2014
comment
Да, вы абсолютно правы насчет бессмысленности. Вопрос заключался в том, можно ли это реализовать. Например, чтобы проверить, понимаю ли я механизм указателей. Прошу прощения за отсутствие фактической цели вопроса. - person ; 15.10.2014

Что ж, просмотр кода в Reflector дал это хорошее решение, которое работает как в Debug, так и в Release.

static unsafe void InitBoxUnsafe(Box* box)
{
    *(box) = new Box(500);
}

static unsafe void Main(string[] args)
{
    Box box;
    InitBoxUnsafe(&box);
    box.Print(); // (&box)->Print(); // in Reflector
}
person Community    schedule 15.10.2014
comment
Здесь вы вручную реализовали out — полностью четко определенную и хорошо понятную концепцию C#, которая не требует unsafe. Это плохой код. Вы должны просто использовать out Box box в сигнатуре метода, box = new Box(500); в теле и InitBoxUnsafe(out box); в вызывающей стороне. Затем является передачей указателей/ссылок. Просто: безопасно. - person Marc Gravell; 15.10.2014
comment
Ну, на самом деле вопрос был в том, чтобы реализовать out с помощью unsafe. Просто сырой код без всякого смысла. В любом случае спасибо за ваши ценные комментарии. - person ; 15.10.2014
comment
Вы только что скопировали значение Box в этот, так что вы действительно делаете то, что делает ref. Важнейший урок, который следует здесь усвоить, состоит в том, что когда вы манипулируете значениями в стеке, вы можете использовать указатели только на ту же или более высокую область. В вашем исходном коде вы распространили указатель на что-то в более низкой области, что запрещено (на самом деле, как в C#, так и в C++ или даже в ассемблере). В этом коде вы дважды инициализируете структуру Box — один раз в Main и один раз в InitBoxUnsafe. InitBoxUnsafe просто перезаписывает локальную переменную в Main, что допустимо. - person Luaan; 16.10.2014