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_ 杂注用于函数的指针参数可能被更改。指针必须指向有效的初始化的数据调用之前,因此,即使更改,则仍必须在返回的有效值。批注指定函数可以随意读取和写入的元素的缓冲区。调用方必须提供缓冲区并将其初始化。
| .gif) 说明 | 
|---|
| 与 _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 获取最优点的叶节点和函数。
何时批注?
下面是一些参考:
- 批注所有指针参数。 
- 杂范围注释,以便代码分析可以确保缓冲区和安全指针。 
- 杂锁定规则和锁副作用。有关详细信息,请参阅对锁定行为进行批注。 
- 杂驱动程序属性和其他特定的属性。 
或者可以批注所有参数使整个中目的清晰并使其易于完成检查注释。