Основные сведения о языке SAL

Язык аннотирования Microsoft Source-Code (SAL) предоставляет набор заметок, которые можно использовать для описания того, как функция использует ее параметры, предположения, что она делает, и гарантирует, что она делает это после завершения. Заметки определяются в файле <sal.h> заголовка. Visual Studio анализ кода для C++ использует аннотации SAL для изменения его анализа функций. дополнительные сведения о языке sal 2,0 для разработки драйверов Windows см. в разделе примечания sal 2,0 для Windows драйверов.

Изначально C и C++ предоставляют разработчикам только ограниченные способы согласованного определения намерений и расхождений. С помощью аннотаций SAL можно более подробно описать функции, чтобы разработчики, использующие их, могли лучше понять, как их использовать.

Что такое SAL и зачем его использовать?

Говоря просто, SAL является недорогым способом позволить компилятору проверить ваш код.

SAL делает код более ценным

SAL поможет сделать разработку кода более понятной, как для людей, так и для средств анализа кода. Рассмотрим этот пример, в котором показана функция memcpy времени выполнения C:

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

Можно ли узнать, что делает эта функция? При реализации или вызове функции необходимо поддерживать определенные свойства, чтобы обеспечить правильность программы. Просто взглянув на объявление, такое как в примере, вы не понимаете, что они представляют. Без аннотаций SAL вам пришлось полагаться на документацию или комментарии к коду. Вот как выглядит документация memcpy :

" memcpy копирует число байтов из src в dest; wmemcpyчисло копий расширенных символов (2 байта). При перекрытии исходного и конечного буферов поведение memcpy не определено. Используйте memmove для обработки перекрывающихся областей.
Важно. Убедитесь, что целевой буфер имеет тот же размер или больше, чем исходный буфер. Дополнительные сведения см. в разделе Предотвращение переполнения буфера.

В документации содержится несколько данных, которые предполагают, что код должен поддерживать определенные свойства для обеспечения правильности программы:

  • memcpycountкопирует байты из исходного буфера в целевой буфер.

  • Конечный буфер должен быть не меньше размера исходного буфера.

Однако компилятор не может прочитать документацию или неформальные комментарии. Это не знает, что существует связь между двумя буферами и count , а также не может эффективно догадаться о связи. SAL может предоставить более наглядные сведения о свойствах и реализации функции, как показано ниже:

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

Обратите внимание, что эти заметки похожи на сведения в документации, но они более лаконичны и следуют семантическому шаблону. При чтении этого кода можно быстро понять свойства этой функции и избежать проблем безопасности переполнения буфера. Более того, Семантические шаблоны, предоставляемые SAL, могут повысить эффективность и эффективность автоматизированных средств анализа кода при раннем обнаружении потенциальных ошибок. Imagine, что кто-то пишет эту реализацию 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;
}

Эта реализация содержит общее сообщение об ошибке. К счастью, автор кода включил аннотацию размера буфера SAL — средство анализа кода может перехватить ошибку, анализируя только эту функцию.

Основы SAL

SAL определяет четыре основных типа параметров, которые классифицируются по шаблону использования.

Категория Аннотация параметра Описание
Входные данные для вызываемой функции _In_ Данные передаются в вызываемую функцию и обрабатываются как доступная только для чтения.
Вход в вызываемую функцию и вывод в вызывающий объект _Inout_ Пригодные для использования данные передаются в функцию и потенциально изменяются.
Вывод в вызывающий объект _Out_ Вызывающий объект предоставляет только пространство для вызываемой функции для записи. Вызываемая функция записывает данные в это пространство.
Вывод указателя на вызывающий объект _Outptr_ Как и выходные данные в вызывающий объект. Значение, возвращаемое вызываемой функцией, является указателем.

Эти четыре основные аннотации можно сделать более явными различными способами. По умолчанию предполагается, что параметры указателей с заметками являются обязательными — они не должны иметь значение NULL, чтобы функция была выполнена. Наиболее часто используемый вариант основных заметок указывает на то, что параметр указателя является необязательным — если он равен NULL, функция может продолжать работать.

В этой таблице показано, как различать обязательные и необязательные параметры.

Параметры обязательны Параметры являются необязательными
Входные данные для вызываемой функции _In_ _In_opt_
Вход в вызываемую функцию и вывод в вызывающий объект _Inout_ _Inout_opt_
Вывод в вызывающий объект _Out_ _Out_opt_
Вывод указателя на вызывающий объект _Outptr_ _Outptr_opt_

Эти аннотации помогают определить возможные неинициализированные значения и недопустимые использование пустых указателей в формальном и точном порядке. Передача значения NULL в обязательный параметр может вызвать сбой или привести к возвращению кода ошибки "сбой". В любом случае функция не может быть выполнена в своей работе.

Примеры SAL

В этом разделе приведены примеры кода для базовых аннотаций SAL.

использование средства анализа Visual Studio Code для поиска дефектов

в примерах средство анализа Visual Studio Code используется вместе с аннотациями SAL для поиска дефектов кода. Вот как это сделать.

использование средств анализа кода Visual Studio и SAL

  1. в Visual Studio откройте проект C++, содержащий аннотации SAL.

  2. в строке меню выберите сборка, запустите Code Analysis для решения.

    Рассмотрим пример _In_ в этом разделе. При выполнении анализа кода на нем отображается следующее предупреждение:

    C6387 недопустимое значение параметра "команду" может быть "0": это не соответствует спецификации функции "uncalled".

Пример. Аннотация _In_

