Condividi tramite


Informazioni su SAL

Il linguaggio di annotazione del codice sorgente Microsoft (SAL) fornisce un set di annotazioni che è possibile usare per descrivere in che modo una funzione usa i relativi parametri, i presupposti su di essi e le garanzie che rende al termine. Le annotazioni vengono definite nel file <sal.h>di intestazione . L'analisi del codice di Visual Studio per C++ usa annotazioni SAL per modificarne l'analisi delle funzioni. Per altre informazioni su SAL 2.0 per lo sviluppo di driver Windows, vedere Annotazioni SAL 2.0 per i driver windows.

In modo nativo, C e C++ offrono solo modi limitati per gli sviluppatori di esprimere in modo coerente finalità e invarianza. Usando le annotazioni SAL, è possibile descrivere le funzioni in modo più dettagliato in modo che gli sviluppatori che li usano possano comprendere meglio come usarle.

Che cos'è SAL e perché è consigliabile usarlo?

Detto semplicemente, SAL è un modo economico per consentire al compilatore di controllare il codice per l'utente.

SAL rende il codice più prezioso

SAL consente di rendere la progettazione del codice più comprensibile, sia per gli esseri umani che per gli strumenti di analisi del codice. Si consideri questo esempio che mostra la funzione memcpydi runtime C :

void * memcpy(
   void *dest,
   const void *src,
   size_t count
);

È possibile stabilire cosa fa questa funzione? Quando una funzione viene implementata o chiamata, è necessario mantenere determinate proprietà per garantire la correttezza del programma. Esaminando solo una dichiarazione, ad esempio quella nell'esempio, non si sa cosa sono. Senza annotazioni SAL, è necessario basarsi sulla documentazione o sui commenti del codice. Ecco la documentazione per memcpy :

"memcpy copia i byte da src a dest; wmemcpy copia il numero di caratteri wide (due byte). Se l'origine e la destinazione si sovrappongono, il comportamento di memcpy non è definito. Usare memmove per gestire le aree di sovrapposizione.
Importante: assicurarsi che il buffer di destinazione sia la stessa dimensione o maggiore del buffer di origine. Per altre informazioni, vedere Evitare sovraccarichi del buffer."

La documentazione contiene un paio di bit di informazioni che suggeriscono che il codice deve mantenere determinate proprietà per garantire la correttezza del programma:

  • memcpy copia il count di byte dal buffer di origine al buffer di destinazione.

  • Il buffer di destinazione deve essere almeno di grandi dimensioni del buffer di origine.

Tuttavia, il compilatore non può leggere la documentazione o i commenti informali. Non sa che esiste una relazione tra i due buffer e counte non può indovinare in modo efficace una relazione. SAL potrebbe fornire maggiore chiarezza sulle proprietà e sull'implementazione della funzione, come illustrato di seguito:

void * memcpy(
   _Out_writes_bytes_all_(count) void *dest,
   _In_reads_bytes_(count) const void *src,
   size_t count
);

Si noti che queste annotazioni sono simili alle informazioni nella documentazione, ma sono più concise e seguono un modello semantico. Quando si legge questo codice, è possibile comprendere rapidamente le proprietà di questa funzione e come evitare problemi di sicurezza di sovraccarico del buffer. Ancora meglio, i modelli semantici forniti da SAL possono migliorare l'efficienza e l'efficacia degli strumenti di analisi automatica del codice nella prima individuazione di potenziali bug. Si supponga di scrivere questa implementazione buggy di wmemcpy:

wchar_t * wmemcpy(
   _Out_writes_all_(count) wchar_t *dest,
   _In_reads_(count) const wchar_t *src,
   size_t count)
{
   size_t i;
   for (i = 0; i <= count; i++) { // BUG: off-by-one error
      dest[i] = src[i];
   }
   return dest;
}

Questa implementazione contiene un errore off-by-one comune. Fortunatamente, l'autore del codice includeva l'annotazione delle dimensioni del buffer SAL. Uno strumento di analisi del codice potrebbe intercettare il bug analizzando da solo questa funzione.

Nozioni di base su SAL

SAL definisce quattro tipi di base di parametri, classificati in base al modello di utilizzo.

