Partilhar via


Compreender a SAL

A linguagem de anotação de código-fonte (SAL) da Microsoft fornece um conjunto de anotações que você pode usar para descrever como uma função usa seus parâmetros, as suposições que faz sobre eles e as garantias que faz quando termina. As anotações são definidas no arquivo de cabeçalho <sal.h>. A análise de código do Visual Studio para C++ usa anotações SAL para modificar sua análise de funções. Para obter mais informações sobre o SAL 2.0 para desenvolvimento de drivers do Windows, consulte Anotações do SAL 2.0 para drivers do Windows.

Nativamente, C e C++ fornecem apenas maneiras limitadas para os desenvolvedores expressarem consistentemente intenção e invariância. Usando anotações SAL, você pode descrever suas funções com mais detalhes para que os desenvolvedores que as estão consumindo possam entender melhor como usá-las.

O que é SAL e por que você deve usá-lo?

Simplificando, SAL é uma maneira barata de deixar o compilador verificar seu código para você.

SAL torna o código mais valioso

O SAL pode ajudá-lo a tornar seu design de código mais compreensível, tanto para humanos quanto para ferramentas de análise de código. Considere este exemplo que mostra a função memcpyde tempo de execução C:

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

Você pode dizer o que essa função faz? Quando uma função é implementada ou chamada, certas propriedades devem ser mantidas para garantir a correção do programa. Basta olhar para uma declaração como a do exemplo, não se sabe quais são. Sem anotações SAL, você teria que confiar na documentação ou comentários de código. Veja o que diz a documentação memcpy :

memcpy copia contagem bytes de src para dest; wmemcpy copia contagem caracteres largos (dois bytes). Se a origem e o destino se sobrepõem, o comportamento de memcpy é indefinido. Use memmove para lidar com regiões sobrepostas.
Importante: Verifique se o buffer de destino é do mesmo tamanho ou maior que o buffer de origem. Para obter mais informações, consulte Evitando saturações de buffer."

A documentação contém alguns bits de informações que sugerem que seu código tem que manter certas propriedades para garantir a correção do programa:

  • memcpy copia count de bytes do buffer de origem para o buffer de destino.

  • O buffer de destino deve ser pelo menos tão grande quanto o buffer de origem.

No entanto, o compilador não pode ler a documentação ou comentários informais. Ele não sabe que há uma relação entre os dois buffers e count, e também não pode efetivamente inferir a existência de uma relação. A SAL poderia fornecer mais clareza sobre as propriedades e a implementação da função, como mostrado aqui:

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

Observe que essas anotações se assemelham às informações da documentação, mas são mais concisas e seguem um padrão semântico. Ao ler esse código, você pode entender rapidamente as propriedades dessa função e como evitar problemas de segurança de saturação de buffer. Melhor ainda, os padrões semânticos que o SAL fornece podem melhorar a eficiência e a eficácia das ferramentas automatizadas de análise de código na descoberta precoce de possíveis bugs. Imagine que alguém escreve esta implementação buggy de 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;
}

Esta implementação contém um erro comum off-by-one. Felizmente, o autor do código incluiu a anotação de tamanho do buffer SAL — uma ferramenta de análise de código poderia detetar o bug analisando apenas essa função.

Noções básicas de SAL

A SAL define quatro tipos básicos de parâmetros, que são categorizados por padrão de uso.

Categoria Anotação de parâmetros Descrição
Entrada para a função chamada _In_ Os dados são passados para a função chamada e são tratados como somente leitura.
Entrada para a função chamada e saída para o chamador _Inout_ Os dados utilizáveis são passados para a função e potencialmente são modificados.
Resultado para quem liga _Out_ O chamador fornece apenas espaço para a função chamada gravar. A função chamada grava dados nesse espaço.
Saída do ponteiro para o chamador _Outptr_ Como Saída para chamador. O valor retornado pela função chamada é um ponteiro.

