了解 SALUnderstanding SAL

备注

本文适用于 Visual Studio 2015。This article applies to Visual Studio 2015. 如果你正在寻找最新的 Visual Studio 文档,请在左上方使用版本选择器。If you're looking for the latest Visual Studio documentation, use the version selector at the top left. 建议升级到 Visual Studio 2019。We recommend upgrading to Visual Studio 2019. 在此处下载Download it here

Microsoft 源代码注释语言(SAL)提供了一组可用于描述函数如何使用其参数的注释、对它们做出的假设,以及在完成时的保证。The Microsoft source-code annotation language (SAL) provides a set of annotations that you can use to describe how a function uses its parameters, the assumptions that it makes about them, and the guarantees that it makes when it finishes. 批注是在标头文件 <sal.h>中定义的。The annotations are defined in the header file <sal.h>. 适用于的C++ Visual Studio 代码分析使用 SAL 批注来修改其函数分析。Visual Studio code analysis for C++ uses SAL annotations to modify its analysis of functions. 有关适用于 Windows 驱动程序开发的 SAL 2.0 的详细信息,请参阅Windows 驱动程序的 sal 2.0 注释For more information about SAL 2.0 for Windows driver development, see SAL 2.0 Annotations for Windows Drivers.

对于开发人员而言C++ ,在本机和 C 中仅提供有限的方式,以一致的方式来表达意图和不变性。Natively, C and C++ provide only limited ways for developers to consistently express intent and invariance. 通过使用 SAL 批注,可以更详细地描述函数,以便使用它们的开发人员可以更好地了解如何使用它们。By using SAL annotations, you can describe your functions in greater detail so that developers who are consuming them can better understand how to use them.

什么是 SAL 以及您为何使用它?What Is SAL and Why Should You Use It?

简单地说,SAL 是一种成本较低的方法,使编译器能够为你检查代码。Simply stated, SAL is an inexpensive way to let the compiler check your code for you.

SAL 使代码更重要SAL Makes Code More Valuable

SAL 有助于使代码设计更易于理解,同时适用于用户和代码分析工具。SAL can help you make your code design more understandable, both for humans and for code analysis tools. 请考虑以下示例,其中显示了 C 运行时函数 memcpyConsider this example that shows the C runtime function memcpy:

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

您能否知道此函数的作用?Can you tell what this function does? 当实现或调用函数时,必须保持某些属性以确保程序的正确性。When a function is implemented or called, certain properties must be maintained to ensure program correctness. 只需查看示例中的声明,就不知道这些内容。Just by looking at a declaration such as the one in the example, you don't know what they are. 如果没有 SAL 批注,则必须依赖文档或代码注释。Without SAL annotations, you'd have to rely on documentation or code comments. 下面是有关 memcpy 的 MSDN 文档的内容:Here’s what the MSDN documentation for memcpy says:

"将计数字节的源复制到目标。"Copies count bytes of src to dest. 如果源和目标重叠,则 memcpy 的行为是不确定的。If the source and destination overlap, the behavior of memcpy is undefined. 使用 memmove 处理重叠区域。Use memmove to handle overlapping regions.
安全说明: 请确保目标缓冲区的大小等于或大于源缓冲区的大小。Security Note: Make sure that the destination buffer is the same size or larger than the source buffer. 有关详细信息,请参阅避免缓冲区溢出。For more information, see Avoiding Buffer Overruns."

文档包含一些信息,这些信息表明你的代码必须维护某些属性以确保程序的正确性:The documentation contains a couple of bits of information that suggest that your code has to maintain certain properties to ensure program correctness:

  • memcpy 将字节的 count 从源缓冲区复制到目标缓冲区。memcpy copies the count of bytes from the source buffer to the destination buffer.

  • 目标缓冲区的大小必须至少与源缓冲区的大小相同。The destination buffer must be at least as large as the source buffer.

    但编译器无法读取文档或非正式注释。However, the compiler can't read the documentation or informal comments. 这并不知道两个缓冲区与 count之间存在关系,也不能有效地推测关系。It doesn't know that there is a relationship between the two buffers and count, and it also can't effectively guess about a relationship. SAL 可以更清楚地了解函数的属性和实现,如下所示:SAL could provide more clarity about the properties and implementation of the function, as shown here:

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

请注意,这些批注与 MSDN 文档中的信息类似,但它们更简洁,它们遵循语义模式。Notice that these annotations resemble the information in the MSDN documentation, but they are more concise and they follow a semantic pattern. 阅读此代码后,你可以快速了解此函数的属性,以及如何避免缓冲区溢出的安全问题。When you read this code, you can quickly understand the properties of this function and how to avoid buffer overrun security issues. 更好的是,SAL 提供的语义模式可以在潜在 bug 的早期发现中提高自动化代码分析工具的效率和有效性。Even better, the semantic patterns that SAL provides can improve the efficiency and effectiveness of automated code analysis tools in the early discovery of potential bugs. 假设有人将此错误实现 wmemcpyImagine that someone writes this buggy implementation of 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;  
}  
  

