Introducción a SAL

El lenguaje de anotación de código fuente (SAL) de Microsoft proporciona un conjunto de anotaciones que puede usar para describir cómo una función usa sus parámetros, las suposiciones que realiza sobre ellos y las garantías que realiza cuando finaliza. Las anotaciones se definen en el archivo de encabezado <sal.h> . Visual Studio análisis de código para C++ usa anotaciones SAL para modificar su análisis de funciones. Para obtener más información sobre SAL 2.0 para Windows desarrollo de controladores, vea Anotaciones de SAL 2.0 para Windows controladores.

De forma nativa, C y C++ proporcionan solo formas limitadas para que los desarrolladores expresen de forma coherente la intención y la invariable. Mediante el uso de anotaciones SAL, puede describir las funciones con más detalle para que los desarrolladores que las consumen puedan comprender mejor cómo usarlas.

¿Qué es SAL y por qué se debe usar?

En pocas palabras, SAL es una manera económica de permitir que el compilador compruebe el código por usted.

SAL hace que el código sea más valioso

SAL puede ayudarle a hacer que el diseño de código sea más comprensible, tanto para humanos como para herramientas de análisis de código. Considere este ejemplo que muestra la función en tiempo de ejecución de C memcpy :


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

¿Puede saber qué hace esta función? Cuando se implementa o llama a una función, se deben mantener determinadas propiedades para garantizar la corrección del programa. Con solo mirar una declaración como la del ejemplo, no se sabe cuáles son. Sin anotaciones SAL, tendría que basarse en documentación o comentarios de código. Esto es lo que indica la memcpy documentación de :

" memcpy copia memcpy bytes de recuento de src a dest; copias cuentan caracteres anchos (dos bytes). Si el origen y el destino se superponen, el comportamiento de memcpy no está definido. Use memmove para controlar las áreas superpuestas.
Importante: Asegúrese de que el búfer de destino tiene el mismo tamaño o mayor que el búfer de origen. Para obtener más información, vea Evitar saturaciones de búfer."

La documentación contiene un par de bits de información que sugieren que el código tiene que mantener determinadas propiedades para garantizar la corrección del programa:

  • memcpy copia el count de bytes del búfer de origen en el búfer de destino.

  • El búfer de destino debe ser al menos tan grande como el búfer de origen.

Sin embargo, el compilador no puede leer la documentación ni los comentarios informales. No sabe que hay una relación entre los dos búferes y , y tampoco puede adivinar una count relación de forma eficaz. SAL podría proporcionar más claridad sobre las propiedades y la implementación de la función, como se muestra aquí:


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

Tenga en cuenta que estas anotaciones se parecen a la información de la documentación, pero son más concisas y siguen un patrón semántico. Al leer este código, puede comprender rápidamente las propiedades de esta función y cómo evitar problemas de seguridad de saturación del búfer. Aún mejor, los patrones semánticos que proporciona SAL pueden mejorar la eficacia y la eficacia de las herramientas de análisis de código automatizadas en la detección temprana de posibles errores. Imagine que alguien escribe esta implementación de error 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 implementación contiene un error común desactivado por uno. Afortunadamente, el autor del código incluía la anotación de tamaño de búfer SAL: una herramienta de análisis de código podría detectar el error mediante el análisis de esta función por sí sola.

Aspectos básicos de SAL

SAL define cuatro tipos básicos de parámetros, que se clasifican por patrón de uso.

Category Anotación de parámetros Descripción
Entrada a la función a la que se ha llamado _In_ Los datos se pasan a la función llamada y se tratan como de solo lectura.
Entrada a función llamada y salida al autor de la llamada _Inout_ Los datos utilizables se pasan a la función y se pueden modificar.
Salida al autor de la llamada _Out_ El autor de la llamada solo proporciona espacio en el que escribir la función llamada. La función llamada escribe datos en ese espacio.
Salida del puntero al autor de la llamada _Outptr_ Como Salida al autor de la llamada. El valor devuelto por la función llamada es un puntero.

