11

我想假设这个问题的目的是检查是否至少有一种方法,即使是通过最不安全的黑客攻击,来保持对非 blittable 值类型的引用。我知道这样的设计类型可以与犯罪相提并论;除了学习之外,我不会在任何实际情况下使用它。所以现在请接受阅读异端的不安全代码。

我们知道可以通过这种方式存储和增加对 blittable 类型的引用:

unsafe class Foo
{
    void* _ptr;

    public void Fix(ref int value)
    {
        fixed (void* ptr = &value) _ptr = ptr;
    }

    public void Increment()
    {
        var pointer = (int*) _ptr;
        (*pointer)++;
    }
}

就安全性而言,上述课程可与虚空中的跳跃相媲美(没有双关语),但正如这里已经提到的那样,它确实有效。如果将分配在堆栈上的变量传递给它,然后调用方方法的作用域终止,您可能会遇到错误或显式访问冲突错误。但是,如果您执行这样的程序:

static class Program
{
    static int _fieldValue = 42;

    public static void Main(string[] args)
    {
        var foo = new Foo();
        foo.Fix(ref _fieldValue);
        foo.Increment();
    }
}

在卸载相关应用程序域之前,不会释放该类,因此适用于该字段。老实说,我不知道高频堆中的字段是否可以重新分配,但我个人认为不会。但是,让我们现在更加抛开安全问题(如果可能的话)。在阅读了这个这个问题之后,我想知道是否有一种方法可以为非 blittable 静态类型创建类似的方法,所以我制作了这个程序,它确实有效。阅读评论以了解它的作用。

static class Program
{
    static Action _event;

    public static void Main(string[] args)
    {
        MakerefTest(ref _event);
        //The invocation list is empty again
        var isEmpty = _event == null;
    }

    static void MakerefTest(ref Action multicast)
    {
        Action handler = () => Console.WriteLine("Hello world.");
        //Assigning a handler to the delegate
        multicast += handler;
        //Executing the delegate's invocation list successfully
        if (multicast != null) multicast();
        //Encapsulating the reference in a TypedReference
        var tr = __makeref(multicast);
        //Removing the handler
        __refvalue(tr, Action) -= handler;
    }
}

实际问题/机会:

我们知道编译器不会让我们存储 ref 传递的值,但是__makeref关键字,尽管没有记录和建议,但提供了封装和恢复对 blittable 类型的引用的可能性。__makeref但是,, 的返回值受到了TypedReference很好的保护。你不能将它存储在一个字段中,你不能装箱,你不能创建一个数组,你不能在匿名方法或 lambdas 中使用它。我所做的只是将上面的代码修改如下:

static void* _ptr;

static void MakerefTest(ref Action multicast)
{
    Action handler = () => Console.WriteLine("Hello world.");
    multicast += handler;
    if (multicast != null) multicast();
    var tr = __makeref(multicast);
    //Storing the address of the TypedReference (which is on the stack!)
    //inside of _ptr;
    _ptr = (void*) &tr;
    //Getting the TypedReference back from the pointer:
    var restoredTr = *(TypedReference*) _ptr;
    __refvalue(restoredTr, Action) -= handler;
}

上面的代码同样有效,看起来比以前更糟,但为了知识,我想用它做更多的事情,所以我写了以下内容:

unsafe class Horror
{
    void* _ptr;

    static void Handler()
    {
        Console.WriteLine("Hello world.");
    }

    public void Fix(ref Action action)
    {
        action += Handler;
        var tr = __makeref(action);
        _ptr = (void*) &tr;
    }

    public void Clear()
    {
        var tr = *(TypedReference*) _ptr;
        __refvalue(tr, Action) -= Handler;
    }
}

该类HorrorFoo该类和上述方法的组合,但您肯定会注意到,它有一个大问题。在方法Fix中,TypedReference tr被声明,它的地址被复制到泛型指针_ptr中,然后方法结束并且tr不再存在。当Clear调用该方法时,“new”tr被破坏,因为_ptr指向堆栈的一个区域,该区域不再是TypedReference. 那么问题来了:

有什么办法可以欺骗编译器使TypedReference实例在不确定的时间内保持活动状态?

任何达到预期结果的方法都将被认为是好的,即使它涉及丑陋、不安全、缓慢的代码。实现以下接口的类将是理想的:

interface IRefStorage<T> : IDisposable
{
    void Store(ref T value);
    //IDisposable.Dispose should release the reference
}

请不要将这个问题判断为一般性讨论,因为它的目的是看看到底是否有一种方法可以存储对 blittable 类型的引用,尽管它可能很邪恶。

最后一点,我知道通过 绑定字段的可能性FieldInfo,但在我看来,后一种方法不支持Delegate非常多的派生类型。

一个可能的解决方案(赏金结果)

我会在 AbdElRaheim 编辑他的帖子以包含他在评论中提供的解决方案时将他的答案标记为已选择,但我想这不是很清楚。无论哪种方式,在他提供的技术中,在以下课程中总结的技术(我稍微修改了一下)似乎是最“可靠”的(使用该术语具有讽刺意味,因为我们正在谈论利用黑客攻击):

unsafe class Horror : IDisposable
{
    void* _ptr;

    static void Handler()
    {
        Console.WriteLine("Hello world.");
    }

    public void Fix(ref Action action)
    {
        action += Handler;
        TypedReference tr = __makeref(action);
        var mem = Marshal.AllocHGlobal(sizeof (TypedReference)); //magic
        var refPtr = (TypedReference*) mem.ToPointer();
        _ptr = refPtr;
        *refPtr = tr;
    }

    public void Dispose()
    {
        var tr = *(TypedReference*)_ptr;
        __refvalue(tr, Action) -= Handler;
        Marshal.FreeHGlobal((IntPtr)_ptr);
    }
}

什么Fix是,从注释中标记为“魔术”的行开始:

  1. 在进程中分配内存——在它的非托管部分。
  2. 声明refPtr为指向 a 的指针TypedReference并将其值设置为上面分配的内存区域的指针。这是完成的,而不是_ptr直接使用,因为具有类型的字段TypedReference*会引发异常。
  3. 隐式强制转换refPtr并将void*指针分配给_ptr.
  4. 设置trrefPtr和 因此指向的值_ptr

他还提供了另一种解决方案,他最初作为答案写的那个,但它似乎不如上面的那个可靠。另一方面,Peter Wishart 还提供了另一种解决方案,但它需要准确的同步,并且每个Horror实例都会“浪费”一个线程。我将借此机会重申,上述方法绝不适用于现实世界,这只是一个学术问题。我希望它对阅读这个问题的人有所帮助。

4

4 回答 4

3

你也可以在不使用非托管内存的情况下实现这一点,方法是在其结构中创建一个类似于类型化引用的“假”类型:

unsafe class Horror
{
    FakeTypedRef ftr;

    static void Handler()
    {
        Console.WriteLine("Hello void.");
    }

    public void Fix(ref Action action)
    {
        action += Handler;
        TypedReference tr = __makeref(action);
        ftr = *(FakeTypedRef*)(&tr);
    }