此实现包含一个公共的、不是一个的错误。This implementation contains a common off-by-one error. 幸运的是,代码作者包含 SAL 缓冲区大小注释,代码分析工具可通过单独分析此函数来捕获 bug。Fortunately, the code author included the SAL buffer size annotation—a code analysis tool could catch the bug by analyzing this function alone.

SAL 基础SAL Basics

SAL 定义了四种基本类型的参数,这些参数按使用模式分类。SAL defines four basic kinds of parameters, which are categorized by usage pattern.

类别Category 参数批注Parameter Annotation 说明Description
被调用函数的输入Input to called function _In_ 数据将传递给被调用的函数,并被视为只读。Data is passed to the called function, and is treated as read-only.
对被调用函数的输入和到调用方的输出Input to called function, and output to caller _Inout_ 可用数据将传递到函数中,并可能被修改。Usable data is passed into the function and potentially is modified.
向调用方输出Output to caller _Out_ 调用方只为所调用的函数提供空间来写入。The caller only provides space for the called function to write to. 被调用的函数将数据写入该空间。The called function writes data into that space.
指向调用方的指针的输出Output of pointer to caller _Outptr_ 类似于输出到调用方Like Output to caller. 被调用函数返回的值是一个指针。The value that's returned by the called function is a pointer.

这四个基本批注可以通过多种方式进行更明确的了解。These four basic annotations can be made more explicit in various ways. 默认情况下,使用批注指针参数是必需的,它们必须为非 NULL,函数才能成功。By default, annotated pointer parameters are assumed to be required—they must be non-NULL for the function to succeed. 基本批注最常使用的变体指示指针参数是可选的,如果该参数为 NULL,则该函数仍可成功执行其工作。The most commonly used variation of the basic annotations indicates that a pointer parameter is optional—if it's NULL, the function can still succeed in doing its work.

此表显示了如何区分必需参数和可选参数:This table shows how to distinguish between required and optional parameters:

参数是必需的Parameters are required 参数是可选的Parameters are optional
被调用函数的输入Input to called function _In_ _In_opt_
对被调用函数的输入和到调用方的输出Input to called function, and output to caller _Inout_ _Inout_opt_
向调用方输出Output to caller _Out_ _Out_opt_
指向调用方的指针的输出Output of pointer to caller _Outptr_ _Outptr_opt_

这些批注有助于识别可能未初始化的值,无效的空指针将以正式准确的方式使用。These annotations help identify possible uninitialized values and invalid null pointer uses in a formal and accurate manner. 将 NULL 传递给必需的参数可能会导致崩溃,否则可能会导致返回 "failed" 错误代码。Passing NULL to a required parameter might cause a crash, or it might cause a "failed" error code to be returned. 无论采用哪种方式,函数都不能成功执行其作业。Either way, the function cannot succeed in doing its job.

SAL 示例SAL Examples

此部分显示基本 SAL 批注的代码示例。This section shows code examples for the basic SAL annotations.

使用 Visual Studio 代码分析工具查找 BugUsing the Visual Studio Code Analysis Tool to Find Defects

在示例中,Visual Studio Code 分析工具与 SAL 注释一起用于查找代码缺陷。In the examples, the Visual Studio Code Analysis tool is used together with SAL annotations to find code defects. 下面介绍了如何执行此操作。Here's how to do that.

使用 Visual Studio 代码分析工具和 SALTo use Visual Studio code analysis tools and SAL
  1. 在 Visual Studio 中,打开C++包含 SAL 批注的项目。In Visual Studio, open a C++ project that contains SAL annotations.

  2. 在菜单栏上,选择 "生成"、 "对解决方案运行代码分析"。On the menu bar, choose Build, Run Code Analysis on Solution.

    请考虑本部分_ 示例中的 _。Consider the _In_ example in this section. 如果对其运行代码分析,将显示以下警告:If you run code analysis on it, this warning is displayed:

    C6387 参数值无效 C6387 Invalid Parameter Value
    "指向" 可能是 "0":这不符合函数 "InCallee" 的规范。'pInt' could be '0': this does not adhere to the specification for the function 'InCallee'.

示例:_ 批注中的 _Example: The _In_ Annotation

