【问题标题】:Unsafe string manipulation mutates unexisting value不安全的字符串操作会改变不存在的值
【发布时间】:2019-08-29 08:06:42
【问题描述】:

string 在 C# 中是一种引用类型,其行为类似于值类型。通常程序员不必担心这一点,因为字符串是不可变的,并且语言设计可以防止我们对它们做无意的危险事情。但是,使用不安全的指针逻辑是否可以直接操作字符串的底层值,如下所示:

    class Program
    {
        static string foo = "FOO";
        static string bar = "FOO";
        const string constFoo = "FOO";

        static unsafe void Main(string[] args)
        {
            fixed (char* p = foo)
            {
                for (int i = 0; i < foo.Length; i++)
                    p[i] = 'M';
            }
            Console.WriteLine($"foo = {foo}"); //MMM
            Console.WriteLine($"bar = {bar}"); //MMM
            Console.WriteLine($"constFoo = {constFoo}"); //FOO
        }
   }

运行时,编译器将优化(实习)字符串,使foobar 都指向相同的底层值。通过以这种方式操作foo,我们还可以更改bar 的值。 const 值由编译器内联,不受此影响。到目前为止没有什么奇怪的。

让我们将固定变量从foo 更改为constFoo,我们开始看到一些奇怪的行为。

    class Program
    {
        static string foo = "FOO";
        static string bar = "FOO";
        const string constFoo = "FOO";

        static unsafe void Main(string[] args)
        {
            fixed (char* p = constFoo)
            {
                for (int i = 0; i < constFoo.Length; i++)
                    p[i] = 'M';
            }
            Console.WriteLine($"foo = {foo}"); //MMM
            Console.WriteLine($"bar = {bar}"); //MMM
            Console.WriteLine($"constFoo = {constFoo}"); //FOO
        }
    }

尽管我们修复和操作了 constFoo,但它的值 foobar 发生了变异。 为什么foobar 会发生变异?

如果我们现在改变foobar 的值,那就更奇怪了。

    class Program
    {
        static string foo = "BAR";
        static string bar = "BAR";
        const string constFoo = "FOO";

        static unsafe void Main(string[] args)
        {
            fixed (char* p = constFoo)
            {
                for (int i = 0; i < constFoo.Length; i++)
                    p[i] = 'M';
            }
            Console.WriteLine($"foo = {foo}"); //BAR
            Console.WriteLine($"bar = {bar}"); //BAR
            Console.WriteLine($"constFoo = {constFoo}"); //FOO
        }
    }

代码运行,我们似乎在某处发生了变异,但我们的变量没有变化。 我们在这段代码中改变了什么?

【问题讨论】:

  • 因为你没有变异constFoo。你正在改变它指向的字符串,它恰好是实习生表中的一个字符串。
  • 这与前一周的this question 相似(尽管不一定重复)。
  • 部分混淆可能是您将字符串视为值类型,而您的意思是它们是 variable-length 引用类型,仍然是引用类型,通过和通过。

标签: c# .net string


【解决方案1】:

您正在修改实习字符串表中的字符串,如下代码所示:

using System;

namespace CoreApp1
{
    class Program
    {
        const string constFoo = "FOO";

        static unsafe void Main(string[] args)
        {
            fixed (char* p = constFoo)
            {
                for (int i = 0; i < constFoo.Length; i++)
                    p[i] = 'M';
            }

            // Madness ensues: The next line prints "MMM":
            Console.WriteLine("FOO"); // Prints the interned value of "FOO" which is now "MMM"
        }
    }
}

这里有点难以解释:

using System;
using System.Runtime.InteropServices;

namespace CoreApp1
{
    class Program
    {
        const string constFoo = "FOO";

        static void Main()
        {
            char[] chars = new StringToChar {str = constFoo }.chr;

            for (int i = 0; i < constFoo.Length; i++)
            {
                chars[i] = 'M';
                Console.WriteLine(chars[i]); // Always prints "M".
            }

            Console.WriteLine("FOO"); // x86: Prints "MMM". x64: Prints "FOM".
        }
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct StringToChar
    {
        [FieldOffset(0)] public string str;
        [FieldOffset(0)] public char[] chr;
    }
}

这不使用任何不安全的代码,但它仍然会改变实习生表中的字符串。

这里更难解释的是,对于 x86,interned 字符串会更改为您所期望的“MMM”,但对于 x64,它会更改为“FOM”。前两个字符的变化发生了什么?我无法解释这一点,但我猜这与将两个字符放入 x64 的单词中有关,而不仅仅是一个。

【讨论】:

  • 提示:RuntimeHelpers.OffsetToStringData
【解决方案2】:

为了帮助您理解这一点,您可以反编译程序集并检查 IL 代码。

进行第二次 sn-p,您将得到如下结果:

// static fields initialization
.method specialname static void .cctor () cil managed 
{
    IL_0000: ldstr "FOO"
    IL_0005: stsfld string Program::foo

    IL_000a: ldstr "FOO"
    IL_000f: stsfld string Program::bar
}

.method static void Main() cil managed 
{
    .entrypoint
    .locals init (
        [0] char* p,
        [1] string pinned,
        // ...
    )

    // fixed (char* ptr = "FOO")
    IL_0001: ldstr "FOO"
    IL_0006: stloc.1
    IL_0007: ldloc.1
    IL_0008: conv.u
    IL_0009: stloc.0
    // ...
}

请注意,在所有三种情况下,字符串都使用ldstr 操作码加载到评估堆栈中。

来自the documentation

公共语言基础架构 (CLI) 保证 两个 ldstr 指令引用两个元数据标记 相同的字符序列返回完全相同的字符串对象(一个 过程称为“字符串实习”)。

因此,在所有三种情况下,您都获得了相同的字符串对象 - 内部字符串实例。这解释了“变异的”const 对象。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-04
    • 1970-01-01
    • 1970-01-01
    • 2019-11-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多