Sicurezza della memoria basata sul tipo senza gestione manuale della memoria o garbage collection di runtime?


13

Diciamo che volevamo un linguaggio di programmazione funzionale e puro, come Haskell o Idris, che si rivolge alla programmazione di sistemi senza garbage collection e non ha runtime (o almeno non più dei "runtime" di C e Rust). Qualcosa che può funzionare, più o meno, su bare metal.

Quali sono alcune delle opzioni per la sicurezza della memoria statica che non richiedono la gestione manuale della memoria o la garbage collection di runtime e come potrebbe essere risolto il problema utilizzando il sistema di tipi di un puro funzionamento simile a Haskell o Idris?


Stai dicendo che vorresti che i tipi nella lingua servissero come modo per evitare la raccolta dei rifiuti? Il problema di base sorge nella valutazione delle funzioni. Una funzione viene valutata in una chiusura, che incapsula l'ambiente di runtime corrente. Questa è la principale fonte di raccolta rifiuti. A meno che non modifichi la regola di digitazione per le funzioni, non vedo come i tipi aiuteranno in questo. Java e altri linguaggi con -strazioni spezzate ottengono ciò mutuando la formazione di chiusure: non consentono riferimenti che richiederebbero la raccolta di gabrage. λ
Andrej Bauer,

Sicuramente Rust ha dovuto affrontare lo stesso problema della valutazione delle funzioni e delle chiusure con il suo modello di proprietà e il correttore di prestiti? Gestione della memoria significa solo sapere per quanto tempo i valori sono vivi, quali altri valori dipendono da essi e distruggere i valori non utilizzati quando sono morti, giusto? Quindi immagino di chiedermi davvero se la gestione della memoria possa essere incapsulata in un insieme di tipi che possono essere verificati per correttezza attraverso il sistema dei tipi, senza estendere i meccanismi di base del linguaggio o del compilatore aggiungendo un sistema di proprietà completamente nuovo e "prendere in prestito checker "(che è il modo di Rust).
Chase,

Che dire dell'LFPL di Martin Hofmann ? Ha un tipo di base speciale, il "diamante", sul quale viene applicata una disciplina di tipo lineare, che consente ai tipi di rendere conto dell'utilizzo di base della memoria (allocazione / deallocazione). Andrebbe nella direzione di cui stai parlando?
Damiano Mazza,

Risposte:


18

In parole povere, ci sono due strategie principali per una gestione manuale sicura della memoria.

  1. Il primo approccio consiste nell'utilizzare alcune logiche strutturali come la logica lineare per controllare l'utilizzo delle risorse. Questa idea ha funzionato fondamentalmente sin dall'inizio della logica lineare, e fondamentalmente lavora sull'osservazione che bandendo la regola strutturale della contrazione, ogni variabile viene utilizzata al massimo una volta, quindi non c'è aliasing. Di conseguenza, la differenza tra aggiornamento sul posto e riassegnazione è invisibile al programma e quindi è possibile implementare la lingua con la gestione manuale della memoria.

    Questo è ciò che fa Rust (utilizza un sistema di tipo affine). Se sei interessato alla teoria dei linguaggi in stile Rust, uno dei migliori articoli da leggere è L3: un linguaggio lineare con posizioni di Ahmed et al . A parte questo, il calcolo LFPL di Damiano Mazza menzionato è anche lineare, ha un linguaggio completo che ne deriva nel linguaggio RAML .

    Se sei interessato alla verifica in stile Idris, dovresti guardare il linguaggio ATS di Xi et al , che è un linguaggio in stile Rust / L3 con supporto per la verifica basato su tipi indicizzati in stile Haskell, reso solo irrilevante e lineare per dare di più controllo sulle prestazioni.

    Un approccio ancora più aggressivamente dipendente è il linguaggio F-star sviluppato da Microsoft Research, che è una teoria del tipo completamente dipendente. Questo linguaggio ha un'interfaccia monade con pre e post-condizioni nello spirito del del Nanevski et al Hoare Type Theory (o anche la mia Integrazione lineare e tipi dipendenti ), e ha un sottoinsieme definito che può essere compilato in codice C di basso livello - infatti, stanno già inviando codice crittografico verificato come parte di Firefox!

    Per essere chiari, né la F-star né l'HTT sono linguaggi tipicamente lineari, ma il linguaggio dell'indice per le loro monadi di solito si basa sulla logica di separazione di Reynold e O'Hearn , che è una logica sostrutturale correlata alla logica lineare che ha visto un grande successo come il linguaggio delle asserzioni per le logiche Hoare per i programmi puntatore.

  2. Il secondo approccio è semplicemente specificare quale assembly (o qualunque IR di basso livello si desideri) e quindi utilizzare una qualche forma di logica lineare o di separazione per ragionare sul suo comportamento direttamente in un assistente di prova. In sostanza, è possibile utilizzare l'assistente di correzione o il linguaggio tipicamente dipendente come un assemblatore di macro molto elaborato che genera solo programmi corretti.

    La logica di separazione di alto livello di Jensen et al per il codice di basso livello ne è un esempio particolarmente puro: crea una logica di separazione per l'assemblaggio x86! Tuttavia, ci sono molti progetti in questo senso, come il Software Toolchain verificato a Princeton e il progetto CertiKOS a Yale.

