2013-01-05 10 views
11

Voglio premettere che lo scopo di questa domanda è controllare se c'è almeno un modo, anche se attraverso l'hack più pericoloso, per mantenere un riferimento a un tipo di valore non bliabile . Sono consapevole che un tale tipo di design è paragonabile a un crimine; Non lo userei in nessun caso pratico, oltre all'apprendimento. Quindi per favore accetta di leggere heretic codice non sicuro per ora.Mantieni vivo un TypedReference dal blocco del metodo senza restituirlo

sappiamo che un riferimento ad un tipo copiabili può essere memorizzato e aumentata in questo modo:

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

In termini di sicurezza, la classe sopra è paragonabile ad un salto nel vuoto (senza giochi di parole), ma funziona, come già detto here. Se viene passata una variabile allocata nello stack e quindi l'ambito del metodo del chiamante termina, è probabile che si verifichi un errore o un errore di violazione di accesso esplicito. Tuttavia, se si esegue un programma come questo:

static class Program 
{ 
    static int _fieldValue = 42; 

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

La classe non sarà disposto fino a quando il dominio applicazione relativa viene scaricato, e così vale per il campo. Onestamente non so se i campi nell'heap ad alta frequenza possano essere riallocati, ma personalmente non penso. Ma mettiamo da parte la sicurezza ancora di più adesso (se possibile). Dopo aver letto le domande this e this mi chiedevo se esistesse un modo per creare un approccio simile per i tipi statici non intercettabili, quindi ho creato questo programma, che funziona effettivamente. Leggi i commenti per vedere cosa fa.

L'attuale problema/opportunità:

Sappiamo che il compilatore non ci permetterà di memorizzare un valore passato da ref, ma la parola chiave __makeref, per quanto privi di documenti e sconsigliato, offre la possibilità di incapsulare e ripristinare un riferimento a tipi blittable. Tuttavia, il valore restituito di __makeref, TypedReference, è ben protetto. Non puoi archiviarlo in un campo, non puoi metterlo in una scatola, non puoi creare un array, non puoi usarlo in metodi anonimi o lambda. Tutto quello che sono riuscito a fare è stato la modifica del codice di cui sopra come segue:

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

Il codice qui sopra funziona altrettanto bene e sembra anche peggio di prima, ma per amore della conoscenza, ho voluto fare di più con esso, in modo da ho scritto il seguente:

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

la classe Horror è una combinazione della classe Foo ed il metodo di cui sopra, ma come avrete sicuramente notato, ha un grosso problema. Nel metodo Fix, viene dichiarato il TypedReferencetr, il suo indirizzo viene copiato all'interno del puntatore generico _ptr, quindi il metodo termina e tr non esiste più. Quando viene chiamato il metodo Clear, il "nuovo" tr è danneggiato perché i punti _ptr puntano a un'area della pila che non è più una TypedReference. Quindi ecco la domanda:

C'è un modo per ingannare il compilatore in modo da mantenere in vita un'istanza TypedReference per un periodo di tempo indeterminato?

Qualsiasi modo per ottenere il risultato desiderato sarà considerato buono, anche se comporta codice brutto, pericoloso, lento.Una classe che implementa la seguente interfaccia sarebbe l'ideale:

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

Si prega di non giudicare la questione discussione generica perché il suo scopo è quello di vedere se, dopo tutto, non v'è un modo per memorizzare i riferimenti a tipi copiabili per quanto malvagio.

Un'ultima osservazione, sono a conoscenza delle possibilità di vincolare i campi tramite FieldInfo, ma mi è sembrato che quest'ultimo metodo non supportasse molto i tipi derivanti da Delegate.

Una possibile soluzione (risultato di taglie)

avrei segnato la risposta di AbdElRaheim a scelta, non appena ha curato il suo posto per includere la soluzione che ha fornito nel suo commento, ma suppongo che non era molto chiaro. In ogni caso, tra le tecniche che ha fornito, quella sintetizzata nella seguente classe (che ho leggermente modificato) è sembrata la più "affidabile" (ironico usare quel termine, visto che stiamo parlando di sfruttare un hack):

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

che Fix non è, a partire dalla linea contrassegnata come "magico" nel commento:

  1. Alloca memoria nel processo - nella parte non gestita di esso.
  2. Dichiara refPtr come puntatore a TypedReference e imposta il valore sul puntatore dell'area di memoria allocata in precedenza. A tal fine, anziché utilizzare direttamente _ptr, un campo con tipo TypedReference* genererebbe un'eccezione.
  3. Trasmette implicitamente refPtr a void* e assegna il puntatore a _ptr.
  4. Imposta tr come valore indicato da refPtr e di conseguenza _ptr.

Ha anche offerto un'altra soluzione, quella che ha originariamente scritto come risposta, ma sembrava meno affidabile di quella precedente. D'altra parte, c'era anche un'altra soluzione fornita da Peter Wishart, ma richiedeva una sincronizzazione accurata e ogni istanza di Horror avrebbe "sprecato" una discussione. Prenderò la possibilità di ripetere che l'approccio di cui sopra è in no in modo destinato all'uso del mondo reale, era solo una questione accademica. Spero che possa essere d'aiuto a chiunque legga questa domanda.

+0

Si sta richiedendo un bug come funzionalità. Abbastanza infame nella programmazione C e C++, lingue che creano molto facilmente questo tipo di bug. Google "puntatore penzolante" per saperne di più. –

risposta

1

Cosa stai cercando di fare esattamente? I locali sono in pila e gli argomenti dipendono anche dalla convenzione di chiamata. Memorizzare o restituire l'indirizzo di un locale o argomento non è buono perché verrà cancellato. Non c'è modo di impedire che vengano sovrascritti, come non chiamare metodi.

Se si attiva il debug non gestito, è possibile utilizzare il debugger di memoria e la finestra di registrazione per vedere cosa sta succedendo.

Qui è più semplice capire l'esempio di C. Perché la stampa non mostra il valore corretto. Perché quando viene invocata la funzione di stampa, il suo stack frame sovrascrive il valore.

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

Non riesci a far durare più a lungo il clr. Una cosa che puoi fare è far uscire la variabile in pila. Di seguito è un esempio. Questo è tutto male però :(

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; 
     } 
    } 
} 
+0

