为什么这段代码在没有使用unsafe关键字的情况下也能工作?

19

在回答自己的有争议的问题时,Mash阐述了您不需要使用"unsafe"关键字就能直接读写任何.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;
   }

然后你可以做一些像改变"immutable"字符串的事情。以下代码在我的机器上打印出"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
@Mash:没问题。希望这会引起更多对你原来问题的积极关注。 - Wim Coenen
@wcoenen:其实这并不重要,即使我曾想过这个问题——我的问题是关于社区内容的,我从中并没有获得任何收入。所以唯一重要的是进行积极的讨论。而且看起来你的问题更好 :) - Mash
3个回答

12

好的,那很恶劣...使用联合的危险。这可能有效,但不是一个很好的主意 - 我想我会将其与反射进行比较(在那里你可以做大多数事情)。如果它能在受限制的访问环境中工作,我会感兴趣看看它是否代表了一个更大的问题...


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

无法从程序集'ConsoleApplication4,Version=1.0.0.0,Culture=neutral,PublicKeyToken=null'加载类型'MemoryAccess',因为对象在偏移量0处重叠,并且程序集必须是可验证的。

而要拥有此标志,您已经需要高度信任 - 因此您已经可以做更危险的事情了。字符串是稍微不同的情况,因为它们不是正常的.NET对象 - 但还有其他例子可以改变它们 - "union"方法是一个有趣的方法。对于另一种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:你能详细说明一下你心中还有哪些改变字符串的方法吗? - Brann
这段代码在不使用unsafe标志的情况下可以正常工作。但是它需要一开始就获得完全信任,并且使用CAS来设置权限并没有帮助(通过拒绝看起来相关的事物)。你能否给出一个示例,改变字符串内容而不使用unsafe块? - Mash
修改后的关于是否为不安全标志或完全信任的问题——我改变了很多事情,所以可能迷失了方向…… 关于“更改字符串内容”的问题——不是我能够一口气回答的。非常棘手;-p - Marc Gravell
哦,你可能可以使用反射(由于你拥有完全的信任)来调用AppendInPlace或类似的方法;示例已添加。 - Marc Gravell
AppendInPlace是一个很好的例子(需要访问私有方法),但从源代码中可以清楚地看出,AppendInPlace只是一种不安全的数组扩展,并不能让您更改托管对象的非托管部分或打破对象的边界。无论如何,感谢提供这个例子。 - Mash
它在完全信任模式下工作的原因是因为验证器在完全信任模式下被禁用了。 - Dinis Cruz

5
抱歉,我把“unsafe”和“fixed”搞混了。以下是更正后的版本:
样例代码不需要使用“unsafe”关键字进行标记的原因是它不包含指针(请参阅下面引用的原因,了解为什么这被认为是不安全的)。您是完全正确的:“安全”可能更好地称为“运行时友好”。有关此主题的更多信息,请参阅Don Box和Chris Sells的 Essential .NET 引用MSDN的话:
在公共语言运行时(CLR)中,不安全代码称为不可验证代码。C#中的不安全代码不一定危险;它只是代码,CLR无法验证其安全性。因此,CLR仅在完全受信任的程序集中执行不安全代码。如果您使用不安全的代码,则您有责任确保您的代码不会引入安全风险或指针错误。
fixed和unsafe之间的区别在于fixed阻止CLR在内存中移动事物,以便运行时外的事物可以安全地访问它们,而unsafe则涉及到完全相反的问题:虽然CLR可以保证dotnet引用的正确解析,但它无法对指针进行这样的操作。您可能还记得各种Microsofties谈论引用不是指针的原因,这就是为什么他们对微妙的区别如此重视的原因。

这段代码确实允许您超出对象边界并更改任何可访问进程的内存部分。您可以重构任何对象的同步块和类型描述符,进入GC数据。实际上,不安全代码提供了什么更多? - Mash
这就是我们所讨论的。这段代码没有CLR无法验证其安全性的迹象(实际上它确实无法)。在.NET中,每个对象内部都使用不安全的代码,但这并不意味着能够构建声明为安全的代码,可以像不安全的代码一样访问和更改数据。 - Mash
这里确实有些可疑。 代码是“安全的”,但允许我们读取与某个对象相邻的内存。 因此,你可以读取可能被运行时在任何时候移动的数据,除非你使用“fixed”关键字。 这正是指针所具有的问题。 - Wim Coenen
有趣的是,编译器禁止引用类型和值类型的联合,但不禁止两个引用类型的联合。运行时在非完全信任模式下会对此进行检查,但仍然可以从明显安全的代码中获取不安全的访问。这是一种不同类型的东西——信任模式和“安全”代码。正如我已经说过的,不安全的代码总是从安全的代码中使用,但如果你的库没有使用不安全的代码(除了BCL内部),那么它本质上是安全的。而这个示例表明这并不是真的。 - Mash
除了在完全信任时禁用验证器外,这意味着CLR实际上不会检查任何内容。CLR依赖编译器创建可验证/安全代码。 - Dinis Cruz

0

你仍然选择退出“托管”部分。这里有一个基本的假设,即如果你能够这样做,那么你知道自己在做什么。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接