Categoria Annotazione dei parametri Descrizione
Input per la funzione chiamata _In_ I dati vengono passati alla funzione chiamata e vengono considerati di sola lettura.
Input per la funzione chiamata e output al chiamante _Inout_ I dati utilizzabili vengono passati alla funzione e potenzialmente vengono modificati.
Output al chiamante _Out_ Il chiamante fornisce spazio solo per la funzione chiamata in cui scrivere. La funzione chiamata scrive i dati in tale spazio.
Output del puntatore al chiamante _Outptr_ Come Output al chiamante. Il valore restituito dalla funzione chiamata è un puntatore.

Queste quattro annotazioni di base possono essere rese più esplicite in vari modi. Per impostazione predefinita, si presuppone che i parametri del puntatore con annotazioni siano obbligatori, perché la funzione abbia esito positivo, devono essere non NULL. La variante più comune delle annotazioni di base indica che un parametro puntatore è facoltativo, se è NULL, la funzione può comunque riuscire a svolgere il proprio lavoro.

Questa tabella illustra come distinguere i parametri obbligatori e facoltativi:

I parametri sono obbligatori I parametri sono facoltativi
Input per la funzione chiamata _In_ _In_opt_
Input per la funzione chiamata e output al chiamante _Inout_ _Inout_opt_
Output al chiamante _Out_ _Out_opt_
Output del puntatore al chiamante _Outptr_ _Outptr_opt_

Queste annotazioni consentono di identificare i possibili valori non inizializzati e l'uso di puntatori Null non validi in modo formale e accurato. Il passaggio di NULL a un parametro obbligatorio potrebbe causare un arresto anomalo o potrebbe causare la restituzione di un codice di errore "non riuscito". In entrambi i casi, la funzione non riesce a eseguire il proprio processo.

Esempi SAL

Questa sezione illustra esempi di codice per le annotazioni SAL di base.

Uso dello strumento di analisi di Visual Studio Code per individuare i difetti

Negli esempi, lo strumento di analisi di Visual Studio Code viene usato insieme alle annotazioni SAL per individuare i difetti del codice. Ecco come farlo.

Per usare gli strumenti di analisi del codice di Visual Studio e SAL

  1. In Visual Studio aprire un progetto C++ contenente annotazioni SAL.

  2. Nella barra dei menu scegliere Compila, Esegui analisi del codice nella soluzione.

    Si consideri l'esempio _In_ in questa sezione. Se si esegue l'analisi del codice, viene visualizzato questo avviso:

    C6387 Il valore del parametro non valido 'pInt' potrebbe essere '0': questo non rispetta la specifica per la funzione 'InCallee'.

Esempio: l'annotazione _In_

L'annotazione _In_ indica che:

  • Il parametro deve essere valido e non verrà modificato.

  • La funzione leggerà solo dal buffer a singolo elemento.

  • Il chiamante deve fornire il buffer e inizializzarlo.

  • _In_ specifica "sola lettura". Un errore comune consiste nell'applicare _In_ a un parametro che deve avere invece l'annotazione _Inout_ .

  • _In_ è consentito ma ignorato dall'analizzatore su scalari non puntatori.

void InCallee(_In_ int *pInt)
{
   int i = *pInt;
}

void GoodInCaller()
{
   int *pInt = new int;
   *pInt = 5;

   InCallee(pInt);
   delete pInt;
}

void BadInCaller()
{
   int *pInt = NULL;
   InCallee(pInt); // pInt should not be NULL
}

Se si usa l'analisi di Visual Studio Code in questo esempio, i chiamanti passano un puntatore non Null a un buffer inizializzato per pInt. In questo caso, pInt il puntatore non può essere NULL.

Esempio: annotazione _In_opt_

_In_opt_ è uguale _In_a , ad eccezione del fatto che il parametro di input può essere NULL e, pertanto, la funzione deve verificare la presenza di questo valore.

void GoodInOptCallee(_In_opt_ int *pInt)
{
   if(pInt != NULL) {
      int i = *pInt;
   }
}

void BadInOptCallee(_In_opt_ int *pInt)
{
   int i = *pInt; // Dereferencing NULL pointer 'pInt'
}

void InOptCaller()
{
   int *pInt = NULL;
   GoodInOptCallee(pInt);
   BadInOptCallee(pInt);
}

L'analisi di Visual Studio Code verifica che la funzione verifichi la presenza di NULL prima di accedere al buffer.

