了解 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

  1. 在 Visual Studio 中,包含 SAL 注释的 C. 打开 C++ 项目。

  2. 在 生成 菜单中,选择 对解决方案运行代码分析。

    考虑本节中的_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 获取最优点的叶节点和函数。

何时批注?

下面是一些参考:

  • 批注所有指针参数。

  • 杂范围注释,以便代码分析可以确保缓冲区和安全指针。

  • 杂锁定规则和锁副作用。有关详细信息,请参阅对锁定行为进行批注

  • 杂驱动程序属性和其他特定的属性。

或者可以批注所有参数使整个中目的清晰并使其易于完成检查注释。

相关资源

Team Blog 代码分析

请参见

参考

对函数参数和返回值进行批注

对函数行为进行批注

批注结构和类

对锁定行为进行批注

指定何时以及在何处应用批注

最佳做法和示例 (SAL)

其他资源

使用 SAL 批注以减少 C/C++ 代码缺陷