Код работает только на 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
out
вместоunsafe
,unsafe
тут вообще не надо - person Mgetz   schedule 15.10.2014unsafe
предназначен для очень специфических приложений (таких как пиксельные функции на растровых изображениях). Это совершенно странный вариант использованияunsafe
, который, безусловно, создаст гораздо больше ошибок, чем вы ожидаете. - person Evan L   schedule 15.10.2014Print
не встроен, поэтому вызов самогоPrint
перезаписываетBox
, который раньше был в стеке. В режиме выпуска метод становится встроенным, поэтому указатель по-прежнему указывает на те же данные, что и раньше. Весело. - person Luaan   schedule 16.10.2014push eax
,xor eax, eax
,mov [esp], eax
и наконецmov [esp], value
). Это может быть какая-то оптимизация компилятора или мера безопасности, но это довольно странно. В любом случае, на моем компьютере он действительно встраивает методPrint
, а указатель по-прежнему ссылается на значение500
в памяти (очевидно, в недопустимой части стека). - person Luaan   schedule 16.10.2014