Noções básicas sobre SAL
A linguagem de anotação de origem da Microsoft (SAL) fornece um conjunto de anotações que você pode usar para descrever como uma função usa seus parâmetros, das suposições que faz sobre eles, e as garantias que faz quando completa.As anotações são definidas no arquivo de cabeçalho <sal.h>.A análise de código Visual Studio para C++ usa anotações de SAL para alterar a análise de funções.Para obter mais informações sobre o SAL 2,0 para o desenvolvimento de driver do Windows, consulte SAL 2,0 anotações para drivers do Windows.
Nativamente, C e C++ fornecem maneiras para desenvolvedores apenas limitadas a intenção e a invariância consistentemente express.Usando anotações de SAL, você pode descrever as funções em detalhes de modo que os desenvolvedores que estão as consumindo possam entender como utilizá-los.
Que são SAL e porque você deve usar o?
Simplesmente indicado, o SAL é uma maneira barata de permitir que o compilador verificar seu código para você.
O SAL faz o código mais valioso
O SAL pode ajudar a tornar seu código mais legível de design, para seres humanos para e ferramentas de análise de código.Considere esse exemplo que mostra a função memcpyde tempo de execução de C:
void * memcpy(
void *dest,
const void *src,
size_t count
);
Você pode instruir o que essa função torna?Quando uma função é implementada ou chamada, certas propriedades devem ser mantidas para garantir a exatidão de programa.Apenas olhando uma declaração como no exemplo, você não souber o que são.Sem anotações de SAL, você teria que confiar na documentação ou codificar comentários.Aqui é o que a documentação do MSDN para memcpy com:
“De bytes de contagem cópias de src ao dest.Se a fonte e o alvo sobrepostos, o comportamento de memcpy é indefinido.Use o memmove para manipular sobrepostos regiões.Nota de segurança: certifique-se de que o buffer de destino é o mesmo tamanho ou maior que o buffer de origem.Para obter mais informações, consulte a evitação de estouros de buffer.”
A documentação conterá um par bit de informações que sugerem que o código tem que manter certas propriedades para garantir a exatidão de programa:
memcpy copia count de bytes de buffer de origem no 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.Não sabe que há uma relação entre os dois buffers e count, e também não pode efetivamente descobrir sobre um relacionamento.O SAL pode fornecer mais clareza sobre as propriedades e implementação de função, conforme 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 para informações na documentação do MSDN, mas eles são mais concisas e seguem um padrão semântico.Quando você lê esse código, você pode rapidamente entender as propriedades dessa função e como evitar problemas de segurança de estouro de buffer.Aumenta mesmo, padrões de semântica que o SAL fornece pode melhorar a eficiência e a eficácia de ferramentas de teste automatizadas de código em descoberta inicial de erros potenciais.Imagine que alguém grava essa implementação com erros 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;
}
Essa implementação contém um aspecto comum fora- - por um erro.Felizmente, o autor do código incluiu a ferramenta de análise de código de anotação - um do tamanho do buffer de SAL pode capturar o erro para essa função apenas.
Noções básicas de SAL
O SAL define quatro tipos básicos de parâmetros, que são categorizados por padrão de uso.
Categoria |
Anotação de parâmetro |
Descrição |
---|---|---|
Entrada para a função chamada |
_In_ |
Os dados são passados para a função chamada, e tratados como somente leitura. |
Entrada para a função chamada, e a saída para o chamador |
_Inout_ |
Os dados são passados úteis na função e possivelmente alterados. |
Saída para o chamador |
_Out_ |
O chamador fornece somente espaço para a função chamada para gravar.A função chamada grava dados naquele espaço. |
A saída do ponteiro para o chamador |
_Outptr_ |
Como Output to caller.O valor retornado pela função é chamado um ponteiro. |
Essas quatro anotações básicas podem ser feitas mais explícitas de várias maneiras.Por padrão, o ponteiro anotado que os parâmetros são considerados para ser necessária eles devem ser não-nulo para que a função foi bem-sucedida.A variação a mais usados de anotações básicas que um ponteiro indica que o parâmetro é opcional- se é NULO, a função pode ainda terá êxito em tornar seu trabalho.
Esta tabela mostra como distinguir entre os parâmetros necessá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 a saída para o chamador |
_Inout_ |
_Inout_opt_ |
Saída para o chamador |
_Out_ |
_Out_opt_ |
A saída do ponteiro para o chamador |
_Outptr_ |
_Outptr_opt_ |
Essas anotações ajudam a identificar valores possíveis não inicializado e usa inválidos do ponteiro zero em uma maneira tipo e exata.Passar NULL para um parâmetro necessário pode causar uma falha, ou pode causar “falhou” o código de erro a ser retornado.De qualquer forma, a função não obterá êxito em fazer seus trabalhos.
Exemplos de SAL
Esta seção mostra exemplos de código para anotações básicas de SAL.
Usando a ferramenta de análise de código Visual Studio para localizar defeitos
Nos exemplos, a ferramenta de análise de código Visual Studio é usada junto com anotações de SAL para localizar defeitos de código.É aqui como fazer isso.
Para usar o Visual Studio código ferramentas de análise e SAL
No Visual Studio, projeto aberto de c++ que contém anotações de SAL.
Na barra de menu, escolha Compilar, Análise de código em execução no solution.
Considere o exemplo de _In_ nesta seção.Se você executa análise de código nele, esse aviso é exibido:
Valor do parâmetro C6387 inválidoa “pinta” pode ser “0 ": isso não está de acordo com a especificação para a função “InCallee”.
Exemplo: A anotação de _In_
Nota de _In_ indica que:
O parâmetro deve ser válido e não será alterado.
A função lê apenas o tamanho de um único elemento.
O chamador deve fornecer o buffer e inicializá-la.
_In_ especifica somente leitura “”.Um erro comum é _In_ aplicar a um parâmetro que deve ter a anotação de _Inout_ em vez disso.
_In_ é permitida mas ignorado pelo analisador em escalares de não 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 você usar a análise de código Visual Studio nesse exemplo, que valida os chamadores passam um ponteiro não-nulo a um buffer inicializado para pInt.Nesse caso, o ponteiro de pInt não pode ser NULO.
Exemplo: A anotação de _In_opt_
_In_opt_ é o mesmo que _In_, exceto que o parâmetro de entrada é permitido ser NULO e, como consequência, 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 Visual Studio valida que a verificação de função para NULL antes que acessa o buffer.
Exemplo: A anotação de _Out_
_Out_ suporta um cenário comum em que um ponteiro não-nulo que aponta para um buffer do 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á-la antes que retorna.
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 ferramenta de análise de código Visual Studio que valida o chamador passa um ponteiro não-nulo a um buffer para pInt e que o buffer seja inicializado pela função antes que retorna.
Exemplo: A anotação de _Out_opt_
_Out_opt_ é o mesmo que _Out_, exceto que o parâmetro é permitido ser NULO e, como consequência, 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 Visual Studio que valida verificações dessa função para NULL antes que pInt está desreferenciado, e se pInt não é NULO, que o buffer estiver inicializado pela função antes que retorna.
Exemplo: A anotação de _Inout_
_Inout_ é usado para fazer anotações um parâmetro do ponteiro que pode ser modificado pela função.O ponteiro deve apontar para dados inicializados válidos antes da chamada, e mesmo se for alterado, ainda deve ter um valor válido em retorno.A anotação especifica que a função pode livremente leitura e gravação no buffer de um elemento.O chamador deve fornecer o buffer e inicializá-la.
Observação |
---|
Como _Out_, _Inout_ deve aplicar para 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 Visual Studio que valida os chamadores passam um ponteiro não-nulo a um buffer inicializado para pInt, e que, antes de retorno, pInt ainda é não-nulo e o buffer estiver inicializado.
Exemplo: A anotação de _Inout_opt_
_Inout_opt_ é o mesmo que _Inout_, exceto que o parâmetro de entrada é permitido ser NULO e, como consequência, 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 Visual Studio que valida verificações dessa função para NULL antes que acessa o buffer, e se pInt não é NULO, que o buffer estiver inicializado pela função antes que retorna.
Exemplo: A anotação de _Outptr_
_Outptr_ é usado para fazer anotações um parâmetro que se destina retornar um ponteiro.O parâmetro próprio não deve ser NULO, e retorna chamados um ponteiro de função não-nulo nele e pontos desse ponteiro para os 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 Visual Studio que valida o chamador passa um ponteiro para *pIntnão-nulo, e que o buffer seja inicializado pela função antes que retorna.
Exemplo: A anotação de _Outptr_opt_
_Outptr_opt_ é o mesmo que _Outptr_, exceto que o parâmetro é opcional chamador - pode passar em um ponteiro NULO 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 Visual Studio que valida verificações dessa função para NULL antes que *pInt está desreferenciado, e que o buffer estiver inicializado pela função antes que retorna.
Exemplo: A anotação de _Success_ em combinação com o _Out_
As anotações podem ser aplicadas à maioria dos objetos.Em particular, você poderá anotar uma função inteira.Uma das características mais óbvios de uma função é que pode terá êxito ou falhar.Mas como a associação entre um buffer e seu tamanho, C/C++ não pode expressar sucesso ou falha de função.Usando a anotação de _Success_ , você pode determinar que êxito para uma função parece.O parâmetro a anotação de _Success_ é apenas uma expressão que quando é verdadeiro indica que a função foi bem-sucedida.A expressão pode ser qualquer coisa que o analisador de anotação pode manipular.Os efeitos de anotações após os retornos de função são aplicáveis somente quando a função terá êxito.Este exemplo mostra como _Success_ interage com _Out_ para 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 análise de código Visual Studio das causas de anotação de _Out_ a validar que o chamador passa um ponteiro não-nulo a um buffer para pInt, e que o buffer seja inicializado pela função antes de retornar.
Recomendação de SAL
Adicionando um ao código existente
O SAL é uma tecnologia poderosa que pode ajudar a melhorar a segurança e confiabilidade do código.Depois que você sabe o SAL, você pode aplicar a nova habilidade para seu trabalho journal.No novo código, você pode usar com base SAL- especificações de design para qualquer lugar; em um código mais antigo, você pode adicionar anotações incremental e assim lançar os benefícios cada vez que você atualizar.
Os cabeçalhos públicos são mais detalhados da Microsoft.Como consequência, sugerimos em seus projetos que você anote primeiro as funções do nó folha e funções que chama APIs do Win32 para obter o máximo benefício de.
Quando eu anoto?
Aqui estão algumas diretrizes:
Anote todos os parâmetros do ponteiro.
Anote um intervalo de valores de modo que a análise de código pode garantir segurança do buffer e do ponteiro.
Anote regras de bloqueio e efeitos colaterais de bloqueio.Para obter mais informações, consulte Anotando o comportamento de bloqueio.
Anote propriedades de driver e outras propriedades específicas do domínio.
Ou você poderá anotar todos os parâmetros para tornar seu espaço livre da intenção em todo e para facilitar a verificar se as anotações são feitas.
Recursos relacionados
Blog da equipe de análise de código
Consulte também
Referência
Anotando parâmetros de função e valores de retorno
Anotando o comportamento da função
Anotando o comportamento de bloqueio
Especificando quando e onde uma anotação se aplica
Práticas recomendadas e exemplos (SAL)
Outros recursos
Usando o SAL anotações para reduzir os defeitos no código C/C++