Estas cuatro anotaciones básicas se pueden hacer más explícitas de varias maneras. De forma predeterminada, se supone que los parámetros de puntero anotados son obligatorios; deben ser distintos de NULL para que la función se haga correctamente. La variación más usada de las anotaciones básicas indica que un parámetro de puntero es opcional; si es NULL, la función puede seguir realizando correctamente su trabajo.

En esta tabla se muestra cómo distinguir entre los parámetros obligatorios y opcionales:

Se requieren parámetros Los parámetros son opcionales
Entrada a la función a la que se ha llamado _In_ _In_opt_
Entrada a función llamada y salida al autor de la llamada _Inout_ _Inout_opt_
Salida al autor de la llamada _Out_ _Out_opt_
Salida del puntero al autor de la llamada _Outptr_ _Outptr_opt_

Estas anotaciones ayudan a identificar posibles valores no inicializados y usos de puntero nulo no válidos de una manera formal y precisa. Si se pasa NULL a un parámetro necesario, puede producirse un bloqueo o puede provocar que se devuelva un código de error "con error". En cualquier caso, la función no puede realizar correctamente su trabajo.

Ejemplos de SAL

En esta sección se muestran ejemplos de código para las anotaciones SAL básicas.

Usar la herramienta Visual Studio Code analysis para buscar defectos

En los ejemplos, la herramienta Visual Studio Code Analysis se usa junto con anotaciones SAL para buscar defectos de código. Aquí te mostramos cómo hacerlo.

Para usar Visual Studio de análisis de código y SAL

  1. En Visual Studio, abra un proyecto de C++ que contenga anotaciones SAL.

  2. En la barra de menús, elija Compilar, Ejecutar Code Analysis en la solución.

    Considere el ejemplo _In_ de esta sección. Si ejecuta análisis de código en él, se muestra esta advertencia:

    C6387 Valor de parámetro no válido 'pInt' podría ser '0': no cumple la especificación de la función 'InCallee'.

Ejemplo: anotación _In_

La _In_ anotación indica que:

  • El parámetro debe ser válido y no se modificará.

  • La función solo leerá desde el búfer de un solo elemento.

  • El autor de la llamada debe proporcionar el búfer e inicializarse.

  • _In_ especifica "solo lectura". Un error común es aplicar _In_ a un parámetro que debe tener la _Inout_ anotación en su lugar.

  • _In_ se permite, pero el analizador lo omite en escalares que no son punteros.

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
}

Si usa Visual Studio Code Analysis en este ejemplo, valida que los llamadores pasen un puntero distinto de NULL a un búfer inicializado para pInt . En este caso, pInt el puntero no puede ser NULL.

Ejemplo: La anotación _In_opt_ de datos

_In_opt_ es igual que , salvo que el parámetro de entrada puede ser NULL y, por lo tanto, la función _In_ debe comprobarlo.


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

Visual Studio Code Analysis valida que la función comprueba si es NULL antes de acceder al búfer.

Ejemplo: anotación _Out_

_Out_ admite un escenario común en el que se pasa un puntero que no es NULL que apunta a un búfer de elementos y la función inicializa el elemento. El autor de la llamada no tiene que inicializar el búfer antes de la llamada; la función llamada se compromete a inicializar antes de que vuelva.

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 valida que el autor de la llamada pasa un puntero no NULL a un búfer para y que la función inicializa el búfer antes pInt de que vuelva.

Ejemplo: La anotación _Out_opt_ de datos

_Out_opt_ es igual que , salvo que el parámetro puede ser NULL y, por lo _Out_ tanto, la función debe comprobarlo.

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

Visual Studio Code Analysis valida que esta función comprueba null antes de que se desreferencia y, si no es NULL, que la función inicialice el búfer antes de pIntpInt que vuelva.

Ejemplo: anotación _Inout_

_Inout_ se usa para anotar un parámetro de puntero que la función puede cambiar. El puntero debe apuntar a datos inicializados válidos antes de la llamada e incluso si cambia, debe seguir teniendo un valor válido en la devolución. La anotación especifica que la función puede leer y escribir libremente en el búfer de un solo elemento. El autor de la llamada debe proporcionar el búfer e inicializarse.

