Экспорт (0) Печать
Развернуть все
Эта статья переведена вручную. Наведите указатель мыши на предложения статьи, чтобы просмотреть исходный текст. Дополнительные сведения.
Перевод
Текст оригинала

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

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

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

Проще говоря, SAL — это простой способ позволить компилятору проверить код.

Hh916383.collapse_all(ru-ru,VS.120).gifSAL делает код более ценным

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


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

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

Копирует count байт из src в dest.При перекрытии иcходной и конечной области поведение 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
);

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

Hh916383.collapse_all(ru-ru,VS.120).gifОсновы SAL

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

Категория

Аннотации параметра

Описание

Для вызова функции

_In_

Данные передаются вызываемой функции и рассматриваются как значения только для чтения.

Входные данные вызываемой функции и выходные для вызывающего объекта

_Inout_

Годные к использованию данные передаются в функцию и потенциально изменены.

Выходные данные вызывающему объекту

_Out_

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

Указатель передается к вызывающему объекту

_Outptr_

Как Выходные данные вызывающему объекту. Значение, возвращаемое вызываемой функцией, является указателем.

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

В данной таблице отличия обязательных и необязательных параметров:

Обязательные параметры.

Необязательные параметры

Для вызова функции

_In_

_In_opt_

Входные данные вызываемой функции и выходные для вызывающего объекта

_Inout_

_Inout_opt_

Выходные данные вызывающему объекту

_Out_

_Out_opt_

Указатель передается к вызывающему объекту

_Outptr_

_Outptr_opt_

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

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

Hh916383.collapse_all(ru-ru,VS.120).gifИспользование средства анализа кода Visual Studio для обнаружения дефектов

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

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

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

  2. В меню Построение выберите Выполнить анализ кода в решении.

    Рассмотрим пример _In_ в этом разделе. Если запустить анализ кода на нем, то отображается предупреждение:

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

Hh916383.collapse_all(ru-ru,VS.120).gifПример : Аннотация _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 для этого примера, они проверят, что вызывающая сторона передаёт ненулевой указатель в инициализацию буфера для pInt. В этом случае указатель pInt не может иметь значение NULL.

Hh916383.collapse_all(ru-ru,VS.120).gifПример: Аннотация _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 проверяет, что функция выполняет проверку на NULL, прежде чем обращается к буферу.

Hh916383.collapse_all(ru-ru,VS.120).gifПример: Аннотация _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 проверяет, что вызывающий код передает указатель на буфер для pInt, отличный от NULL, и что буфер инициализируется функцией до ее завершения.

Hh916383.collapse_all(ru-ru,VS.120).gifПример: Аннотация _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 проверяет, что эта функция выполняет проверку на NULL, прежде чем разыменовать pInt, и если pInt не равен NULL, буфер инициализируется функцией до ее завершения.

Hh916383.collapse_all(ru-ru,VS.120).gifПример: Аннотация _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 проверяет, что вызывающие объекты должны передавать указатель, отличный от NULL, на инициализированный буфер для pInt, и что перед завершением, pInt остается отличным от NULL и буфер инициализирован.

Hh916383.collapse_all(ru-ru,VS.120).gifПример: Аннотация _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 проверяет, что эта функция выполняет проверку на NULL, прежде чем выполнять обращение к буферу, и если pInt не равен NULL, то буфер инициализируется функцией до ее завершения.

Hh916383.collapse_all(ru-ru,VS.120).gifПример: Аннотация _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 проверяет, что вызывающий код передает указатель на буфер для *pInt, отличный от NULL, и что буфер инициализируется функцией до ее завершения.

Hh916383.collapse_all(ru-ru,VS.120).gifПример: Аннотация _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 проверяет, что эта функция выполняет проверку на NULL, прежде чем разыменовать *pInt, и что буфер инициализируется функцией до ее завершения.

Hh916383.collapse_all(ru-ru,VS.120).gifПример: Аннотация _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 проверяет, что вызывающий передает указатель на буфер для pInt, отличный от NULL, и что буфер инициализируется функцией до ее завершения.

Hh916383.collapse_all(ru-ru,VS.120).gifДобавление аннотаций к существующему коду

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

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

Hh916383.collapse_all(ru-ru,VS.120).gifКогда добавлять аннотации?

Далее представлены некоторые рекомендации:

  • Аннотируйте все параметры-указатели.

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

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

  • Аннотируйте свойства драйвера и других предметно-ориентированных свойств.

Или вы можете аннотировать все параметры, чтобы сделать ваши намерения ясными во всем и чтобы упростить проверку, что аннотации были сделаны.

Добавления сообщества

ДОБАВИТЬ
Показ:
© 2015 Microsoft