了解 SAL
Microsoft 源代码注释语言 (SAL) 提供可以用来描述它使有关他们的一组函数说明如何使用参数,假设和保证它使得何时完成。在头文件注释 <sal.h>定义。C++ 的 Visual Studio 代码分析使用 SAL 注释函数修改其分析。有关 Windows 驱动程序开发的 SAL 2.0 的更多信息,请参见 SAL Windows 驱动程序的说明 2.0。
本身, C# 和 C++开发人员只提供有限的一种一致地快速意图不变性。使用 SAL 注释,可以用更详细地描述函数,以便使用自己的开发人员可以更好地了解如何使用它们。
什么是 SAL 以及您为何使用它?
为简单起见,SAL 是一种可以小的方式使编译器可以检查您的代码。
SAL 使代码更重要
SAL 有助于使代码易于理解设计,用于人员和为代码分析工具。注意显示 C 运行时函数 memcpy的此示例:
void * memcpy(
void *dest,
const void *src,
size_t count
);
是否可以调用此函数?当函数实现或调用时,必须维护某些属性确保程序更正。通过声明一个如查看一个示例中,不知道他们是什么。无 SAL 注释,则必须由文档或代码注释。这是任何 memcpy 的 MSDN 文档指出:
“复制字节为 dest src 的计数。如果源和目标字符串重叠,memcpy的行为是未定义。使用 memmove 处理重叠区域。Security Note:,确保目标缓冲区的大小和源缓冲区大小相同。有关更多信息,请参见避免缓冲区溢出。
文档包含建议的两个代码必须位信息维护某些属性确保程序正确性:
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 提供可以改进的自动化代码分析工具效率和效果中的潜在 bug 早期的语义的查看模式。假设用户编写的多虫用此 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 缓冲区大小注释代码分析工具可以通过分析此单独函数捕获 Bug。
SAL 基础
SAL 定义了四的基本参数,由用法模式分类。
类别 |
批注参数 |
说明 |
---|---|---|
向调用函数的输入 |
_In_ |
数据传递给被调用函数和被视为只读。 |
为被调用函数到调用方的输入和输出 |
_Inout_ |
可用数据传入函数也可能要修改。 |
为调用方的输出 |
_Out_ |
调用方为调用的函数只提供空间信息写入。调用函数编写数据放入该空间。 |
输出到调用方的指针 |
_Outptr_ |
像 Output to caller。通过调用的函数返回的值是一个指针。 |
这四基本的注释可以显式允许各种方式。默认情况下,假定参数需要其的批注指针必须是非 void 的函数才能成功。基本注释的最常用的一种变形指示参数是上,选项则为 NULL,则函数可以在完成工作仍将成功。
此表演示如何区分所需和可选参数之间切换:
对于参数,为必选项。 |
参数可选 |
|
---|---|---|
向调用函数的输入 |
_In_ |
_In_opt_ |
为被调用函数到调用方的输入和输出 |
_Inout_ |
_Inout_opt_ |
为调用方的输出 |
_Out_ |
_Out_opt_ |
输出到调用方的指针 |
_Outptr_ |
_Outptr_opt_ |
这些注释有助于标识可能的未初始化值和无效的 null 指针使用采用一个形和精确方法。传递 null 到必需的参数可能导致系统崩溃,或者可能产生“失败”将返回错误代码。不论是用哪种方式,函数不能成功工作成功。
SAL 示例
本节演示的基本 SAL 注释的代码示例。
使用 Visual Studio 代码分析工具查找 Bug
在此示例中,Visual Studio 代码分析工具将 SAL 批注用于发现代码缺陷。这是如何做到这一点。
使用 Visual Studio 代码分析工具和 SAL
在 Visual Studio 中,包含 SAL 注释的 C. 打开 C++ 项目。
在 生成 菜单中,选择 对解决方案运行代码分析。
考虑本节中的_In_ example_。如果运行的代码分析警告,此显示:
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 代码分析,它验证调用方传递非 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 代码分析功能验证空测试。
示例: _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 代码分析工具验证调用方传递非 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 代码分析验证空结果,此函数在 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 代码分析会对调用方传递非 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 代码分析验证空结果,此函数在 取消引用,如果 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 代码分析验证调用方传递非 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 代码分析验证空结果,此函数在 取消引用,如果 *pInt 不为 NULL,则缓冲区由函数初始化,则返回。
示例: 与 _Out_ 组合的 _Success_ 批注
批注还可应用于大多数对象。具体而言,可以使用批注来批注整个函数。一个函数的最显而易见的特性是其可成功或失败。但是,如在缓冲区及其大小之间的关联,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 代码分析会对调用方传递非 null 指针。pInt的缓冲区,并且,缓冲区由函数初始化,则返回。
SAL 最佳做法
向现有代码中添加批注
SAL 可帮助您改善代码的安全性和可靠性的强大的技术。在了解 SAL 后,可以将新的技能运用于日常工作。在新的代码中可以声明基于 SAL 的规范;在旧的代码,在更新时,可以添加注释和增量从而增加的优点。
Microsoft 公共标题中批注。因此,建议在项目首次批注调用 Win32 API 获取最优点的叶节点和函数。
何时批注?
下面是一些参考:
批注所有指针参数。
杂范围注释,以便代码分析可以确保缓冲区和安全指针。
杂锁定规则和锁副作用。有关详细信息,请参阅对锁定行为进行批注。
杂驱动程序属性和其他特定的属性。
或者可以批注所有参数使整个中目的清晰并使其易于完成检查注释。