Esempio: l'annotazione _Out_

_Out_ supporta uno scenario comune in cui viene passato un puntatore non NULL che punta a un buffer di elementi e la funzione inizializza l'elemento. Il chiamante non deve inizializzare il buffer prima della chiamata; la funzione chiamata promette di inizializzarla prima che venga restituita.

void GoodOutCallee(_Out_ int *pInt)
{
   *pInt = 5;
}

void BadOutCallee(_Out_ int *pInt)
{
   // Did not initialize pInt buffer before returning!
}

void OutCaller()
{
   int *pInt = new int;
   GoodOutCallee(pInt);
   BadOutCallee(pInt);
   delete pInt;
}

Visual Studio Code Analysis Tool convalida che il chiamante passa un puntatore non NULL a un buffer per pInt e che il buffer viene inizializzato dalla funzione prima che venga restituito.

Esempio: annotazione _Out_opt_

_Out_opt_ è uguale _Out_a , ad eccezione del fatto che il parametro può essere NULL e, pertanto, la funzione deve verificarne la presenza.

void GoodOutOptCallee(_Out_opt_ int *pInt)
{
   if (pInt != NULL) {
      *pInt = 5;
   }
}

void BadOutOptCallee(_Out_opt_ int *pInt)
{
   *pInt = 5; // Dereferencing NULL pointer 'pInt'
}

void OutOptCaller()
{
   int *pInt = NULL;
   GoodOutOptCallee(pInt);
   BadOutOptCallee(pInt);
}

L'analisi di Visual Studio Code convalida che questa funzione verifica la presenza di NULL prima pInt che venga dereferenziata e, se pInt non è NULL, che il buffer viene inizializzato dalla funzione prima che venga restituito.

Esempio: annotazione _Inout_

_Inout_ viene usato per annotare un parametro puntatore che può essere modificato dalla funzione . Il puntatore deve puntare a dati inizializzati validi prima della chiamata e, anche se viene modificato, deve avere comunque un valore valido in caso di restituzione. L'annotazione specifica che la funzione può leggere liberamente e scrivere nel buffer di un elemento. Il chiamante deve fornire il buffer e inizializzarlo.

Nota

Come _Out_, _Inout_ deve essere applicato a un valore modificabile.

void InOutCallee(_Inout_ int *pInt)
{
   int i = *pInt;
   *pInt = 6;
}

void InOutCaller()
{
   int *pInt = new int;
   *pInt = 5;
   InOutCallee(pInt);
   delete pInt;
}

void BadInOutCaller()
{
   int *pInt = NULL;
   InOutCallee(pInt); // 'pInt' should not be NULL
}

L'analisi di Visual Studio Code convalida che i chiamanti passano un puntatore non NULL a un buffer inizializzato per pInte che, prima di restituire, pInt è ancora diverso da NULL e il buffer viene inizializzato.

Esempio: annotazione _Inout_opt_

_Inout_opt_ è uguale _Inout_a , ad eccezione del fatto che il parametro di input può essere NULL e, pertanto, la funzione deve verificare la presenza di questo valore.

void GoodInOutOptCallee(_Inout_opt_ int *pInt)
{
   if(pInt != NULL) {
      int i = *pInt;
      *pInt = 6;
   }
}

void BadInOutOptCallee(_Inout_opt_ int *pInt)
{
   int i = *pInt; // Dereferencing NULL pointer 'pInt'
   *pInt = 6;
}

void InOutOptCaller()
{
   int *pInt = NULL;
   GoodInOutOptCallee(pInt);
   BadInOutOptCallee(pInt);
}

L'analisi di Visual Studio Code convalida che questa funzione verifica la presenza di NULL prima di accedere al buffer e, se pInt non è NULL, che il buffer viene inizializzato dalla funzione prima che venga restituito.

Esempio: l'annotazione _Outptr_

_Outptr_ viene usato per annotare un parametro destinato a restituire un puntatore. Il parametro stesso non deve essere NULL e la funzione chiamata restituisce un puntatore non NULL e tale puntatore punta ai dati inizializzati.

void GoodOutPtrCallee(_Outptr_ int **pInt)
{
   int *pInt2 = new int;
   *pInt2 = 5;

   *pInt = pInt2;
}