Tutti questi approcci "sembreranno" un po 'come Rust, poiché tenere traccia della proprietà limitando l'uso delle variabili è la chiave per tutti.


3

I tipi lineari e la logica di separazione sono entrambi fantastici, ma possono richiedere un po 'di sforzo da programmatore. Scrivere un elenco di collegamenti sicuri in Rust potrebbe essere piuttosto difficile, per esempio.

Ma esiste un'alternativa che richiede molto meno sforzo da parte del programmatore, sebbene con garanzie meno rigide. Un (piuttosto vecchio) flusso di lavoro è quello di garantire la sicurezza della memoria usando (di solito una pila di) regioni. Usando l'inferenza della regione, un compilatore può decidere staticamente in quale regione dovrebbe andare una parte di dati allocati e dislocare la regione quando non rientra nell'ambito.

L'inferenza della regione è decisamente sicura (non è possibile deallocare memoria raggiungibile) e richiede un'interferenza minima del programmatore, ma non è "totale" (cioè può comunque perdere memoria, anche se sicuramente molto meglio di "non fare nulla"), quindi di solito è combinata con GC in pratica. IlMLtonIl compilatore ML Kit utilizza le regioni per eliminare la maggior parte delle chiamate GC, ma ha ancora un GC perché altrimenti perderebbe memoria. Secondo alcuni dei primi pionieri delle regioni, l'inferenza di regione non è stata effettivamente inventata per questo scopo (era per la parallelizzazione automatica, credo); ma si è scoperto che potrebbe essere utilizzato anche per la gestione della memoria.

Per un punto di partenza, direi il saggio "Implementazione del calcolo tipizzato Call-by-Value λ-calculus usando una pila di regioni" di Mads Tofte e Jean-Pierre Talpin. Per ulteriori articoli sull'inferenza della regione, cerca altri articoli di M. Tofte e J.-P. Talpin, alcune delle opere di Pierre Jouvelot, nonché la serie di articoli su Ciclone di Greg Morrisett, Mike Hicks e Dan Grossman.


-2

Uno schema banale per i sistemi "bare metal" è semplicemente vietare tutte le allocazioni di memoria di runtime. Ricorda, anche la malloc/freecoppia C richiede una libreria di runtime. Ma anche quando tutti gli oggetti sono definiti in fase di compilazione, possono essere definiti in modo typesafe.

Il problema principale qui è la finzione di valori immutabili in linguaggi funzionali puri, che vengono creati durante l'esecuzione del programma. L'hardware reale (e certamente i sistemi bare metal) si basano sulla RAM mutabile, che è in offerta limitata. Il runtime di un'implementazione del linguaggio funzionale in pratica alloca dinamicamente la RAM quando vengono creati nuovi valori "immutabili" e immondizia li raccoglie quando il valore "immutabile" non è più necessario.

E per i problemi più interessanti, la durata di almeno alcuni valori dipende dall'input di runtime (utente), quindi la durata non può essere determinata staticamente. Ma anche se la durata non dipende dall'input, può essere altamente non banale. Prendi il semplice programma ripetutamente trovando numeri primi semplicemente controllando ogni numero in ordine, controllando contro tutti i numeri primi fino a sqrt(N). Chiaramente questo ha bisogno di mantenere i numeri primi e può riciclare la memoria utilizzata per i non primi.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.