Sul motivo per cui un puntatore a una variabile allocata nello stack potrebbe non essere valido dopo la chiamata in cui è stato interrotto, grazie per la spiegazione, ma quello era il punto effettivo che ho usato per porre la domanda - Trovare un modo per allocarlo sul mucchio. Il metodo che hai suggerito, però, è nuovo per me e funziona (anche se come hai detto tu, è davvero "malvagio"). Vuoi spiegare il suo trucco? Accetterò la risposta nel caso non vengano forniti metodi migliori. Con "meglio" intendo un modo sistematico di spingere un 'TypedReference' sull'heap e recuperarlo quando necessario. – Mir

+0

sorry tr è in realtà un locale che viene creato sullo stack non come argomento. È possibile utilizzare il metodo ToObject statico e archiviare l'oggetto come campo nella classe. Ancora confuso di proposito però? Le TypeReferences sono principalmente per roba __arglist e hacky come questa http://stackoverflow.com/questions/4764573/why-is-typedreference-behind-the-scenes-its-so-fast-and-safe-almost-magical – AbdElRaheim

+0

The Lo scopo è scoprire se esiste un modo per aggirare i controlli del compilatore contro la persistente istanza di 'TypedReference'. 'ToObject' mi consente di memorizzarlo come' oggetto', ma non so come cancellarlo. – Mir

2

Sì! È possibile ingannare il compilatore nella creazione di un campo di tipo TypedReference, con la creazione di un iteratore o lambda che uno (o forse un blocco async) utilizza:

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

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

Purtroppo, il CLR non consente in realtà di utilizzare il classe. Otterrai un TypeLoadException quando ci proverai.

In altre parole, i tipi TypedReference e ArgIterator sono protetti non solo dal compilatore, ma dal runtime. Se vuoi salvare una copia di uno dei due, dovrai farlo facendo un blit non sicuro sull'heap o su Reflection.

Nota che l'ho provato con il compilatore .Net 4.0 C#. Altri compilatori potrebbero essere più intelligenti.

+0

Ho provato questo con .NET 4.0 e 4.5 tramite VS2012 e io nessuno dei due si compila correttamente. Riguardo all'utilizzo di 'TypedReference.ToObject', non sono riuscito a trovare un modo per rimuovere o ottenere l'indirizzo di' TypedReference' a cui punta l'oggetto risultante. Ho provato 'GCHandle.Alloc' ma dato che' TypedReference' punta a un tipo non blittable (in questo caso, 'Action'), la chiamata genera un'eccezione. – Mir

1

TypedReference sembra abbastanza definitivamente bloccato.

Credo che era più semplice per bloccare proprio il tipo giù per tenerlo al sicuro, piuttosto che permettergli di essere passato circa ma solo in un contesto non sicuro.

si può tenere su di uno per mentre ... vi costerà un filo però :)

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

Anche questo metodo funziona, tuttavia richiede una sincronizzazione accurata per produrre risultati affidabili. Nella tua istanza, 'Thread.Sleep' è fondamentale per l'esecuzione corretta del codice. Inoltre, richiederebbe un'altra sincronizzazione dopo 'horror.Clear()' altrimenti non ci sarebbe alcuna garanzia che il gestore sia ripristinato allo stato originale. +1 in entrambi i casi, è una possibile soluzione. – Mir

2

È anche possibile raggiungere questo obiettivo senza utilizzare non gestito la memoria, con la creazione di un tipo di "falso" che assomiglia digitato riferimento nella sua struttura:

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

Edit Importante: Vi consiglio vivamente di contro passando qualsiasi di riferimento intorno come un puntatore. Il GC è autorizzato a spostare liberamente gli oggetti sull'heap gestito come meglio crede, e non vi è alcuna garanzia che il puntatore rimanga valido anche dopo che è stato restituito dal metodo. Potresti non vedere l'effetto immediato di questo a causa del debug, ma stai scatenando ogni sorta di problemi.

Se è davvero necessario gestirlo come puntatore (e potrebbero esserci motivi legittimi), è necessario emettere un CIL personalizzato con un riferimento aggiunto. Potrebbe anche essere inizializzato estraendo il puntatore da TypedReference, ma garantisce che la posizione non cambierà. Passa poi a un metodo lambda.

Problemi correlati