Основные сведения о языке 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
В 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 Code, он проверяет, передает ли вызывающий указатель на инициализированный буфер без pInt
null. В этом случае 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, чтобы получить наибольшее преимущество.
Когда я делаю аннотацию?
Ниже приведены некоторые рекомендации.
Заметите все параметры указателя.
Заметки диапазона значений, чтобы анализ кода обеспечивал безопасность буфера и указателя.
Отметка правил блокировки и блокировка побочных эффектов. Дополнительные сведения см. в разделе "Аннотирование поведения блокировки".
Заметите свойства драйвера и другие свойства, относящиеся к домену.
Кроме того, вы можете сделать все параметры понятными для всего намерения и упростить проверку того, что заметки были выполнены.