2009-10-07 10 views
8

Se si è qualcuno che programma in C o C++, senza i vantaggi del linguaggio gestito della gestione della memoria, controllo del tipo o protezione del sovraccarico del buffer, utilizzando l'aritmetica del puntatore, come si fa a garantire che i programmi siano sicuri? Usi molti test unitari o sei solo un programmatore cauto? Hai altri metodi?Come si programma in modo sicuro al di fuori di un ambiente di codice gestito?

+0

Non mi aspetto davvero che ci sia una miriade di risposte. Sospetto che esistano pochissime tecniche di base utilizzate dalla maggior parte delle persone. –

+7

C/C++ dispone di gestione della memoria (puntatori intelligenti). È un linguaggio fortemente tipizzato. La protezione del buffer è facoltativa (utilizzare a() anziché all'operatore []). Quindi non è come se usassimo coltelli di pietra qui. –

+3

@Martin Di solito non sono uno che entra nel merito "Mi lamento con qualcuno che ha usato" C/C++ "", ma penso che valga la pena di dire semplicemente C++ in questa affermazione. C non ha metodi standard per la protezione della memoria o il controllo dei limiti del buffer. – Falaina

risposta

24

Tutto quanto sopra. Io uso:

  1. molta cautela
  2. Puntatori Intelligente il più possibile
  3. Strutture dati che sono stati testati, un sacco di standard library
  4. Prove di unità per tutto il tempo
  5. strumenti di validazione di memoria come MemValidator e AppVerifier
  6. Pregate ogni notte che non si blocchi sul sito del cliente.

In realtà, sto solo esagerando. Non è male e in realtà non è troppo difficile mantenere il controllo delle risorse se si struttura correttamente il codice.

Nota interessante. Ho una grande applicazione che utilizza DCOM e ha moduli gestiti e non gestiti. I moduli non gestiti in genere sono difficili da eseguire il debug durante lo sviluppo, ma funzionano molto bene sul sito del cliente a causa dei numerosi test eseguiti su di esso. I moduli gestiti a volte soffrono di codice errato perché il garbage collector è così flessibile, i programmatori diventano pigri nel controllare l'utilizzo delle risorse.

+0

I puntatori intelligenti sono uno di cui non ho mai sentito parlare prima. Dovrò verificarlo. –

+0

Devo usarne molti perché sono afflitto dal codice legacy COM. Senza puntatori intelligenti perderò la traccia di tutti quei riferimenti e la memoria delle emorragie. –

+2

Ho sviluppato un'allergia per aver visto puntatori nudi in codice C++. Se ne vedo uno il mio istinto è di avvolgerlo in un puntatore intelligente, anche se non è necessario. L'istinto mi è servito bene - non ricordo di avere un puntatore pendente per probabilmente dieci anni o più. – philsquared

13

Altrettanto rilevante - come si fa si assicurarsi che i file e le prese siano chiusi, i blocchi rilasciati, yada yada. La memoria non è l'unica risorsa e, con GC, perdi di per sé una distruzione affidabile/tempestiva.

Né GC né non GC sono automaticamente superiori. Ognuno ha vantaggi, ognuno ha il suo prezzo e un buon programmatore dovrebbe essere in grado di far fronte a entrambi.

Ho detto tanto in una risposta a this question.

+0

Esistono tecniche per eseguire RAII nei linguaggi gestiti: http: //www.levelofindirection.com/journal/2009/9/24/raii-and-closures-in-java.html http://www.levelofindirection.com/journal/2009/9/24/rai-and-readability-in-c. html – philsquared

+0

... e http://www.levelofindirection.com/journal/2009/9/24/raii-and-closures-in-java.html – philsquared

+1

@Phil - lettura interessante, ma ovviamente chiunque pensi "questo dimostra che C# e Java battono C++ "dovrebbero effettivamente leggere quei collegamenti. Se un idioma era una cura magica, gli idiomi per assicurare la corretta eliminazione degli oggetti allocati su heap in C++ sarebbero anche cure magiche, e non vedremmo i fan della garbage collection prendere in giro il C++. – Steve314

0

C++ ha tutte le funzionalità citate.

C'è la gestione della memoria. È possibile utilizzare gli Smart Pointer per un controllo molto preciso. Oppure ci sono un paio di Garbage Collectors disponibili sebbene non facciano parte dello standard (ma la maggior parte delle situazioni Smart Pointers sono più che adeguate).

C++ è un linguaggio fortemente tipizzato. Proprio come C#.

Utilizziamo buffer. Puoi scegliere di utilizzare la versione controllata dei limiti dell'interfaccia. Ma se sai che non c'è un problema, sei libero di usare la versione non controllata dell'interfaccia.

Confrontare il metodo a() (selezionato) con l'operatore [] (Deselezionato).

Sì, usiamo Test unità. Proprio come dovresti usare in C#.

Sì, siamo programmatori cauti. Proprio come dovresti essere in C#. L'unica differenza è che le insidie ​​sono diverse nelle due lingue.

+0

Non ho visto la domanda "C++ ha i moderni vantaggi della gestione della memoria", ma "Se si programma in C++, * senza * i moderni vantaggi della gestione della memoria, ..., come ci si assicura che i tuoi programmi sono al sicuro? " –

+0

Se programma senza puntatori intelligenti, è molto più difficile assicurarsi che i miei programmi siano al sicuro. Non vedo la rilevanza, però. Se si programma in C# senza utilizzare l'istruzione "using" (che IIRC è un'aggiunta abbastanza recente), come si assicura che le altre risorse siano disposte correttamente? –

+0

Non sono adeguati i puntatori intelligenti nelle stesse situazioni in cui il conteggio dei riferimenti VB6 e COM era adeguato? Questo è ciò che Microsoft ha voluto migliorare quando ha scelto lo stile .NET della garbage collection. – MarkJ

16

Uso un sacco di asserzioni e costruisco sia una versione di "debug" che una versione di "rilascio". La mia versione di debug funziona molto più lentamente della mia versione di rilascio, con tutti i controlli che esegue.

Corro frequentemente sotto Valgrind e il mio codice ha zero perdite di memoria. Zero. È molto più facile mantenere un programma privo di perdite piuttosto che prendere un programma bacato e correggere tutte le perdite.

Inoltre, il mio codice viene compilato senza avvisi, nonostante il set del compilatore sia impostato per avvisi extra. A volte gli avvertimenti sono sciocchi, ma a volte puntano a un bug e io lo aggiusto senza bisogno di trovarlo nel debugger.

Sto scrivendo pure C (non posso usare C++ su questo progetto), ma sto facendo C in un modo molto coerente. Ho classi orientate agli oggetti, con costruttori e distruttori; Devo chiamarli a mano, ma la coerenza aiuta. E se dimentico di chiamare un distruttore, Valgrind mi colpisce in testa finché non lo aggiusto.

Oltre al costruttore e al distruttore, scrivo una funzione di autocontrollo che controlla l'oggetto e decide se è sensato o meno; ad esempio, se un handle di file è nullo ma i dati di file associati non vengono azzerati, ciò indica un qualche tipo di errore (l'handle è stato danneggiato, o il file non è stato aperto ma quei campi nell'oggetto hanno il cestino). Inoltre, molti dei miei oggetti hanno un campo "firma" che deve essere impostato su un valore specifico (specifico per ogni oggetto diverso). Le funzioni che utilizzano oggetti in genere affermano che gli oggetti sono sani di mente.

Ogni volta che I malloc() un po 'di memoria, la mia funzione riempie la memoria con i valori 0xDC. Una struttura che non è completamente inizializzata diventa ovvia: i conteggi sono troppo grandi, i puntatori non sono validi (0xDCDCDCDC), e quando guardo la struttura nel debugger è ovvio che non è inizializzato. Questo è molto meglio della memoria a riempimento zero quando si chiama malloc(). (Ovviamente il riempimento 0xDC è solo nella compilazione di debug, non è necessario che la versione di rilascio rifiuti quella volta.)

Ogni volta che liberare memoria, cancello il puntatore. In questo modo, se ho uno stupido bug in cui il codice tenta di utilizzare un puntatore dopo che la sua memoria è stata liberata, ottengo immediatamente un'eccezione a puntatore nullo, che mi indica esattamente l'errore. Le mie funzioni di destructor non prendono un puntatore a un oggetto, prendono un puntatore a un puntatore e clobberano il puntatore dopo aver distrutto l'oggetto. Inoltre, i distruttori cancellano i loro oggetti prima di liberarli, quindi se un pezzo di codice ha una copia di un puntatore e prova a usare un oggetto, l'assegno di integrità controlla immediatamente.

Valgrind mi dirà se un codice scrive fuori dalla fine di un buffer. Se non avessi questo, avrei messo i valori "canarini" dopo la fine dei buffer, e ho avuto il test di integrità testarli. Questi valori canarino, come i valori delle firme, sarebbero di tipo debug-build-only, quindi la versione di rilascio non avrebbe una memoria gonfia.

Ho una raccolta di test unitari e quando applico qualsiasi modifica importante al codice, è molto confortante eseguire i test unitari e avere una certa sicurezza di non aver rotto in modo orribile le cose. Naturalmente eseguo i test unitari sia sulla versione di debug che sulla versione di rilascio, quindi tutte le mie affermazioni hanno la possibilità di trovare problemi.

Mettere tutta questa struttura in posizione è stato un po 'di sforzo in più, ma ripaga ogni giorno. E mi sento abbastanza felice quando un assertore spara e mi indica un errore, invece di dover eseguire l'errore nel debugger. A lungo termine, è solo meno lavoro per mantenere le cose pulite tutto il tempo.

Infine, devo dire che in realtà mi piace la notazione ungherese.Ho lavorato a Microsoft qualche anno fa e, come Joel, ho appreso l'app Ungherese e non la variante spezzata. Lo fa davvero make wrong code look wrong.

+1

Sembra tutto fantastico ... ma sono contento di avere persone come Eric Lippert che mettono la struttura in posizione senza che io alzi un dito. – MarkJ

2

La risposta di Andrew è buona, ma aggiungerei anche disciplina all'elenco. Trovo che dopo un po 'di pratica con il C++ si ottiene un buon feeling per ciò che è sicuro e che cosa è begging for the velociraptors to come eat you. Si tende a sviluppare uno stile di codifica che si sente a proprio agio quando si seguono le pratiche di sicurezza e si lascia che si senta l'heebie-jeebies dovresti provare a dire , lancia un puntatore intelligente su un puntatore non elaborato e passa a qualcos'altro.

Mi piace pensare ad esso come un elettroutensile in un negozio. È abbastanza sicuro una volta che hai imparato ad usarlo correttamente e fintanto che ti assicuri di seguire sempre tutte le regole di sicurezza. È quando pensi di poter rinunciare agli occhiali di sicurezza che ti fanno male.

1

Ho eseguito sia C++ che C# e non vedo tutto l'hype sul codice gestito.

Oh, giusto, c'è un garbage collector per la memoria, che è utile ... a meno che non si astenga dall'usare semplici puntatori vecchi in C++, ovviamente, se si usano solo smart_pointers, non si hanno così tanti problemi.

Ma poi mi piacerebbe sapere ... Il vostro garbage collector vi protegge da:

  • mantenendo le connessioni al database aperto?
  • mantenendo blocchi sui file?
  • ...

c'è molto di più alla gestione delle risorse di gestione della memoria. La cosa buona è C++ è che si impara rapidamente cosa significa gestione delle risorse e RAII, in modo che diventi un riflesso:

  • se voglio un puntatore, voglio un auto_ptr, uno shared_ptr o un weak_ptr
  • se voglio una connessione DB, voglio una 'connessione' oggetto
  • se apro un file, voglio un oggetto 'File'
  • ...

per quanto riguarda i sovraccarichi del buffer, beh, non è come stiamo usando char * e size_t ovunque. Abbiamo alcune cose che chiamiamo 'stringa', 'iostream' e, naturalmente, il già citato vettore :: al metodo che ci libera da quei vincoli.

Le librerie testate (stl, boost) sono buone, li usano e passano a problemi più funzionali.

+2

Le connessioni al database e i blocchi dei file sono un'aringa rossa. Esistono modelli semplici e ben stabiliti per questi nei linguaggi gestiti. In C# è l'istruzione "using", che dispone di risorse automaticamente quando non sono più necessarie. –

+2

IMO il problema principale con i puntatori intelligenti in C++ è che non esiste uno standard reale. Se si utilizzano librerie/framework di terze parti, è molto improbabile che tutti utilizzino lo stesso tipo di puntatore intelligente. In questo modo è possibile fare affidamento su di essi all'interno di un modulo, ma non appena si interfacciano componenti di fornitori diversi, si torna alla gestione manuale della memoria. – Niki

+0

@nikie: quando utilizzo componenti di terze parti, mi aspetto che siano molto chiari nella loro strategia di gestione della memoria. Ma poi, le sole 3 librerie che abbiamo a lavoro sono OpenSource come Boost o Cyptopp, quindi non ho molta esperienza lì. –

1

Accanto a molti dei buoni consigli qui riportati, il mio strumento più importante è DRY - Do not Repeat Yourself. Non diffondo codice soggetto a errori (ad esempio per gestire le allocazioni di memoria con malloc() e free()) su tutta la mia base di codice. Ho esattamente una singola posizione nel mio codice in cui vengono chiamati malloc e gratuiti. È nelle funzioni wrapper MemoryAlloc e MemoryFree.

C'è tutto il controllo degli argomenti e la gestione iniziale degli errori che di solito viene fornita come codice di codice ripetuto attorno alla chiamata a malloc. Inoltre, abilita qualsiasi cosa con la necessità di modificare solo una posizione, iniziando con semplici controlli di debug come contare le chiamate di successo a malloc e gratuite e verificare alla fine del programma che entrambi i numeri siano uguali, fino a tutti i tipi di controlli di sicurezza estesi.

A volte, quando ho letto una domanda come "Devo sempre assicurarmi che strncpy termini la stringa, c'è un'alternativa?"

strncpy(dst, src, n); 
dst[n-1] = '\0'; 

seguito da giorni di discussione, mi chiedo sempre se l'arte di estrarre funzionalità ripetuto in funzioni è un'arte perduta di programmazione più elevato che non viene insegnato nelle lezioni di programmazione.

char *my_strncpy (dst, src, n) 
{ 
    assert((dst != NULL) && (src != NULL) && (n > 0)); 
    strncpy(dst, src, n); 
    dst[n-1] = '\0'; 
    return dst; 
} 

problema primario di duplicazione del codice risolto - Ora pensiamo se strncpy è davvero lo strumento giusto per il lavoro. Prestazione? Ottimizzazione prematura! E una posizione unica per iniziare con esso dopo che si rivela essere il collo di bottiglia.

3

Utilizzo il C++ da 10 anni. Ho usato C, Perl, Lisp, Delphi, Visual Basic 6, C#, Java e varie altre lingue che non riesco a ricordare in cima alla mia testa.

La risposta alla tua domanda è semplice: devi sapere cosa stai facendo, più di C#/Java. Il numero superiore a è ciò che genera rilanci simili a quelli di Jeff Atwood relativi allo "Java Schools".

La maggior parte delle domande, in un certo senso, sono prive di senso. I "problemi" che si presentano sono semplicemente le informazioni su come l'hardware funziona davvero su. Mi piacerebbe sfidarti a scrivere una CPU & RAM in VHDL/Verilog e vedere come funziona davvero, anche quando è davvero semplificato. Inizierete ad apprezzare che il modo C#/Java è un modo di fare astrazione sull'hardware.

Una sfida più semplice sarebbe quella di programmare un sistema operativo elementare per un sistema integrato dall'accensione iniziale; ti mostrerà anche ciò che devi sapere.

(Ho anche scritto C# e Java)

+0

+1 per "devi sapere cosa stai facendo". –

+0

Fare domande fa parte del processo di raggiungere il luogo in cui "sai cosa stai facendo". –

+0

Non ti sto bussando, Robert. Ti ho dato la migliore comprensione di come si programma in sicurezza al di fuori del codice VM, oltre a un percorso per comprendere le macchine reali. –

3

Scriviamo in C per sistemi embedded. Oltre a utilizzare alcune delle tecniche comuni a qualsiasi linguaggio di programmazione o ambiente, impieghiamo anche:

  • Uno strumento di analisi statica (ad esempio PC-Lint).
  • Conformità allo MISRA-C (applicato dallo strumento di analisi statico).
  • Assenza di allocazione dinamica della memoria.
Problemi correlati