    public void Clear()
    {
        fixed(FakeTypedRef* ptr = &ftr)
        {
            var tr = *(TypedReference*)ptr;
            __refvalue(tr, Action) -= Handler;
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    struct FakeTypedRef
    {
        public IntPtr Value;
        public IntPtr Type;
    }
}

重要编辑:我强烈建议不要任何引用作为指针传递。GC 可以在它认为合适的时候自由移动托管堆上的对象,并且不能保证即使从方法返回后指针也不会保持有效。由于调试,您可能看不到此操作的直接效果,但您正在由此引发各种问题。

如果您确实需要将其作为指针处理(并且可能有一些合理的原因),则需要发出带有固定引用的自定义 CIL。它甚至可以通过从TypedReference中提取指针来初始化,但它保证位置不会改变。然后将其传递给 lambda 方法。

于 2014-11-14T10:57:54.220 回答
2

是的!您可以TypedReference通过创建使用一个(或可能是async块)的迭代器或 lambda 来欺骗编译器创建类型字段:

static IEnumerable<object> Sneaky(TypedReference t1)
{
    yield return TypedReference.ToObject(t1);
}

static Func<object> Lambda(TypedReference t1)
{
    return () => TypedReference.ToObject(t1);
}

不幸的是,CLR 不允许您实际使用该类。TypeLoadException当你尝试时,你会得到一个。

换句话说,TypedReferenceandArgIterator类型不仅受到编译器的保护,还受到运行时的保护。如果要保存其中任何一个的副本,则必须通过对堆或反射执行不安全的 blit 来做到这一点。

请注意,我使用 .Net 4.0 C# 编译器对此进行了尝试。其他编译器可能更聪明。

于 2013-01-07T07:19:41.797 回答
1

TypedReference seems pretty conclusively locked down.

I guess it was simpler to just lock the type down to keep it safe, rather than allow it to be passed about but only in an unsafe context.

You can hold on to one for while... will cost you a thread though :)

namespace TehHorror
{
    using System;
    using System.Threading;    
    class Horror
    {
        private ManualResetEvent waiter = null;    
        public void Fix(ref Action multicast)
        {
            waiter = new ManualResetEvent(false);
            multicast += HorrorHandler;
            if (multicast != null) multicast();
            var tr = __makeref(multicast);
            waiter.WaitOne();
            __refvalue(tr, Action) -= HorrorHandler;
        }    
        public void Clear() { waiter.Set(); }    
        private static void HorrorHandler()
        {
            Console.WriteLine("Hello from horror handler.");
        }
    }    
    class Program
    {
        static void Main()
        {
            Action a = () => Console.WriteLine("Hello from original delegate");
            var horror = new Horror();
            a.Invoke();
            Action fix = () => horror.Fix(ref a);
            fix.BeginInvoke(fix.EndInvoke, null);
            Thread.Sleep(1000);
            horror.Clear();
            a.Invoke();
        }
    }
}
于 2013-01-11T19:38:16.493 回答
1

你到底想做什么?局部变量在堆栈上,参数也取决于调用约定。存储或返回本地或参数的地址并不好,因为它会被覆盖。除了不调用方法之外,没有办法阻止它们被覆盖。

如果您打开非托管调试,您可以使用内存调试器和注册窗口来查看发生了什么。

这是更容易理解的 C 示例。为什么打印不显示正确的值。因为当 print 函数被调用时,它的堆栈帧会覆盖该值。

int* bad(int x, int y)
{
    int sum = x + y;
    return &sum;
};

int* bad2(int x, int y)
{
    x += y;
    return &x;
}

int _tmain(int argc, _TCHAR* argv[])
{
    int* sum1 = bad(10, 10);
    int* sum2 = bad(100, 100);
    printf("%d bad", *sum1);  // prints 200 instead of 20

    sum1 = bad2(10, 10);
    sum2 = bad2(100, 100);
    printf("%d bad", *sum1);  // prints 200 instead of 20

    return 0;
};

不能让 clr 坚持更长时间。您可以做的一件事是将堆栈上的变量推得更远。下面是一个例子。但这一切都很糟糕:(

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Xml.Linq;
using System.Runtime.InteropServices;

namespace Bad
{
    class Program
    {
        static void Main(string[] args)
        {
            Action a = () => Console.WriteLine("test");
            Horror h = new Horror();
            h.Fix(new Big(), ref a, new Big());
            h.Clear();
            Console.WriteLine();
        }
    }
    [StructLayout(LayoutKind.Sequential, Size = 4096)]
    struct Big
    {
    }
    unsafe class Horror
    {
        void* _ptr;

        static void Handler()
        {
            Console.WriteLine("Hello world.");
        }


        public void Fix(Big big, ref Action action, Big big2)
        {
            action += Handler;
            var tr = __makeref(action);
            _ptr = (void*)&tr;
        }

        public void Clear()
        {
            var tr = *(TypedReference*)_ptr;
            __refvalue(tr, Action) -= Handler;
        }
    }
}
于 2013-01-09T04:08:22.733 回答