Estas quatro anotações básicas podem ser tornadas mais explícitas de várias maneiras. Por padrão, os parâmetros de ponteiro anotados são considerados necessários — eles devem ser não-NULL para que a função seja bem-sucedida. A variação mais comumente usada das anotações básicas indica que um parâmetro de ponteiro é opcional — se for NULL, a função ainda pode ter sucesso em fazer seu trabalho.

Esta tabela mostra como distinguir entre parâmetros obrigatórios e opcionais:

Os parâmetros são necessários Os parâmetros são opcionais
Entrada para a função chamada _In_ _In_opt_
Entrada para a função chamada e saída para o chamador _Inout_ _Inout_opt_
Resultado para quem liga _Out_ _Out_opt_
Saída do ponteiro para o chamador _Outptr_ _Outptr_opt_

Essas anotações ajudam a identificar possíveis valores não inicializados e usos inválidos de ponteiro nulo de maneira formal e precisa. Passar NULL para um parâmetro necessário pode causar uma falha ou pode fazer com que um código de erro "falha" seja retornado. De qualquer forma, a função não consegue fazer o seu trabalho.

Exemplos de SAL

Esta seção mostra exemplos de código para as anotações básicas de SAL.

Usando a ferramenta de análise de código do Visual Studio para localizar defeitos

Nos exemplos, a ferramenta de análise de código do Visual Studio é usada junto com anotações SAL para localizar defeitos de código. Veja como fazer isso.

Para usar ferramentas de análise de código do Visual Studio e SAL

  1. No Visual Studio, abra um projeto C++ que contém anotações SAL.

  2. Na barra de menus, escolha Compilar, Executar análise de código na solução.

    Considere o exemplo _In_ nesta seção. Se você executar a análise de código nele, este aviso será exibido:

    C6387 Valor de parâmetro inválido 'pInt' pode ser '0': isso não segue a especificação para a função 'InCallee'.

Exemplo: A anotação _in_

A _In_ anotação indica que:

  • O parâmetro deve ser válido e não será modificado.

  • A função só será lida a partir do buffer de elemento único.

  • O chamador deve fornecer o buffer e inicializá-lo.

  • _In_ especifica "somente leitura". Um erro comum é aplicar _In_ a um parâmetro que deveria ter a _Inout_ anotação.

  • _In_ é permitido, mas ignorado pelo analisador em escalares que não são de ponteiro.

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 utilizar a análise de código do Visual Studio neste exemplo, ele validará que os chamadores fornecem um ponteiro não-nulo para um buffer inicializado para pInt. Nesse caso, pInt o ponteiro não pode ser NULL.

Exemplo: A anotação _In_opt_

_In_opt_ é o mesmo _In_que , exceto que o parâmetro de entrada pode ser NULL e, portanto, a função deve verificar isso.

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

A análise de código do Visual Studio valida que a função verifica se há NULL antes de acessar o buffer.

Exemplo: A anotação _out_

_Out_ suporta um cenário comum no qual um ponteiro não-NULL que aponta para um buffer de elemento é passado e a função inicializa o elemento. O chamador não precisa inicializar o buffer antes da chamada; A função chamada promete inicializá-lo antes que ele retorne.

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

A análise de código do Visual Studio valida que o chamador passa um ponteiro não-nulo para um buffer para pInt e que o buffer é inicializado pela função antes de retornar.

Exemplo: A anotação _out_opt_

_Out_opt_ é o mesmo _Out_que , exceto que o parâmetro pode ser NULL e, portanto, a função deve verificar isso.

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

A análise de código do Visual Studio valida que essa função verifica se há NULL antes pInt de ser desreferenciada e, se pInt não for NULL, que o buffer é inicializado pela função antes de retornar.

Exemplo: A anotação _Inout_

_Inout_ é usado para anotar um parâmetro de ponteiro que pode ser alterado pela função. O ponteiro deve apontar para dados inicializados válidos antes da chamada e, mesmo que seja alterado, ainda deve ter um valor válido no retorno. A anotação especifica que a função pode ler e gravar livremente no buffer de um elemento. O chamador deve fornecer o buffer e inicializá-lo.