_In_ 批注指示:The _In_ annotation indicates that:

  • 参数必须有效且不会被修改。The parameter must be valid and will not be modified.

  • 函数将只从单元素缓冲区中读取。The function will only read from the single-element buffer.

  • 调用方必须提供缓冲区并对其进行初始化。The caller must provide the buffer and initialize it.

  • _In_ 指定 "只读"。_In_ specifies "read-only". 常见的错误是将 _In_ 应用于应改为 _Inout_ 批注的参数。A common mistake is to apply _In_ to a parameter that should have the _Inout_ annotation instead.

  • 在非指针标量上,分析器允许使用 _In_ 但不允许使用。_In_ is allowed but ignored by the analyzer on non-pointer scalars.

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 分析,它将验证调用方是否将非 Null 指针传递到 pInt的已初始化缓冲区。If you use Visual Studio Code Analysis on this example, it validates that the callers pass a non-Null pointer to an initialized buffer for pInt. 在这种情况下,pInt 指针不能为 NULL。In this case, pInt pointer cannot be NULL.

示例: _In_opt_ 批注Example: The _In_opt_ Annotation

_In_opt__In_相同,不同之处在于允许输入参数为 NULL,因此函数应检查此值。_In_opt_ is the same as _In_, except that the input parameter is allowed to be NULL and, therefore, the function should check for this.

  
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。Visual Studio Code Analysis validates that the function checks for NULL before it accesses the buffer.

示例: _Out_ 批注Example: The _Out_ Annotation

_Out_ 支持一个常见情况,即在其中传递指向元素缓冲区的非 NULL 指针,并且该函数将初始化元素。_Out_ supports a common scenario in which a non-NULL pointer that points to an element buffer is passed in and the function initializes the element. 调用方无需在调用之前初始化缓冲区;被调用的函数承诺在返回之前对其进行初始化。The caller doesn’t have to initialize the buffer before the call; the called function promises to initialize it before it returns.

  
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 分析工具将验证调用方是否将非 NULL 指针传递到 pInt 的缓冲区,并验证该缓冲区是否在返回之前由函数进行了初始化。Visual Studio Code Analysis Tool validates that the caller passes a non-NULL pointer to a buffer for pInt and that the buffer is initialized by the function before it returns.

示例: _Out_opt_ 批注Example: The _Out_opt_ Annotation

_Out_opt__Out_相同,不同之处在于允许参数为 NULL,因此该函数应检查此值。_Out_opt_ is the same as _Out_, except that the parameter is allowed to be NULL and, therefore, the function should check for this.

  
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 分析验证此函数在取消引用 pInt 之前是否检查 NULL,如果 pInt 不为 NULL,则该函数将在返回前通过函数初始化缓冲区。Visual Studio Code Analysis validates that this function checks for NULL before pInt is dereferenced, and if pInt is not NULL, that the buffer is initialized by the function before it returns.

示例: _Inout_ 批注Example: The _Inout_ Annotation

_Inout_ 用于批注可能由函数更改的指针参数。_Inout_ is used to annotate a pointer parameter that may be changed by the function. 指针必须指向有效的已初始化数据,然后才能调用,即使它发生更改,返回的值也必须有效。The pointer must point to valid initialized data before the call, and even if it changes, it must still have a valid value on return. 批注指定函数可以从单元素缓冲区自由读取和写入。The annotation specifies that the function may freely read from and write to the one-element buffer. 调用方必须提供缓冲区并对其进行初始化。The caller must provide the buffer and initialize it.

备注

_Out_一样,_Inout_ 必须应用于可修改的值。Like _Out_, _Inout_ must apply to a modifiable value.

  
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 分析验证调用方将非 NULL 指针传递到 pInt的已初始化缓冲区,并且在返回之前,pInt 仍为非 NULL,并且已初始化缓冲区。Visual Studio Code Analysis validates that callers pass a non-NULL pointer to an initialized buffer for pInt, and that, before return, pInt is still non-NULL and the buffer is initialized.

示例: _Inout_opt_ 批注Example: The _Inout_opt_ Annotation

_Inout_opt__Inout_相同,不同之处在于允许输入参数为 NULL,因此函数应检查此值。_Inout_opt_ is the same as _Inout_, except that the input parameter is allowed to be NULL and, therefore, the function should check for this.

  
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,则该函数将在返回前通过函数初始化缓冲区。Visual Studio Code Analysis validates that this function checks for NULL before it accesses the buffer, and if pInt is not NULL, that the buffer is initialized by the function before it returns.

示例: _Outptr_ 批注Example: The _Outptr_ Annotation

_Outptr_ 用于批注要返回指针的参数。_Outptr_ is used to annotate a parameter that's intended to return a pointer. 参数本身不应为 NULL,并且被调用的函数在其中返回非 NULL 指针,该指针指向已初始化的数据。The parameter itself should not be NULL, and the called function returns a non-NULL pointer in it and that pointer points to initialized data.

  
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 指针,并验证该缓冲区是否在返回前由函数进行了初始化。Visual Studio Code Analysis validates that the caller passes a non-NULL pointer for *pInt, and that the buffer is initialized by the function before it returns.