_In_Заметка указывает на следующее:

  • Параметр должен быть допустимым и не будет изменяться.

  • Функция будет считывать только из одноэлементного буфера.

  • Вызывающий объект должен предоставить буфер и инициализировать его.

  • _In_ Указывает "только для чтения". Распространенной ошибкой является применение _In_ к параметру, который вместо этого должен иметь _Inout_ заметку.

  • _In_ параметр разрешен, но игнорируется анализатором для скаляров, не являющихся указателями.

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
}

при использовании Visual Studio Code анализа в этом примере проверяется, что вызывающие объекты передают указатель, отличный от Null, в инициализированный буфер для pInt . В этом случае pInt указатель не может иметь значение null.

Пример: _In_opt_ Аннотация

_In_opt_ тот же, что _In_ и, за исключением того, что входной параметр может иметь значение null, поэтому функция должна проверить это.

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ный анализ проверяет, что функция проверяет наличие значения NULL перед обращением к буферу.

Пример. Аннотация _Out_

_Out_ поддерживает общий сценарий, в котором передается указатель, не имеющий значения NULL, указывающий на буфер элемента, а функция инициализирует элемент. Вызывающему объекту не нужно инициализировать буфер перед вызовом; вызываемая функция обещает инициализировать ее перед возвратом.

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 проверит, что вызывающий объект передает указатель, отличный от NULL, в буфер для pInt и что буфер инициализируется функцией перед возвратом.

Пример: _Out_opt_ Аннотация

_Out_opt_ то же самое, что _Out_ и, за исключением того, что параметру может быть присвоено значение null, поэтому функция должна проверить это.

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ный анализ проверяет, что эта функция проверяет наличие значения NULL перед pInt разыменованием, и если pInt не имеет значение null, то буфер инициализируется функцией перед возвратом.

Пример. Аннотация _Inout_

_Inout_ используется для добавления примечания к параметру указателя, который может быть изменен функцией. Указатель должен указывать на допустимые инициализированные данные перед вызовом, и даже если он изменяется, он по-прежнему должен иметь допустимое значение для возврата. Заметка указывает, что функция может свободно считывать из буфера с одним элементом и записывать в него. Вызывающий объект должен предоставить буфер и инициализировать его.

Примечание

Like _Out_ , _Inout_ должен применяться к изменяемому значению.

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ный анализ проверяет, что вызывающие объекты передают указатель, отличный от null, в инициализированный буфер для pInt , и что перед возвратом pInt все еще имеет значение, отличное от null, и буфер инициализируется.

Пример: _Inout_opt_ Аннотация

_Inout_opt_ тот же, что _Inout_ и, за исключением того, что входной параметр может иметь значение null, поэтому функция должна проверить это.

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ный анализ проверяет, что эта функция проверяет наличие значения NULL перед обращением к буферу, и если pInt не имеет значение null, то буфер инициализируется функцией перед возвратом.

Пример. Аннотация _Outptr_

_Outptr_ используется для аннотации параметра, который должен возвращать указатель. Сам параметр не должен иметь значение NULL, а вызываемая функция возвращает в нее указатель, отличный от NULL, и этот указатель указывает на инициализированные данные.

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ный анализ проверяет, что вызывающий объект передает указатель, отличный от NULL, для *pInt , и что буфер инициализируется функцией перед возвратом.

Пример: _Outptr_opt_ Аннотация

_Outptr_opt_ то же самое, что _Outptr_ и, за исключением того, что параметр является необязательным — вызывающий объект может передать пустой указатель для параметра.

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ный анализ проверяет, что эта функция проверяет наличие значения NULL перед *pInt разыменованием, и что буфер инициализируется функцией перед возвратом.

Пример. Аннотация _Success_ в сочетании с _Out_

Заметки можно применять к большинству объектов. В частности, можно закомментировать всю функцию. Одной из самых очевидных характеристик функции является то, что она может завершиться успешно или неудачно. Но, как и в связи между буфером и его размером, C/C++ не может выражать успешную или неудачную работу функции. С помощью _Success_ аннотации можно сказать, как выглядит успешная функция. Параметром для _Success_ заметки является просто выражение, которое, если оно равно true, указывает, что функция выполнена успешно. Выражение может быть любым, которое может обрабатывать средство синтаксического анализа заметок. Эффекты заметок после возврата функции применимы только в случае, если функция выполнена. В этом примере показано, как _Success_ взаимодействует с _Out_ , чтобы выполнить нужное действие. Для представления возвращаемого значения можно использовать ключевое слово return .

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

_Out_заметка приводит к тому, что Visual Studio Codeный анализ проверит, что вызывающий объект передает указатель, отличный от NULL, в буфер для pInt и что буфер инициализируется функцией перед возвратом.

Рекомендации по SAL

Добавление заметок к существующему коду

SAL — это мощная технология, которая может помочь повысить безопасность и надежность кода. После изучения SAL можно применить новые навыки к повседневной работе. В новом коде спецификации на основе SAL можно использовать по принципу разработки. в старом коде можно добавлять заметки постепенно и таким образом повышать их преимущества при каждом обновлении.

У общедоступных заголовков Майкрософт уже есть аннотации. Поэтому мы рекомендуем использовать в своих проектах, что сначала закомментировать функции и функции конечных узлов, вызывающие API-интерфейсы Win32, чтобы получить наибольшее преимущество.

Когда делать аннотации?

Ниже приведены некоторые рекомендации.

  • Закомментировать все параметры указателя.

  • закомментировать заметки диапазона значений, чтобы Code Analysis могли обеспечить безопасность буфера и указателя.

  • Закомментировать правила блокировки и блокировать побочные эффекты. Дополнительные сведения см. в разделе Аннотирование режима блокировки.

  • Добавление заметка к свойствам драйвера и другим свойствам, зависящим от домена.

Кроме того, можно добавить заметки ко всем параметрам, чтобы сделать свою намерение понятной, чтобы упростить проверку выполнения заметок.

См. также: