C#out参数的不安全版本
任务是编写一个方法来初始化声明的变量,就像使用out参数一样,但使用不安全的上下文。下面的不安全代码在C++中工作,但在C语言中打印0。有人能帮忙吗 UPD。代码在发布模式下工作。C#out参数的不安全版本,c#,unsafe,C#,Unsafe,任务是编写一个方法来初始化声明的变量,就像使用out参数一样,但使用不安全的上下文。下面的不安全代码在C++中工作,但在C语言中打印0。有人能帮忙吗 UPD。代码在发布模式下工作。 static void InitBox(out Box box) { box = new Box(100); } unsafe static void InitBoxUnsafe(Box** box) { Box b = new Box(500
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);
}
}
<>代码只在C++中工作,因为您只使用非常简单的应用程序来尝试它。 您正在捕获堆栈上的一个变量,当您进入另一个堆栈帧时,该变量将被覆盖,这提示了许多有趣且难以跟踪的错误 安全C#版本之所以有效,是因为它实际上并没有捕获局部变量的地址,而是将其值复制到上面的帧中 现在,如果显式地为结构分配非托管内存,您应该能够做到这一点。当然,如果您希望出于性能原因这样做,这可能会使您的性能变得更差,而且这必然会给您带来大量与使用越来越不安全的代码相关的问题:) 也可以简单地复制该值,而不是从不安全的方法中传播地址,但我也不想弄乱它 所以,主要的问题是:
out
和ref
有什么问题?为什么您试图使用不安全的代码来做一些安全代码可以轻松、干净地完成的事情
编辑:
在处理不安全代码时,查看实际发出的汇编代码可能是值得的。这并不能给您任何保证,因为C#编译器和JIT编译器可能在不同的计算机上产生不同的结果(甚至可能在不同的时间在同一台计算机上),但它可以给您一些见解。在这种情况下,汇编中的代码可以简化为:
主要内容:
- 将[ebp-8](当前为堆栈顶部)设置为零(基本上就是
Box*pBox;
行)。因此,在[ebp-8],我们有pBox
- 将
pBox
的地址通过ecx
传递到InitBoxUnsafe
(因此ecx
现在的值为ebp-8
)
InitBoxUnsafe(有点明显,但仍然很有趣-这不是内联的):
- 我们在堆栈顶部创建一个新的
框
结构(基本上归结为mov[esp],0
,mov[esp],500
——在堆栈上分配值类型非常简单)
- 将值
esp
存储在eax
中,这样eax
现在就有了新的框的地址
“实例”
- 最后,将
eax
存储在[ecx]
中,这样Main
范围中的pBox
变量现在在InitBoxUnsafe
范围中具有局部变量的地址。此时,指针仍然有效,长方体仍然在堆栈上
- 当我们从该方法返回时,它的所有堆栈都会弹出
返回Main:
- 由于我们弹出了
InitBoxUnsafe
的堆栈,esp
现在回到调用之前的位置,并且pBox
现在在当前堆栈指针上方有一个地址。该值可能仍然存在,但指针现在是非法的。当然,我们在不安全的代码中,所以没有人可以拍我们的手腕
- 发布版本和调试版本现在有了不同的路径:
- 在调试版本中,像往常一样执行
Print
调用。这涉及到在堆栈上放置一些值,最重要的是返回地址。但是,这会覆盖*pBox
的值,因为它指向堆栈中的同一点。因此,当实际调用Console.WriteLine
时,它将打印返回地址,而不是500
。我假设在64位上,这通常意味着0
,因为返回地址的那部分通常是零,而在32位上,它通常是垃圾
- 在发行版中(并且没有附加调试器),
Print
调用是内联的。这意味着在控制台.WriteLine
调用自身之前,堆栈实际上不会被覆盖,而*pBox
的值在调用之前被捕获。但是,在InitBoxUnsafe
和pBox->Print()
之间对堆栈执行任何操作都会破坏该值。事实上,只需连续调用pBox->Print()
两次就足够了——例如,第一个将打印出500
,而第二个将打印出控制台
流的地址。或者,如果只调用没有任何局部变量或参数的方法,它可能会打印出返回地址
正如你所看到的,代码与C++在等效代码上的差别并不大。这两种方法的输出之间的差异(或者说,在不同的计算机上运行这两种方法,或者在不同的编译器中编译这两种方法)是由于这样一个事实,即您所做的是非法的和未定义的行为,您不应该指望这种行为。永远
现在,如果你看一下你的第二个变量,你已经传递了Main
的本地box
变量的地址,而不是指向指针的指针,整个指针的混乱几乎完全被跳过了-InitBoxUnsafe
现在只做mov[&box],500直接-完全合法的操作。事实上,在没有调试器的发布模式下,InitBoxUnsafe
现在可以安全地内联,完全摆脱调用-整个过程现在基本上编译成Box=(Box)500代码>。在框
上调用打印
现在也完全安全了,因为该值现在固定在范围内(直接在[ebp-8]
中,而不是在[[ebp-8]]]
中)
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
}