了解 SAL

Microsoft 源代码注释语言 (SAL) 提供设置批注可以使用描述的功能如何使用其参数,它对其假设并确保它使其在完成。批注可标头文件 <sal.h>定义。Visual Studio C++ 的代码分析使用 SAL 注释修改函数进行分析。有关 windows 驱动程序开发的 SAL 2.0 的更多信息,请参见 SAL windows 驱动程序的 2.0 批注

本身,C 和 C++ 为开发人员提供仅限一种一致地快速用途以及不变性。使用 SAL 注释,可以更详细地描述您的功能,以便使用它们的开发人员可以更好地了解如何使用它们。

什么原因是 SAL 和应使用它?

为简单起见,SAL 是一个成本较低的方式让编译器检查您的代码。

Hh916383.collapse_all(zh-cn,VS.110).gifSAL 使代码更重要

SAL 有助于使您的代码模型可理解,对于方以及代码分析工具。考虑公开 C 运行时函数 memcpy的以下示例:

void * memcpy(
   void *dest, 
   const void *src, 
   size_t count
);

是否能够调用此功能?在实现功能时或调用时,必须维护某些属性以确保程序的有效性。通过查看一个声明例如于该示例中,您不知道它们是。没有 SAL 注释,您必须确定文档或代码注释。这就是" memcpy 的 MSDN 文档添加:

“复制计数字节为 dest 的 src。如果源和目标,重叠行为 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
);

请注意下面的批注类似于 MSDN 文档中的信息,但是,它们更简洁,它们遵循语义形式。当您读取此代码时,您可以快速理解此功能属性以及如何避免缓冲区溢出安全问题。请改进,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 缓冲区区域大小批注代码分析工具可以通过分析单独此函数捕获 bug。

Hh916383.collapse_all(zh-cn,VS.110).gifSAL 基础知识

SAL 定义了四基本的参数,则使用模式类别。

类别

批注参数

描述

调用函数的输入

_In_

数据传递给被调用函数和被视为只读。

调用的函数和输出的输入到调用方

_Inout_

可用数据传递到函数以及可能的修改。

对调用方的输出

_Out_

调用方为调用的函数只提供空间写入。调用函数数据写入该空间中。

指针输出到调用方的

_Outptr_

Output to caller。通过调用函数返回的值是指针。

这四个基本的批注可以显式采用多种方式。默认情况下,假定参数所需它们的批注指针必须为非 null 使该功能可以成功。基本的批注中最常用的变体指示参数是可选的指针,则为空,该功能可以在完成其工作仍成功。

此表显示如何区分必选参数和可选参数之间:

需要参数

参数是可选的。

调用函数的输入

_In_

_In_opt_

调用的函数和输出的输入到调用方

_Inout_

_Inout_opt_

对调用方的输出

_Out_

_Out_opt_

指针输出到调用方的

_Outptr_

_Outptr_opt_

这些批注有助于标识可能的未初始化值和无效 null 指针使用的正式和准确方式。传递 NULL 到一个必需的参数可能会导致失败,也可能导致“失败”将返回的错误代码。每种方法,函数不能在使其工作成功。

SAL 示例

本节演示基本 SAL 注释的代码示例。

Hh916383.collapse_all(zh-cn,VS.110).gif使用查找 Visual Studio 代码分析工具 bug

在此示例中,Visual Studio 代码分析工具与 SAL 注释一起用于查找代码缺陷。这是如何执行此操作。

若要使用 Visual Studio 代码分析工具和 SAL

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

  2. 在菜单栏上,依次选择 生成对解决方案运行代码分析

    考虑本节中的_In_示例。如果您运行的代码分析,此警告显示:

    C6387 参数值无效“pint”来为“0 ":这不遵循函数的“InCallee”规范。

Hh916383.collapse_all(zh-cn,VS.110).gif示例:_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 指针不能为空。

Hh916383.collapse_all(zh-cn,VS.110).gif示例:_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 代码分析验证空功能测试,则访问缓冲区之前。

Hh916383.collapse_all(zh-cn,VS.110).gif示例:_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 的缓冲区,并缓冲区由函数初始化,在返回之前。

Hh916383.collapse_all(zh-cn,VS.110).gif示例:_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 不为空,缓冲区由函数初始化,在返回之前。

Hh916383.collapse_all(zh-cn,VS.110).gif示例:_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,并且缓冲区初始化。

Hh916383.collapse_all(zh-cn,VS.110).gif示例:_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 不为空之前,缓冲区由函数初始化,在返回之前。

Hh916383.collapse_all(zh-cn,VS.110).gif示例:_Outptr_批注

_Outptr_ 用于说明用于返回指针的参数。该参数不应为空,因此,调用函数返回其中的一个非 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 指针,因此,缓冲区由函数初始化,在返回之前。

Hh916383.collapse_all(zh-cn,VS.110).gif示例:_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 取消引用之前,并且,缓冲区由函数初始化,在返回之前。

Hh916383.collapse_all(zh-cn,VS.110).gif示例:与_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 最优方法

Hh916383.collapse_all(zh-cn,VS.110).gif添加批注到现有代码

SAL 是可帮助您提高代码的安全性和可靠性功能强大的技术。在了解 SAL 后,可以将新的技能于您的日常工作。在新代码,可以通过设计始终使用基于 SAL 的规范;旧的代码,可以增量添加批注从而都递增优点。更新。

Microsoft 公共标头已说明。因此,建议在项目首次说明称为 Win32 API 访问该大多数优点的叶节点功能。

Hh916383.collapse_all(zh-cn,VS.110).gif我时说明?

下面是一些准则:

  • 批注所有指针参数。

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

  • 锁定规则和锁定副作用的 Annotate。有关更多信息,请参见对锁定行为进行批注

  • 说明驱动器属性和其他域特定属性。

也可以批注所有参数指示您的意图在和中轻松地检查批注完成。

相关资源

代码分析团队博客

请参见

参考

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

对函数行为进行批注

批注结构和类

对锁定行为进行批注

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

最佳做法和示例 (SAL)

其他资源

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