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

Язык заметок исходного кода Майкрософт (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 копирует число расширенных символов (два байта). При перекрытии исходного и конечного буферов поведение memcpy не определено. Используйте memmove для обработки перекрывающихся областей.
Важно: Убедитесь, что целевой буфер имеет тот же размер или больше, что и исходный буфер. Дополнительные сведения см. в разделе Предотвращение переполнения буфера.

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

  • memcpy копирует count байты из исходного буфера в буфер назначения.

  • Целевой буфер должен быть по крайней мере таким же, как исходный буфер.

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

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

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

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

    C6387 Недопустимое значение параметра "pInt" может иметь значение "0": это не соответствует спецификации для функции "InCallee".

Пример. Заметка _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 Analysis проверяет, проверяет ли функция значение 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_ используется для заметок к параметру указателя, который может быть изменен функцией. Указатель должен указывать на допустимые инициализированные данные перед вызовом, и даже если они изменяются, он должен по-прежнему иметь допустимое значение при возврате. Заметка указывает, что функция может свободно считывать данные из одноэлейного буфера и записывать их в нее. Вызывающий объект должен предоставить буфер и инициализировать его.

Примечание

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

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

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

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

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

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

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

Когда делать заметки?

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

  • Добавить заметки ко всем параметрам указателя.

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

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

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

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

См. также раздел