为啥这段代码没有 unsafe 关键字也能工作?

Posted

技术标签:

【中文标题】为啥这段代码没有 unsafe 关键字也能工作?【英文标题】:Why does this code work without the unsafe keyword?为什么这段代码没有 unsafe 关键字也能工作? 【发布时间】:2010-10-22 00:29:16 【问题描述】:

在an answer 到他自己的controversial question 中,Mash 已经说明您不需要“不安全”关键字来直接读取和写入任何 .NET 对象实例的字节。您可以声明以下类型:

   [StructLayout(LayoutKind.Explicit)]
   struct MemoryAccess
   

      [FieldOffset(0)]
      public object Object;

      [FieldOffset(0)]
      public TopBytes Bytes;
   

   class TopBytes
   
      public byte b0;
      public byte b1;
      public byte b2;
      public byte b3;
      public byte b4;
      public byte b5;
      public byte b6;
      public byte b7;
      public byte b8;
      public byte b9;
      public byte b10;
      public byte b11;
      public byte b12;
      public byte b13;
      public byte b14;
      public byte b15;
   

然后你可以做一些事情,比如改变一个“不可变”的字符串。以下代码在我的机器上打印“bar”:

 string foo = "foo";
 MemoryAccess mem = new MemoryAccess();
 mem.Object = foo;
 mem.Bytes.b8 = (byte)'b';
 mem.Bytes.b10 = (byte)'a';
 mem.Bytes.b12 = (byte)'r';
 Console.WriteLine(foo);

您还可以通过使用相同技术破坏对象引用来触发AccessViolationException。

问题:我认为(在纯托管 C# 代码中)unsafe 关键字是执行此类操作所必需的。为什么这里不需要? 这是否意味着纯托管的“安全”代码根本不安全?

【问题讨论】:

感谢您改变提出相同问题的方式。上一个线程已过火。 @Mash:没问题。希望这会更加积极地关注您的原始问题。 @wcoenen:这并不重要,真的,即使我在考虑它——我的问题是社区内容,我没有从中获得任何收益。所以唯一重要的是积极的讨论。看起来你的问题看起来更好:) 【参考方案1】:

好吧,这很糟糕……使用联合的危险。这可能有效,但不是一个好主意 - 我想我会将它与反射(你可以做大部分事情)进行比较。我很想看看这是否适用于受限访问环境 - 如果是这样,它可能代表一个更大的问题......


我刚刚在没有“完全信任”标志的情况下对其进行了测试,运行时拒绝了它:

无法加载类型“MemoryAccess” 从程序集'ConsoleApplication4, 版本=1.0.0.0,文化=中性, PublicKeyToken=null' 因为对象 重叠在偏移量 0 和 程序集必须是可验证的。

要拥有这个标志,您已经需要高度信任 - 这样您就可以做更多令人讨厌的事情。字符串的情况略有不同,因为它们不是普通的 .NET 对象——但还有其他改变它们的方法的例子——不过,“联合”方法是一种有趣的方法。对于另一种 hacky 方式(有足够的信任):

string orig = "abc   ", copy = orig;
typeof(string).GetMethod("AppendInPlace",
    BindingFlags.NonPublic | BindingFlags.Instance,
    null, new Type[]  typeof(string), typeof(int) , null)
    .Invoke(orig, new object[]  "def", 3 );
Console.WriteLine(copy); // note we didn't touch "copy", so we have
                         // mutated the same reference

【讨论】:

@Marc:您能否详细说明一下您想到的其他变异字符串的方法? 这段代码在没有不安全标志的情况下可以正常工作。但它首先需要完全信任,并且使用 CAS 设置权限无济于事(通过拒绝看起来相关的事情)。你能举一个改变字符串内容而不使用不安全块的例子吗? 已重新编辑是否是不安全标志或完全信任 - 我更改了各种内容,所以我可能迷失了方向......重新“更改字符串内容” - 不是我的头顶。非常恶心;-p 哦,您可以使用反射(因为您完全信任)来调用 AppendInPlace 或类似的;添加示例。 AppendInPlace 很好的例子(它需要私有方法访问),但从源代码很明显,AppendInPlace 只是一个不安全的数组扩展,不允许您更改托管对象的非托管部分或打破目的。无论如何,谢谢。【参考方案2】:

哎呀,我把unsafefixed 搞混了。这是一个更正的版本:

示例代码不需要使用unsafe 关键字标记的原因是它不包含指针(请参阅下面的引用了解为什么这被认为是不安全的)。您说得很对:“安全”最好称为“运行时友好”。有关此主题的更多信息,请参阅 Don Box 和 Chris Sells Essential .NET

引用 MSDN,

在公共语言运行时 (CLR) 中, 不安全的代码被称为 无法验证的代码。 C# 中的不安全代码 不一定是危险的;它是 只是不能保证安全的代码 由 CLR 验证。 CLR 将 因此仅在以下情况下执行不安全代码 它在一个完全受信任的程序集中。如果 你使用不安全的代码,这是你的 有责任确保您的 代码不会引入安全风险 或指针错误。

fixed 和 unsafe 的区别在于,fixed 会阻止 CLR 在内存中移动东西,以便运行时之外的东西可以安全地访问它们,而 unsafe 则完全相反:而 CLR 可以保证正确dotnet 引用的分辨率,它不能为指针这样做。您可能还记得各种微软人都在讨论引用如何不是指针,这就是为什么他们对细微的区别如此大惊小怪。

【讨论】:

这段代码确实可以让您超越对象边界并更改进程可访问内存的任何部分。您可以重建任何对象的同步块和类型描述符,进入 GC 数据。实际上,不安全带来的不仅仅是什么? 这就是我们所说的。此代码没有迹象表明 CLR 无法验证其安全性(实际上不能)。 .NET 中的每个对象在内部都使用不安全代码,但这不会导致构造声明为安全的代码的能力,该代码可以访问和更改不安全的数据。 这里确实有问题。代码是“安全的”,但允许我们读取与某个对象相邻的内存。因此,除非您使用“fixed”关键字,否则您可以随时读取运行时可能移动的数据。这与指针的问题完全相同。 有趣的是编译器禁止引用类型和值类型的联合,而不是两种引用类型。当您未处于完全信任模式时,运行时会在某处检查这一点,但您仍然从明显安全的代码中获得不安全的访问。这是一种不同的东西——信任模式和“安全”代码。正如我已经说过的,不安全代码总是在安全代码中使用,但它指出,如果您的库不使用不安全代码(BCL 内部代码除外) - 它本质上是安全的。而这个样本表明这不是真的。 除了在完全信任时禁用验证程序,这意味着 CLR 并没有真正检查任何内容。 CLR 依赖于编译器来创建可验证/安全的代码【参考方案3】:

您仍在选择退出“托管”位。有一个基本假设,如果你能做到这一点,那么你就知道你在做什么。

【讨论】:

以上是关于为啥这段代码没有 unsafe 关键字也能工作?的主要内容,如果未能解决你的问题,请参考以下文章

为啥即使模型名称在 Laravel 上是复数,它也能工作?

我画了一块STM32F407的板子,用了16M和32.768两个晶振,调试发现,不焊这两个晶振也能工作,这是为啥

SHTML 和 DHTML - 应该使用它们,为啥不使用它们?

单片机学多久能工作,单片机学好了能应聘什么工作?

或者是无效的 C++:为啥这段代码会编译?

为啥 std::any 没有 unsafe_any_cast?