Поделиться через


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

Язык заметки исходного кода Майкрософт (SAL) предоставляет набор заметок, которые можно использовать для описания того, как функция использует свои параметры, предположения, которые она делает о них, и гарантии того, что она делает после завершения. Заметки определяются в файле <sal.h>заголовка. Анализ кода Visual Studio для C++ использует заметки SAL для изменения его анализа функций. Дополнительные сведения о разработке драйверов ДЛЯ Windows SAL 2.0 см . в заметках 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 определяет четыре основных типа параметров, которые классифицируются по шаблону использования.

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

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

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

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

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

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

Пример: заметка _Success_ в сочетании с _Out_

Заметки можно применять к большинству объектов. В частности, можно анонимировать всю функцию. Одна из самых очевидных характеристик функции заключается в том, что она может завершиться успешной или неудачной. Но, как и связь между буфером и его размером, C/C++ не может выразить успех или сбой функции. Используя заметку _Success_ , вы можете сказать, как выглядит успешно для функции. Параметр заметки _Success_ — это просто выражение, указывающее, что функция успешно выполнена. Выражение может быть любым, что может обрабатывать средство синтаксического анализа заметок. Эффекты заметок после возврата функции применимы только при успешном выполнении функции. В этом примере показано, как _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_ приводит к проверке того, что вызывающий объект передает указатель, отличный от NULL, в буфер pIntи что буфер инициализируется функцией перед возвратом.

Рекомендации ПО SAL

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

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

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

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

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

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

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

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

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

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

См. также