示例: _Outptr_opt_ 批注Example: The _Outptr_opt_ Annotation

_Outptr_opt__Outptr_相同,不同之处在于参数是可选的,调用方可以传入参数的 NULL 指针。_Outptr_opt_ is the same as _Outptr_, except that the parameter is optional—the caller can pass in a NULL pointer for the parameter.

  
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 分析验证此函数在取消引用 *pInt 之前是否检查 NULL,并验证该函数是否在返回之前由函数进行了初始化。Visual Studio Code Analysis validates that this function checks for NULL before *pInt is dereferenced, and that the buffer is initialized by the function before it returns.

示例: _成功_ 批注与 _Out_Example: The _Success_ Annotation in Combination with _Out_

批注可应用于大多数对象。Annotations can be applied to most objects. 特别是,您可以批注整个函数。In particular, you can annotate a whole function. 函数最明显的特征之一是它可以成功或失败。One of the most obvious characteristics of a function is that it can succeed or fail. 但就像缓冲区及其大小之间的关联,C/C++无法表示函数成功或失败。But like the association between a buffer and its size, C/C++ cannot express function success or failure. 通过使用 _Success_ 注释,可以说出函数的成功情况。By using the _Success_ annotation, you can say what success for a function looks like. _Success_ 批注的参数只是一个表达式,表示函数已成功。The parameter to the _Success_ annotation is just an expression that when it is true indicates that the function has succeeded. 表达式可以是批注分析器可处理的任何内容。The expression can be anything that the annotation parser can handle. 当函数返回后,批注的效果仅适用于函数成功。The effects of the annotations after the function returns are only applicable when the function succeeds. 此示例演示 _Success_ 如何与 _Out_ 进行交互以执行正确的操作。This example shows how _Success_ interacts with _Out_ to do the right thing. 您可以使用关键字 return 来表示返回值。You can use the keyword return to represent the return value.

  
_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 Code 分析验证调用方将非 NULL 指针传递到用于 pInt的缓冲区,并且该缓冲区在返回前由函数进行初始化。The _Out_ annotation causes Visual Studio Code Analysis to validate that the caller passes a non-NULL pointer to a buffer for pInt, and that the buffer is initialized by the function before it returns.

SAL 最佳做法SAL Best Practice

向现有代码中添加批注Adding Annotations to Existing Code

SAL 是一项功能强大的技术,可帮助您提高代码的安全性和可靠性。SAL is a powerful technology that can help you improve the security and reliability of your code. 了解 SAL 后,您可以将新技能应用于您的日常工作。After you learn SAL, you can apply the new skill to your daily work. 在新代码中,可以在整个设计中使用基于 SAL 的规范;在较旧的代码中,你可以递增地添加批注,因此,每次更新时都会增加权益。In new code, you can use SAL-based specifications by design throughout; in older code, you can add annotations incrementally and thereby increase the benefits every time you update.

Microsoft 公共标头已进行批注。Microsoft public headers are already annotated. 因此,我们建议在项目中首先批注叶节点函数和调用 Win32 Api 的函数以获得最大的好处。Therefore, we suggest that in your projects you first annotate leaf node functions and functions that call Win32 APIs to get the most benefit.

何时批注?When Do I Annotate?

下面是一些指导原则:Here are some guidelines:

  • 批注所有指针参数。Annotate all pointer parameters.

  • 为值范围批注添加批注,以便代码分析可以确保缓冲区和指针安全性。Annotate value-range annotations so that Code Analysis can ensure buffer and pointer safety.

  • 批注锁定规则和锁定副作用。Annotate locking rules and locking side effects. 有关详细信息,请参阅对锁定行为进行批注For more information, see Annotating Locking Behavior.

  • 批注驱动程序属性和其他特定于域的属性。Annotate driver properties and other domain-specific properties.

    或者,您可以为所有参数添加注释以使您的意图清晰明了,并使您能够轻松地检查批注是否已完成。Or you can annotate all parameters to make your intent clear throughout and to make it easy to check that annotations have been done.

代码分析团队博客Code Analysis Team Blog

另请参阅See Also

使用 SAL 注释减少 C/C++代码缺陷 Using SAL Annotations to Reduce C/C++ Code Defects
批注函数参数和返回值 Annotating Function Parameters and Return Values
批注函数行为 Annotating Function Behavior
批注结构和类 Annotating Structs and Classes
批注锁定行为 Annotating Locking Behavior
指定何时以及在何处应用批注 Specifying When and Where an Annotation Applies
最佳做法和示例Best Practices and Examples