将TypedReference保持在方法块之外,而不返回它

时间:2013-01-05 02:37:59

标签: c# delegates unsafe ref typedreference

我想假设这个问题的目的是检查是否至少有一种方法,即使是通过最不安全的黑客,也要保持对非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)++;
    }
}

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

static class Program
{
    static int _fieldValue = 42;

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

在卸载相关应用程序域之前,不会处理该类,因此适用于该字段。老实说,我不知道高频堆中的字段是否可以重新分配,但我个人认为不是。但是现在让我们放弃安全(如果可能的话)。在阅读thisthis个问题后,我想知道是否有办法为非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类型的引用的可能性。但是,__makerefTypedReference的返回值受到很好的保护。你不能将它存储在一个字段中,你不能将它存储起来,你不能创建它的数组,你不能在匿名方法或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;
    }
}

Horror类是Foo类和上述方法的组合,但正如您必须注意到的,它有一个大问题。在方法Fix中,声明TypedReference tr,将其地址复制到通用指针_ptr内,然后方法结束,tr不再存在。调用Clear方法时,“新”tr已损坏,因为_ptr指向堆栈中不再是TypedReference的区域。所以问题就出现了:

有没有办法欺骗编译器让TypedReference实例在不确定的时间内保持活动状态?

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

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

请不要将问题判断为一般性讨论,因为它的目的是看看 是否存储了对blittable类型的引用的方法,尽管它可能是邪恶的。< / p>

最后一句话,我知道通过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声明为指向TypedReference的指针,并将其值设置为上面分配的内存区域的指针。这样做,而不是直接使用_ptr,因为类型为TypedReference*的字段会引发异常。
  3. refPtr隐式强制转换为void*并将指针指定给_ptr
  4. tr设置为refPtr指向的值,然后_ptr
  5. 他还提供了另一个解决方案,他最初写的答案,但它似乎不如上面那个可靠。另一方面,Peter Wishart还提供了另一种解决方案,但它需要准确的同步,并且每个Horror实例都会“浪费”一个线程。我将借此机会重申上述方法采用 no 方式进行实际使用,这只是一个学术问题。我希望对阅读这个问题的人有所帮助。

3 个答案:

答案 0 :(得分:2)

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

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方法。

答案 1 :(得分: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;
        }
    }
}

答案 2 :(得分:1)

TypedReference似乎已经被确定地锁定了。

我想只是将类型锁定以保持安全,而不是允许它传递但仅在不安全的上下文中更简单。

你可以暂时保持一个...虽然会花费你一个线程:)

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();
        }
    }
}