void BadOutPtrCallee(_Outptr_ int **pInt)
{
   int *pInt2 = new int;
   // Did not initialize pInt buffer before returning!
   *pInt = pInt2;
}

void OutPtrCaller()
{
   int *pInt = NULL;
   GoodOutPtrCallee(&pInt);
   BadOutPtrCallee(&pInt);
}

L'analisi di Visual Studio Code convalida che il chiamante passa un puntatore non NULL per *pInte che il buffer viene inizializzato dalla funzione prima che venga restituito.

Esempio: annotazione _Outptr_opt_

_Outptr_opt_ è uguale _Outptr_a , ad eccezione del fatto che il parametro è facoltativo. Il chiamante può passare un puntatore NULL per il parametro .

void GoodOutPtrOptCallee(_Outptr_opt_ int **pInt)
{
   int *pInt2 = new int;
   *pInt2 = 6;

   if(pInt != NULL) {
      *pInt = pInt2;
   }
}

void BadOutPtrOptCallee(_Outptr_opt_ int **pInt)
{
   int *pInt2 = new int;
   *pInt2 = 6;
   *pInt = pInt2; // Dereferencing NULL pointer 'pInt'
}

void OutPtrOptCaller()
{
   int **ppInt = NULL;
   GoodOutPtrOptCallee(ppInt);
   BadOutPtrOptCallee(ppInt);
}

L'analisi di Visual Studio Code convalida che questa funzione controlla la presenza di NULL prima *pInt che venga dereferenziata e che il buffer venga inizializzato dalla funzione prima che venga restituito.

Esempio: l'annotazione _Success_ in combinazione con _Out_

Le annotazioni possono essere applicate alla maggior parte degli oggetti. In particolare, è possibile annotare un'intera funzione. Una delle caratteristiche più ovvie di una funzione è che può avere esito positivo o negativo. Tuttavia, come l'associazione tra un buffer e le relative dimensioni, C/C++ non può esprimere l'esito positivo o negativo della funzione. Usando l'annotazione, è possibile pronunciare l'esito _Success_ positivo di una funzione. Il parametro dell'annotazione è solo un'espressione _Success_ che, quando è true, indica che la funzione ha avuto esito positivo. L'espressione può essere qualsiasi elemento che il parser di annotazione può gestire. Gli effetti delle annotazioni dopo la restituzione della funzione sono applicabili solo quando la funzione ha esito positivo. Questo esempio mostra come _Success_ interagisce con _Out_ per eseguire la cosa giusta. È possibile usare la parola chiave return per rappresentare il valore restituito.

_Success_(return != false) // Can also be stated as _Success_(return)
bool GetValue(_Out_ int *pInt, bool flag)
{
   if(flag) {
      *pInt = 5;
      return true;
   } else {
      return false;
   }
}

L'annotazione _Out_ fa sì che l'analisi di Visual Studio Code verifichi che il chiamante passi un puntatore non NULL a un buffer per pInte che il buffer venga inizializzato dalla funzione prima che venga restituito.

Procedure consigliate SAL

Aggiunta di annotazioni al codice esistente

SAL è una tecnologia potente che consente di migliorare la sicurezza e l'affidabilità del codice. Dopo aver appreso SAL, è possibile applicare la nuova competenza al lavoro quotidiano. Nel nuovo codice è possibile usare specifiche basate su SAL in base alla progettazione; nel codice precedente, è possibile aggiungere annotazioni in modo incrementale e quindi aumentare i vantaggi ogni volta che si aggiorna.

Le intestazioni pubbliche Microsoft sono già annotate. Pertanto, ti consigliamo di aggiungere prima annotazioni alle funzioni e alle funzioni dei nodi foglia che chiamano API Win32 per ottenere il massimo vantaggio.

Quando si annotano?

Ecco alcune linee guida:

  • Annotare tutti i parametri del puntatore.

  • Annotare le annotazioni dell'intervallo di valori in modo che l'analisi del codice possa garantire la sicurezza del buffer e del puntatore.

  • Annotare le regole di blocco e gli effetti collaterali di blocco. Per altre informazioni, vedere Annotazione del comportamento di blocco.

  • Annotare le proprietà del driver e altre proprietà specifiche del dominio.

In alternativa, è possibile annotare tutti i parametri per rendere chiara la finalità in tutto e per semplificare la verifica che le annotazioni siano state eseguite.

Vedi anche