Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Язык заметки исходного кода Майкрософт (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 для поиска дефектов
В примерах средство анализа кода Visual Studio используется вместе с заметками SAL для поиска дефектов кода. Вот как это сделать.
Использование средств анализа кода Visual Studio и SAL
В Visual Studio откройте проект C++, содержащий заметки SAL.
В строке меню выберите Сборка, Выполнить анализ кода на решении.
Рассмотрим пример _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 в этом примере, он подтверждает, что вызывающие передают ненулевой указатель на инициализированный буфер для 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 проверяет, проверяет ли функция значение 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 проверяет, передает ли вызывающий указатель на буфер pInt
, отличный от NULL, и что буфер инициализируется функцией перед возвратом.
Пример: заметка _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, то функция инициализирует буфер перед возвратом.
Пример: аннотация _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 проверяет, что вызывающие передают ненулевой указатель на инициализированный буфер для 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 проверяет, проверяет ли эта функция значение 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 проверяет, что вызывающий объект передает указатель *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 проверяет, проверяет ли эта функция значение 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_
приводит к анализу кода Visual Studio для проверки того, что вызывающий объект передает указатель, отличный от NULL, в буфер pInt
, и что буфер инициализирован функцией перед возвратом.
Лучшие практики SAL
Добавление заметок в существующий код
SAL — это мощная технология, которая помогает повысить безопасность и надежность кода. После обучения SAL вы можете применить новый навык к повседневной работе. В новом коде можно использовать спецификации на основе SAL по всему дизайну; в более старом коде можно добавлять заметки постепенно и тем самым увеличивать преимущества при каждом обновлении.
Общедоступные заголовки Майкрософт уже аннотированы. Поэтому мы рекомендуем в ваших проектах сначала аннотировать функции конечных узлов и функции, которые вызывают Win32 API, чтобы извлечь максимальную пользу.
Когда я делаю аннотацию?
Ниже приведены некоторые рекомендации.
Заметите все параметры указателя.
Аннотируйте аннотации диапазона значений, чтобы анализ кода мог обеспечить безопасность буфера и указателя.
Аннотировать правила блокировки и побочные эффекты блокировки. Дополнительные сведения см. в Аннотировании поведения блокировки.
Заметите свойства драйвера и другие свойства, относящиеся к домену.
Или вы можете пометить все параметры, чтобы сделать ваше намерение ясным, и упростить проверку того, что аннотации были выполнены.