Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
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 :
memcpycopia contagem bytes de src para dest;wmemcpycopia contagem caracteres largos (dois bytes). Se a origem e o destino se sobrepõem, o comportamento dememcpyé indefinido. Usememmovepara 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:
memcpycopiacountde 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
No Visual Studio, abra um projeto C++ que contém anotações SAL.
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.