Nota

Al _Out_ igual que , debe aplicarse a un valor _Inout_ modificable.

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
}

Visual Studio Code Analysis valida que los autores de la llamada pasen un puntero distinto de NULL a un búfer inicializado para y que, antes de la devolución, sigue siendo distinto de NULL y se inicializa el pIntpInt búfer.

Ejemplo: La anotación _Inout_opt_ de datos

_Inout_opt_ es igual que , salvo que el parámetro de entrada puede ser NULL y, por lo _Inout_ tanto, la función debe comprobarlo.

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

Visual Studio Code Analysis valida que esta función comprueba null antes de acceder al búfer y, si no es NULL, que la función inicializa el búfer antes de que pInt vuelva.

Ejemplo: anotación _Outptr_

_Outptr_ se usa para anotar un parámetro que está pensado para devolver un puntero. El propio parámetro no debe ser NULL y la función llamada devuelve un puntero distinto de NULL en él y ese puntero apunta a los datos 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);
}

Visual Studio Code Analysis valida que el autor de la llamada pasa un puntero que no es NULL para y que la función inicializa el búfer antes *pInt de que vuelva.

Ejemplo: La anotación _Outptr_opt_ de datos

_Outptr_opt_ es igual que , salvo que el parámetro es opcional: el autor de la llamada _Outptr_ puede pasar un puntero NULL para el 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);
}

Visual Studio Code Analysis valida que esta función comprueba null antes de que se desreferencia y que la función inicialice el búfer antes *pInt de que vuelva.

Ejemplo: anotación _Success_ en combinación con _Out_

Las anotaciones se pueden aplicar a la mayoría de los objetos. En concreto, puede anotar una función completa. Una de las características más obvias de una función es que puede tener éxito o producir un error. Pero al igual que la asociación entre un búfer y su tamaño, C/C++ no puede expresar el éxito o el error de la función. Mediante el uso _Success_ de la anotación, puede decir el aspecto correcto de una función. El parámetro de la _Success_ anotación es simplemente una expresión que, cuando es true, indica que la función se ha hecho correctamente. La expresión puede ser cualquier cosa que el analizador de anotaciones pueda controlar. Los efectos de las anotaciones después de la devolución de la función solo son aplicables cuando la función se realiza correctamente. En este ejemplo se muestra _Success_ cómo interactúa con para hacer lo _Out_ correcto. Puede usar la palabra clave return para representar el valor devuelto.

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

La anotación hace que Visual Studio Code Analysis valide que el autor de la llamada pasa un puntero no NULL a un búfer para y que la función inicializa el búfer antes de _Out_pInt que vuelva.

Procedimiento recomendado de SAL

Agregar anotaciones al código existente

SAL es una tecnología eficaz que puede ayudarle a mejorar la seguridad y confiabilidad del código. Después de aprender SAL, puede aplicar la nueva aptitud a su trabajo diario. En el nuevo código, puede usar especificaciones basadas en SAL por diseño en todo el proceso; en código anterior, puede agregar anotaciones de forma incremental y, por tanto, aumentar las ventajas cada vez que actualice.

Los encabezados públicos de Microsoft ya están anotados. Por lo tanto, se recomienda que en los proyectos anote primero las funciones de nodo hoja y las funciones que llaman a las API de Win32 para sacar el máximo partido.

¿Cuándo anoto?

Estas son algunas directrices:

  • Anotar todos los parámetros de puntero.

  • Anote anotaciones de intervalo de valores para que Code Analysis seguridad de búfer y puntero.

  • Anotar reglas de bloqueo y efectos secundarios de bloqueo. Para obtener más información, vea Anotar el comportamiento de bloqueo.

  • Anotar las propiedades del controlador y otras propiedades específicas del dominio.

O bien, puede anotar todos los parámetros para que su intención sea clara en todo y para que sea fácil comprobar que se han realizado anotaciones.

Vea también