Небезопасная версия параметра C # out

1

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

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 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);
         }
    }
  • 5
    Похоже, вы захватываете адрес в стеке в середине цепочки вызовов; это удивительно опасно, даже если это работает, нет?
  • 0
    использовать out вместо unsafe , вам не нужно unsafe здесь вообще
Показать ещё 9 комментариев
Теги:
unsafe

2 ответа

5

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

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

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

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

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

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

РЕДАКТИРОВАТЬ:

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

Главный:

  • Установите [ebp - 8] (в настоящее время верхнюю часть стека) на ноль (в основном на Box *pBox; line). Итак, в [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 стек InitBoxUnsafe, esp теперь возвращается туда, где он был до вызова, - и теперь pBox имеет адрес выше текущего указателя стека. Значение все равно может быть там, но указатель теперь является незаконным. Конечно, мы находимся в unsafe коде, поэтому нет никакого шанса нанести нам запястье...
  • Там есть другой путь для версии выпуска и отладочная версия:
    • В отладочной версии вызов Print выполняется как обычно. Это включает в себя добавление нескольких значений в стек, а самое главное - обратный адрес. Однако это перезаписывает значение *pBox, поскольку оно указывает на ту же точку в стеке. Поэтому, когда Console.WriteLine фактически вызывается, он будет распечатывать обратный адрес, а не 500. Я предполагаю, что на 64-битном уровне это обычно означает 0, потому что эта часть обратного адреса будет обычно равна нулю, а на 32-битной - обычно будет хлам.
    • В выпуске (и без приложения отладчика) вызов Print inline. Это означает, что стек фактически не перезаписывается до тех пор, пока не вызовет сам Console.WriteLine, а значение *pBox будет захвачено задолго до того, как будет выполнен вызов. Тем не менее, почти все, что нажимает на стек между InitBoxUnsafe и pBox->Print() также уничтожит значение. Фактически достаточно просто вызывать pBox->Print() дважды - первый будет печатать 500, а второй будет печатать адрес потока Console, например. Или, если вы вызываете только метод, который не имеет локальных переменных или аргументов, он может распечатать обратный адрес.

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

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

  • 0
    Спасибо за подробный ответ! Мне было просто интересно, будет ли эта реализация работать, и удивляться, почему она не работает. Итак, насколько я понимаю, нет способа реализовать это поведение в небезопасном контексте?
  • 1
    @IvanZ проблема не в том, что это «невозможно», а в том, что это не означает, что вы думаете, что это значит . Это полностью «возможно» и работает просто отлично, так же, как и то, что вы просите. Проблема в том, что то, что вы просите это сделать, бессмысленно.
Показать ещё 1 комментарий
0

Ну, серфинг-код под 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
}
  • 1
    То , что вы сделали здесь вручную осуществить out - совершенно четко определенные и хорошо понимали C # понятие , которое не требует unsafe . Это плохой код. Вы должны просто использовать out Box box в сигнатуре метода, box = new Box(500); в теле и InitBoxUnsafe(out box); у звонящего. Это затем передавая указатели / ссылки. Просто: безопасно.
  • 0
    Ну, вопрос был на самом деле реализовать out использованием unsafe . Просто сырой код без какого-либо смысла. В любом случае спасибо за ваши ценные комментарии.
Показать ещё 1 комментарий

Ещё вопросы

Сообщество Overcoder
Наверх
Меню