Observação

Como _Out_, _Inout_ deve aplicar-se a um valor modificável.

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
}

A análise de código do Visual Studio valida que os chamadores passam um ponteiro não-NULL para um buffer inicializado para pInt, e que, antes de retornar, pInt ainda é não-NULL e o buffer é inicializado.

Exemplo: A anotação _Inout_opt_

_Inout_opt_ é o mesmo _Inout_que , exceto que o parâmetro de entrada pode ser NULL e, portanto, a função deve verificar isso.

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

A análise de código do Visual Studio valida que essa função verifica se há NULL antes de acessar o buffer e, se pInt não for NULL, que o buffer é inicializado pela função antes de retornar.

Exemplo: A anotação _outptr_

_Outptr_ é utilizado para anotar um parâmetro que se destina a retornar um ponteiro. O parâmetro em si não deve ser NULL, e a função chamada retorna um ponteiro não-NULL nele e esse ponteiro aponta para dados inicializados.

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

A análise de código do Visual Studio valida que o chamador passa um ponteiro não nulo para *pInt, e que o buffer é inicializado pela função antes de retornar.

Exemplo: A anotação _Outptr_opt_

_Outptr_opt_ é o mesmo que _Outptr_, exceto que o parâmetro é opcional — pode ser passado um ponteiro NULL para o parâmetro.

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

A análise de código do Visual Studio valida que essa função verifica se há NULL antes *pInt de ser desreferenciada e que o buffer é inicializado pela função antes de retornar.

Exemplo: A anotação _Success_ em combinação com _out_

As anotações podem ser aplicadas à maioria dos objetos. Em particular, você pode anotar uma função inteira. Uma das características mais óbvias de uma função é que ela pode ter sucesso ou falhar. Mas, como a associação entre um buffer e seu tamanho, C/C++ não pode expressar sucesso ou falha da função. Usando a _Success_ anotação, pode-se dizer como é o sucesso de uma função. O parâmetro para a _Success_ anotação é apenas uma expressão que, quando é verdadeira, indica que a função foi bem-sucedida. A expressão pode ser qualquer coisa que o analisador de anotações possa manipular. Os efeitos das anotações após o retorno da função só são aplicáveis quando a função é bem-sucedida. Este exemplo mostra como _Success_ interage para _Out_ fazer a coisa certa. Você pode usar a palavra-chave return para representar o valor de retorno.

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

A anotação _Out_ faz com que a análise de código do Visual Studio valide que o chamador deve passar um ponteiro não-NULL para um buffer em pInt e que a função inicialize esse buffer antes de retornar.

Melhores Práticas SAL

Adicionando anotações ao código existente

A SAL é uma tecnologia poderosa que pode ajudá-lo a melhorar a segurança e a confiabilidade do seu código. Depois de aprender SAL, você pode aplicar a nova habilidade ao seu trabalho diário. No novo código, você pode usar especificações baseadas em SAL desde o início; no código mais antigo, pode adicionar anotações incrementalmente e aumentar os benefícios sempre que atualizar.

Os cabeçalhos públicos da Microsoft já estão anotados. Portanto, sugerimos que em seus projetos você primeiro anote funções de nó folha e funções que chamam APIs do Win32 para obter o máximo benefício.

Quando faço anotações?

Aqui estão algumas diretrizes:

  • Anote todos os parâmetros do ponteiro.

  • Anote anotações de intervalo de valores para que a Análise de Código possa garantir a segurança do buffer e do ponteiro.

  • Anote as regras de bloqueio e os efeitos colaterais de bloqueio. Para obter mais informações, consulte Anotação do Comportamento de Bloqueio.

  • Anote as propriedades do driver e outras propriedades específicas do domínio.

Ou você pode anotar todos os parâmetros para deixar sua intenção clara e facilitar a verificação de que as anotações foram feitas.

Ver também