UID 2
精华
积分 7750
威望 点
宅币 个
贡献 次
宅之契约 份
最后登录 1970-1-1
在线时间 小时
标准注释语言SAL
序言
SAL是微软源代码注释语言。通过源代码注释,可以将源码中隐藏的某些意图表达出来,也有助于自动静态源码分析工具更准确地理解代码,减少他们的误判。本文详细讨论了SAL语法的各个方面,并给出用法示例。
SAL可以很好地接轨高级语言和底层语言,减少高级语言编程出现的错误,最显著的是可以减少缓冲区溢出代码的产生,对于高级语言编程极为必要,据我所知目前只有2005版以后的MSVC系列编译器支持SAL。本文原文在附录B,若发现文中有什么翻译不当之处,敬请告知。下面是本文结构:
理解SAL:本章阐述了SAL注释语言的核心语言,及相关示例。 注释函数参数和返回值:描述了用于注释函数参数类型和返回值类型的SAL语法。 注释函数行为:描述了用于注释函数行为的SAL语法。 注释结构体和类:描述了用于注释结构体和类的SAL语法。 注释加锁行为:描述了如何将SAL用于锁机制。 指定SAL注释应用范围和时机:描述了SAL注释作用条件和作用域相关语法。 内部函数SAL注释概览:列出了内部函数使用的SAL注释。 优秀实例:给出了如何使用SAL注释的例子,同时也说明了常见的误用。
为什么有这篇翻译文章?直接使用中文MSDN不就可以了吗?
诚然,可以通过改变语言编码变成中文,例如将“http://msdn.microsoft.com/en-us/library/
hh916382.aspx”中的en-us改为zh-cn,就是全中文的了。然而翻译是一种机械行为,包含大量错误翻译和无效翻译,对于之间没有了解过的知识点,很容易被误导或者完全无法理解。本篇文章是本人一边学习一边翻译的结果,本人英语水平并不差,不过这并不代表所翻译的人人都能理解,也不能保证所有地方一定正确,如果发现有误,请通过QQ或email联系我。
第一章 理解SAL
微软源代码注释语言SAL,提供了一套用于描述函数参数的使用规则定义,允许对其作出假设并确保编译器遵守规则。注释定义在<sal.h>文件中。Visual Studio C++为函数分析提供了SAL注释用于代码分析。
一般的C/C++语法表达意图和不变性的方式及其有限。使用了SAL注释后,可以尽可能为你的函数定义它们,以便于使用该代码的人可以更好地理解和使用。
1.1什么是SAL?为什么要用SAL?
简单地说,SAL提供了一种简单的方式让编译器检查源代码。
1.1.1 SAL让代码更有含义
SAL可以帮你设计对于分析工具和操作人员都更为容易理解的代码。来看下面这个例子,它是C运行库函数memcpy的声明:void * memcpy(void *dest, const void *src, size_t count); 复制代码 你能说出这个函数是做什么用的吗?在一个函数的实现和调用中,编译器会检查相关规则以确保程序的正确性。如果只看类似上例所示声明,是无法知道他们是用来做什么的 。在没有SAL注释的情况下必须参考相关文档和代码注释才能了解他们的作用。下面是MSDN文档中关于memcpy的叙述:
“将count个字节从src复制到dest中。如果源内存区域和目的内存区域重叠则memcpy的行为是未定义的。memmove函数用于处理重叠区域。安全提示:确保目的缓冲区的大小大于或等于源缓冲区大小。更多信息见Avoiding Buffer Overruns一节。”
该文档包含了很多信息并建议在代码中遵守一定的规则以确保程序正确性:
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++)
{ // 缺陷:多出一个元素导致缓冲区溢出
dest[i ] = src[i ];
}
return dest;
} 复制代码 (本人对这点严重不赞同)
该实现存在缓冲区溢出缺陷。幸运的是,代码的作者使用了SAL缓冲区大小注释——代码分析工具只需分析该函数就可以捕获该缺陷。
1.1.2 SAL基础知识
SAL定义了四种基本参数类型,按用法规范可分类如下:
类型 参数注释 描述 输入传递给被调用函数 _In_ 将输入数据传送给被调用的函数,以只读类型对待 输入传递给被调用函数并输出给调用函数 _Inout_ 可用数据传送给被调用的函数并允许修改 输出传递给调用函数 _Out_ 调用函数只提供写入空间给被调用函数,被调用函数将处理结果写入该空间 输出指针传递给调用函数 _Outptr_ 调用函数只提供写入空间给被调用函数,被调用函数将处理结果写入该空间,返回的是该空间地址。
这四种基本类型注释有多种显示表示形式。经过注释的指针类型参数默认为必须的参数——为了程序能执行成功它们不能为NULL。基本注释最常用的一种变体用于表示一个指针类型参数是可选的参数——如果为NULL,函数仍能正常运行。
下表列出了必须参数和可选参数的不同点:必须的参数 可选的参数 输入传递给被调用函数 _In_ _In_opt_ 输入传递给被调用函数并输出给调用函数 _Inout_ _Inout_opt_ 输出传递给调用函数 _Out_ _Out_opt_ 输出指针传递给调用函数 _Outptr_ _Outptr_opt_
这些注释采用了一种正式而准确的方法用于标识出可能存在的未初始化变量、无效空指针。将NULL传递给一个必须参数可能会导致程序崩溃,或者返回一个失败错误代码,无论怎样该函数都无法成功执行了。
1.2 SAL实例
该节给出了使用基本SAL注释的例子。
1.2.1 使用Visual Studio代码分析工具查找程序缺陷
在以下例子中,需要使用Visual Studio代码分析工具结合SAL注释查找代码缺陷。操作步骤如下:
1)在Visual Studio中,打开一个含有SAL注释的C++项目。
2)在菜单栏选择“生成”,“对解决方案运行代码分析”。对于本节中的_In_例子,如果运行了代码分析会得到如下警告:warning : C6387: “pInt”可能是“0”: 这不符合函数“InCallee”的规范。
3)以下代码需要包含<sal.h>头文件
1.2.2 例子:_In_注释
_In_注释有以下几点需要注意:
参数有效且不会被修改。 函数只能从单元素缓冲区读取数据。 调用者必须提供缓冲区并对其进行初始化。 _In_规定了只读。常见的错误是吧_Inout_误用为_In_。 _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不能为NULL
} 复制代码 Visual Studio代码分析工具会验证函数传递一个非空指针给未初始化缓冲区指针pInt。在本例中,pInt不能为NULL。使用代码分析工具后,会出现:“warning : C6387: "pInt"可能是"0": 这不符合函数"InCallee"的规范。”
1.2.3 例子:_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; // 'pInt'解引用为空指针
}
void InOptCaller()
{
int *pInt = NULL;
GoodInOptCallee(pInt);
BadInOptCallee(pInt);
} 复制代码 Visual Studio代码分析工具会验证函数访问缓冲区时检查是否为NULL。使用代码分析工具后,会出现:“warning : C6011: 取消对 NULL 指针"pInt"的引用。”
1.2.4 例子:_Out_注释
_Out_用于一种常见场景:指向单元素缓冲区的非空指针作为参数传递,函数初始化其中的元素。调用者不需要再调用前初始化缓冲区,而规定被调用函数在返回前初始化该缓冲区。void GoodOutCallee(_Out_ int *pInt)
{
*pInt = 5;
}
void BadOutCallee(_Out_ int *pInt)
{
//没有在返回之前初始化pInt缓冲区!
}
void OutCaller()
{
int *pInt = new int;
GoodOutCallee(pInt);
BadOutCallee(pInt);
delete pInt;
} 复制代码 该处有汉化错误,应为:‘pInt’解引用为空指针,另外由于是opt的,因此警告可以忽略
Visual Studio代码分析工具会验证调用者是否传入一个指向缓冲区的非空指针pInt,且函数返回前对其进行初始化。使用代码分析工具后,会出现:“warning : C6101: 返回未初始化的内存"*pInt"。通过此函数的成功路径未设置命名的 _Out_ 参数。”
1.2.5 例子:_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; //‘pInt’解引用为空指针
}
void OutOptCaller()
{
int *pInt = NULL;
GoodOutOptCallee(pInt);
BadOutOptCallee(pInt);
} 复制代码 Visual Studio代码分析工具会验证函数中pInt解引用时上是否NULL值,如果pInt非空,再验证函数返回前是否进行了初始化。使用代码分析工具后,会出现:“warning : C6011: 取消对 NULL 指针"pInt"的引用。”
1.2.6 例子:_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'不能为NULL
} 复制代码 Visual Studio代码分析工具会验证调用者是否传递一个指向初始化过的缓冲区的非空指针pInt,并且在返回前pInt仍然非空且缓冲区经过初始化。使用代码分析工具后,会出现:“warning : C6387: "pInt"可能是"0": 这不符合函数"InOutCallee"的规范。”
1.2.7 例子:_Inout_opt_注释
_Inout_opt_和_Inout_类似,除了输入参数允许为零,因此函数需要在实现中进行相应检测。void GoodInOutOptCallee(_Inout_opt_ int *pInt)
{
if(pInt != NULL)
{
int i = *pInt;
*pInt = 6;
}
}
void BadInOutOptCallee(_Inout_opt_ int *pInt)
{
int i = *pInt; //'pInt'解引用为空指针
*pInt = 6;
}
void InOutOptCaller()
{
int *pInt = NULL;
GoodInOutOptCallee(pInt);
BadInOutOptCallee(pInt);
} 复制代码 Visual Studio代码分析工具会验证该函数在访问缓冲区前pInt是否为NULL,如果非空再检查函数返回前是否对其进行初始化。使用代码分析工具后,会出现:“warning : C6011: 取消对 NULL 指针"pInt"的引用。”
1.2.8 例子:_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;
//返回前没有初始化pInt
*pInt = pInt2;
}
void OutPtrCaller()
{
int *pInt = NULL;
GoodOutPtrCallee(&pInt);
BadOutPtrCallee(&pInt);
} 复制代码 Visual Studio代码分析工具会验证调用者是否传递了一个非空指针*pInt,且函数返回前是否对缓冲区进行初始化。使用代码分析工具后,会出现:“warning : C6101: 返回未初始化的内存"*pInt2"。通过此函数的成功路径未设置命名的 _Out_ 参数。”
1.2.9 例子:_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,同时函数返回时缓冲区是否经过初始化。使用代码分析工具后,会出现:“warning : C6011: 取消对 NULL 指针"pInt"的引用。”
1.2.10 例子:_Success_注释和_Out_注释混合
SAL注释可用于所有类型,甚至是整个函数,最显著的特性之一是注释函数返回成功或失败。而C/C++不能通过缓冲区及其大小的关系表示出函数成功或失败,而使用_Success_注释则可以实现。传递给_Success_注释的参数是一个表达式,用于标明什么情况下函数成功。该表达式可以为注释解析器可以理解的任何形式。经过注释,仅在函数成功后返回数据才有效。该例示例了_Success_如何和_Out_联用,可以使用关键字return代表返回值。_Success_(return != false)//也可写作_Success_(return)
bool GetValue(_Out_ int *pInt, bool flag)
{
if(flag)
{
*pInt = 5;
return true;
}
else
{
return false;
}
} 复制代码 _Out_注释会让Visual Studio代码分析工具验证调用者是否传递了一个指向缓冲区的非空指针pInt,并且函数返回前对其进行初始化。
1.3 SAL最佳实践
1.3.1 在现有代码中加入SAL注释
SAL是一个强有力的技术用于帮助改进代码的安全性和可靠性,在学了SAL之后可以将这种新技术应用到日常工作中。在新代码中可以使用基于SAL的规范进行全新设计;在旧代码中可以逐步加入注释,如此每次更新代码是都会受益。
微软公共头文件已经经过注释,因此我们建议在你的工程中首先注释那些叶节点函数和调用了Win32 API的函数,这样可以最大限度受益。
用VS2010测试该例未发现任何效果??
1.3.2 什么时候应该用注释?
下面是一些参考意见:
注释所有的指针类型参数。 通过注释有边界的变量,代码分析工具可以确保缓冲区和指针安全性。 注释加锁规则,避免产生意外情况,详见“注释加锁行为”一章。 注释驱动程序规则和其他规定域规则。
你还可以自定义注释所有参数,以达到更清楚地表达自己的意图,或者更容易检测代码缺陷的目的。
第二章 注释函数参数和返回值
本章描述了对于简单函数参数类型(标量 、指针、结构体和类)和几乎所有缓冲区类型的常见标注法。还展示了注释的常见使用规范。对于其他函数相关的注释类型,详见“注释函数行为”一章。标志总览:
_opt_:表示可选,即可以传递参数的常规值,也可输入零作为参数。
_z_:表示规则应用的参数数组末尾元素为零,通常用于字符串。
_read_:表示规则应用的参数数组会在函数中读取,通常传参前需要数组元素有效。
_write_:表示规则应用的参数数组会在函数中写入,通常传参前无需数组元素有效,在函数中需要对元素进行初始化。
_update_:表示规则应用的参数数组会在函数中读写,传参前和函数执行后都要保证数组元素有效。
_bytes_:该变体表示接受的注释参数为字节大小而非元素大小,通常用于参数数组大小无法通过元素大小推断时使用。
2.1 指针类型参数
如下表中的注释所示,在注释了一个指针类型参数后,若发现该指针为零,分析器会报告一个错误,这种行为对于任何数据项的指针都适用。
这里标量指除了指针类型的基本变量类型,包括int char short long double float等
注释 描述 _In_ 注释输入参数类型为标量、结构体、结构体指针等时使用,简单类型常常用到该注释。参数在传递前必须保证有效且在函数中不会被修改。例:char * __cdecl strchr(_In_z_ const char * _Str, _In_ int _Val); 复制代码 适用于注释非数组的单元素参数。 _Out_ 注释输出参数类型为标量、结构体、结构体指针等时使用,不能用于无法返回信息的对象——例如用值传递方式传递的标量,如:void func1(_Out_ int param){param=0;}
void func2(_Out_ int& param){param=0;} 复制代码 前者会报警告C6506: 无效的批注:“SAL_writableTo”属性(带元素计数)只能用于指针值、数组值或引用类型值: 函数“func1”_Param_(1),后者则无措。该参数在传递前不需要初始化,在退出函数前需要初始化。例:errno_t __cdecl _get_errno(_Out_ int * _Value); 复制代码 适用于注释非数组的单元素参数。 _Inout_ 注释的参数表明可能被函数修改。该参数必须在传递前(pre-state)和函数结束前(post-state)都必须保证有效(经过初始化),且一般这两种状态下含有的值不同。该参数必须为可修改类型。适用于注释输入和输出数据共用的参数。例:int __cdecl fputs(_In_z_ const char * _Str, _Inout_ FILE * _File); 复制代码 _In_z_ 以零字符‘\0’结尾的字符串指针作为输入参数时使用。要求字符串在传参前有效。PSTR的变体类型在声明中已经加入了完善的SAL注释,在使用时应优先考虑。适用于注释输入为只读字符串的参数。详细讨论见2.1.1节。实例:void CopyStr( _In_z_ const char* szFrom,
_Out_z_cap_(cchTo) char* szTo,
size_t cchTo ); 复制代码 _Inout_z_ 以零字符‘\0’结尾且用于修改的字符串指针作为输入参数时使用。该参数在传参前和函数结束前都保证有效,然而所指向的数据应该在函数中修改。零字符终止符可以被移动,同时只应该访问终止符之前的那些元素。适用于注释输入和输出字符串共用的参数。实例:void toupper( _Inout_z_ char* sz ); 复制代码 _In_reads_(s)
_In_reads_bytes_(s) 用于数组指针参数,且函数会读取数组数据。需要给定参数s为数组元素,且每个元素传参前保证有效。另一种_bytes_变体则需要给定整个数组所占字节而不是元素个数,只在数组大小不能通过元素大小得知时使用(长度未知)。如果已知数组大小,那么它等于s乘以元素大小。后面注释的_bytes_变体类似。例如,同样功能的函数,如果wchar_t类型参数版本使用了_bytes_变体形式则char版本的函数也要使用_bytes_变体形式。适用于注释输入为已知大小只读数组或字符串的参数。详细讨论见2.1.2节。实例:int __cdecl _snwscanf_s(
_In_reads_(_MaxCount) _Pre_z_ const wchar_t * _Src,
_In_ size_t _MaxCount,
_In_z_ _Scanf_s_format_string_ const wchar_t * _Format,
...);
int __cdecl memcmp(
_In_reads_bytes_(_Size) const void * _Buf1,
_In_reads_bytes_(_Size) const void * _Buf2,
_In_ size_t _Size); 复制代码 _In_reads_z_(s) 用于终止元素为零且数组大小已知的数组指针参数,函数最多能访问到第s个元素,且要求每个元素在传参前保证有效。适用于注释输入为只读的、元素个数已知的且末尾元素为零的数组或字符串参数。详细讨论见2.1.3节。 _In_reads_or_z_(s) 用于终止元素为零或数组大小已知的数组指针参数,函数最多能访问到第s个元素,且要求每个元素在传参前保证有效。适用于strn-系(例如strncpy)形式类似函数,即注释输入为只读的、元素个数已知或末尾元素为零的数组或字符串参数。例:char * __CRTDECL strncpy(
_Out_writes_z_(_Size) char (&_Dest)[_Size],
_In_reads_or_z_(_Count) const char * _Source,
_In_ size_t _Count); 复制代码 _Out_writes_(s)
_Out_writes_bytes_(s) 注释用于写入的s个元素的数组指针参数。元素不需要传参前有效,同时函数返回前经过初始化的元素个数无需指定。该注释应用于函数返回前状态。适用于注释用于输出的数组或字符串参数。详细讨论见2.1.4节。例:int __cdecl _snprintf_c(
_Out_writes_(_MaxCount) char * _DstBuf,
_In_ size_t _MaxCount,
_In_z_ _Printf_format_string_ const char * _Format, ...);
size_t __cdecl fread(
_Out_writes_bytes_(_ElementSize*_Count) void * _DstBuf,
_In_ size_t _ElementSize,
_In_ size_t _Count,
_Inout_ FILE * _File); 复制代码 _Out_writes_z_(s) 注释用于写入的s个元素的数组指针参数。零终止符之前的元素在传参前无需有效,在函数返回前必须存在且有效。适用于注释用于输出的数组或字符串参数。例:errno_t __cdecl wcscpy_s(
_Out_writes_z_(_DstSize) wchar_t * _Dst,
_In_ rsize_t _DstSize,
_In_z_ const wchar_t * _Src); 复制代码 _Inout_updates_(s)
_Inout_updates_bytes_(s) 注释指向用于读写的s个元素的数组指针参数。元素在传参前和函数返回前都必须有效。适用于注释用于更新的数据。例:LWSTDAPI_(BOOL) PathQuoteSpacesA(
_Inout_updates_(MAX_PATH) LPSTR lpsz);
void __cdecl qsort_s(
_Inout_updates_bytes_(_NumOfElements* _SizeOfElements) void * _Base,
_In_ rsize_t _NumOfElements,
_In_ rsize_t _SizeOfElements,
_In_ int (__cdecl * _PtFuncCompare)(void *, const void *, const void *),
void *_Context); 复制代码 _Inout_updates_z_(s) 注释指向末尾元素为零的数组指针参数。零终止符之前的元素必须在传参前和函数返回前存在且有效,且传参后的值应该和函数返回前不同,包括零终止符的位置。适用于注释用于更新的末尾元素为零的数组或字符串。例:errno_t __cdecl strncat_s(
_Inout_updates_z_(_SizeInBytes) char * _Dst,
_In_ rsize_t _SizeInBytes,
_In_reads_or_z_(_MaxCount) const char * _Src,
_In_ rsize_t _MaxCount); 复制代码 _Out_writes_to_(s,c)
_Out_writes_bytes_to_(s,c)
_Out_writes_all_(s)
_Out_writes_bytes_all_(s) 注释s个元素的数组指针参数。元素无需在调用前有效,在函数返回前前c个元素必须有效。适用于注释用于输出的数组或字符串参数,为_Out_write_的可控数量版本。例:STDAPI_(DWORD) SHGetAssocKeys(
_In_ IQueryAssociations *pqa,
_Out_writes_to_(cKeys, return) HKEY *rgKeys,
DWORD cKeys);
wchar_t * __CRTDECL wmemcpy(
_Out_writes_all_(_N) wchar_t *_S1,
_In_reads_(_N) const wchar_t *_S2,
_In_ size_t _N)
void * __cdecl memcpy(
_Out_writes_bytes_all_(_Size) void * _Dst,
_In_reads_bytes_(_Size) const void * _Src,
_In_ size_t _Size); 复制代码 _Inout_updates_to_(s,c)
_Inout_updates_bytes_to_(s,c) 注释s个元素的数组指针参数,函数会读写数组元素,元素在传参前必须有效,同时前c个元素在函数返回前必须有效。适用于注释用于更新的数据,为_Inout_update_的可控数量版。 _Inout_updates_all_(s)
_Inout_updates_bytes_all_(s) 注释s个元素的数组指针参数,函数会读写数组元素,所有元素在传参前和函数返回前都必须有效。 _In_reads_to_ptr_(p) 注释数组指针参数。p指向元素之前的元素必须在传参前有效。p - _Curr_由语言标准所定义。 _In_reads_to_ptr_z_(p) 注释零终止符作为末尾元素的数组指针参数。p指向元素之前的元素必须在传参前有效。p - _Curr_由语言标准所定义。 _Out_writes_to_ptr_(p) 注释数组指针参数。p指向元素之前的元素无须传参前有效,而在函数返回前必须有效。p - _Curr_由语言标准所定义。 _Out_writes_to_ptr_z_(p) 注释零终止符作为末尾元素的数组指针参数。p指向元素之前的元素无须传参前有效,而在函数返回前必须有效。p - _Curr_由语言标准所定义。
2.1.1 _In_z_注释的详细讨论
测试代码:#include<sal.h>
void func1(_In_z_ int param){}
void main()
{
int arr;
func1(arr);
} 复制代码 使用源码分析工具分析会得到两个警告:
1)warning : C6510: 无效的批注:"NullTerminated"属性只可在元素类型为整型或指针类型的缓冲区上使用: 函数"func1"_Param_(1)。
2)使用未初始化的内存"arr"。
错误1是说修饰的参数需要是指针类型,可以是指针数组也可以是整形(char, int, short, long, …)数组或指针数组。
改成如下代码:void func1(_In_z_ int* param){}
void main()
{
int arr[2];
func1(arr);
} 复制代码 会得到两个警告:
1)warning : C6054: 可能没有为字符串"arr"添加字符串零终止符。
2)warning : C6001: 使用未初始化的内存"arr"。
错误1是说,无论该参数是整数类型的哪一类,必须是零终止符'\0'(也就是0)元素结尾,也就是最后一个元素为零;对于字符串,编译器存储时是自动末尾补'\0'的,其他整形类型,就需要手动补零。
改成如下代码:int arr[2]={3};
func1(arr); 复制代码 由于只对第一个元素进行了赋值,因此仍然有警告1)出现。
再次改成如下代码:int arr[2]={3,1};
func1(arr); 复制代码 仍会得到警告1),因此需要让编译器对arr的最后一个元素赋值为0才能消除警告。
_In_z_和_Inout_z_常用于字符串参数的注释。
下面是PSTR系类型在WDK8的winnt.h中定义,其中_Null_terminated_是SAL注释,这也意味着字符串类型完全可以使用以下包含SAL注释的类型,而不需要自己手动写注释。//
// ANSI (Multi-byte Character) types
//
typedef CHAR *PCHAR, *LPCH, *PCH;
typedef CONST CHAR *LPCCH, *PCCH;
typedef _Null_terminated_ CHAR *NPSTR, *LPSTR, *PSTR;
typedef _Null_terminated_ PSTR *PZPSTR;
typedef _Null_terminated_ CONST PSTR *PCZPSTR;
typedef _Null_terminated_ CONST CHAR *LPCSTR, *PCSTR;
typedef _Null_terminated_ PCSTR *PZPCSTR;
typedef _NullNull_terminated_ CHAR *PZZSTR;
typedef _NullNull_terminated_ CONST CHAR *PCZZSTR;
typedef CHAR *PNZCH;
typedef CONST CHAR *PCNZCH;
//
// Neutral ANSI/UNICODE types and macros
//
#ifdef UNICODE // r_winnt
#ifndef _TCHAR_DEFINED
typedef WCHAR TCHAR, *PTCHAR;
typedef WCHAR TBYTE , *PTBYTE ;
#define _TCHAR_DEFINED
#endif /* !_TCHAR_DEFINED */
typedef LPWCH LPTCH, PTCH;
typedef LPCWCH LPCTCH, PCTCH;
typedef LPWSTR PTSTR, LPTSTR;
typedef LPCWSTR PCTSTR, LPCTSTR;
typedef LPUWSTR PUTSTR, LPUTSTR;
typedef LPCUWSTR PCUTSTR, LPCUTSTR;
typedef LPWSTR LP;
typedef PZZWSTR PZZTSTR;
typedef PCZZWSTR PCZZTSTR;
typedef PUZZWSTR PUZZTSTR;
typedef PCUZZWSTR PCUZZTSTR;
typedef PZPWSTR PZPTSTR;
typedef PNZWCH PNZTCH;
typedef PCNZWCH PCNZTCH;
typedef PUNZWCH PUNZTCH;
typedef PCUNZWCH PCUNZTCH;
#define __TEXT(quote) L##quote // r_winnt
#else /* UNICODE */ // r_winnt
#ifndef _TCHAR_DEFINED
typedef char TCHAR, *PTCHAR;
typedef unsigned char TBYTE , *PTBYTE ;
#define _TCHAR_DEFINED
#endif /* !_TCHAR_DEFINED */
typedef LPCH LPTCH, PTCH;
typedef LPCCH LPCTCH, PCTCH;
typedef LPSTR PTSTR, LPTSTR, PUTSTR, LPUTSTR;
typedef LPCSTR PCTSTR, LPCTSTR, PCUTSTR, LPCUTSTR;
typedef PZZSTR PZZTSTR, PUZZTSTR;
typedef PCZZSTR PCZZTSTR, PCUZZTSTR;
typedef PZPSTR PZPTSTR;
typedef PNZCH PNZTCH, PUNZTCH;
typedef PCNZCH PCNZTCH, PCUNZTCH; 复制代码
2.1.2 _In_reads_(s)和_In_reads_bytes_(s)注释的详细讨论 #include<sal.h>
void func1(_In_reads_(10) wchar_t* param){}
void main()
{
func1(L"123");
} 复制代码 执行代码分析后会得到警告:C6385: 从“"123"”中读取的数据无效: 可读大小为“8”个字节,但可能读取了“20”个字节。该警告的含义是,源串“123”包含空间为4*sizeof(wchar_t)=8字节,其中包含终止符L’\0’,而函数声明却支持10*sizeof(wchar_t)字节大小的数组。不能容许函数定义的数组大小大于给定的数组大小,否则可能会造成溢出。
改为如下代码:wchar_t chs[5];
func1(chs); 复制代码 会产生两个警告:
1)warning : C6385: 从"chs"中读取的数据无效: 可读大小为"10"个字节,但可能读取了"20"个字节。(该警告解释同上)
2)C6001: 使用未初始化的内存"chs"。
对于_bytes_版本的注释类似,不再赘述。
2.1.3 _In_reads_z_(s)注释的详细讨论 #include<sal.h>
void func1(_In_reads_z_(10) wchar_t* param){}
void main()
{
wchar_t chs[10];
func1(chs);
} 复制代码 使用代码分析工具会产生两个警告:
1)warning : C6054: 可能没有为字符串"chs"添加字符串零终止符。
2)warning : C6001: 使用未初始化的内存"chs"。
2.1.4 _Out_writes_(s)和_Out_writes_bytes_(s)
考虑下面的代码:typedef _Null_terminated_ wchar_t *PWSTR;
void MyStringCopy(_Out_writes_ (size) PWSTR p1, _In_ size_t size, _In_ PWSTR p2); 复制代码 在该例中,调用者提供已知元素个数的数组p1,MyStringCopy的作用是初始化一些元素使它们有效。PWSTR的_Null_terminated_注释表明p1在函数返回前需要初始化为零字符结尾。这种情况下虽然可以定义有效元素个数,但是并不需要提供该信息。
In this example, the caller provides a buffer of size elements for p1. MyStringCopy makes some of those elements valid. More importantly, the _Null_terminated_ annotation on PWSTR means that p1 is null-terminated in post-state. In this way, the number of valid elements is still well-defined, but a specific element count is not required.
2.2 可选指针类型参数
指针类型注释用_opt_表示该参数可能为零,其他方面和不加_opt_相同,下面是指针变量参数中可能存在的_opt_变种形式:_In_opt_
_Out_opt_
_Inout_opt_
_In_opt_z_
_Inout_opt_z_
_In_reads_opt_
_In_reads_bytes_opt_
_In_reads_opt_z_ _Out_writes_opt_
_Out_writes_opt_z_
_Inout_updates_opt_
_Inout_updates_bytes_opt_
_Inout_updates_opt_z_
_Out_writes_to_opt_
_Out_writes_bytes_to_opt_
_Out_writes_all_opt_
_Out_writes_bytes_all_opt_ _Inout_updates_to_opt_
_Inout_updates_bytes_to_opt_
_Inout_updates_all_opt_
_Inout_updates_bytes_all_opt_
_In_reads_to_ptr_opt_
_In_reads_to_ptr_opt_z_
_Out_writes_to_ptr_opt_
_Out_writes_to_ptr_opt_z_
2.3 输出指针类型参数
输出指针类型参数需要进行特殊注释以区分参数和指向位置是否为空。注意:函数返回结果和函数返回值是完全不同的概念,后者仅仅是return sth.,而前者是函数为了实现某功能在某缓冲区写入的数据结果,可能为返回值,也可能利用输入参数进行返回,如下面的fopen_s函数所示。 Annotation Description _Outptr_ 参数不能为零,并且指向位置在函数返回前不能为空且需要有效,用于指针类型。 _Outptr_opt_ 参数可以为零,并且指向位置在函数返回前不能为空且需要有效 _Outptr_result_maybenull_ 参数不能为零,并且指向位置在函数返回前可能为空,例:
errno_t __cdecl fopen_s(
_Outptr_result_maybenull_ FILE ** _File,
_In_z_ const char * _Filename,
_In_z_ const char * _Mode); _Outptr_opt_result_maybenull_ 参数可以为零,并且指向位置在函数返回前可能为空
下面的这个表中展示了其他标志字串所代表的含义,常见的有:_z_,_COM_,_buffer_,_bytebuffer_和_to_。如果注释的是com接口组件就要使用com格式的注释,同时不能将com格式的注释用于其他类型接口。Annotation Description _Outptr_result_z_
_Outptr_opt_result_z_
_Outptr_result_maybenull_z_
_Ouptr_opt_result_maybenull_z_ 代表返回指针指向的是是零终止符结束的缓冲区。 _COM_Outptr_
_COM_Outptr_opt_
_COM_Outptr_result_maybenull_
_COM_Outptr_opt_result_maybenull_ 代表返回指针为com组件类型,如果带有_On_failure_后置条件,则返回值为空。 _Outptr_result_buffer_(s)
_Outptr_result_bytebuffer_(s)
_Outptr_opt_result_buffer_(s)
_Outptr_opt_result_bytebuffer_(s) 代表返回指针指向有s个有效元素(字节)的数组。 _Outptr_result_buffer_to_(s, c)
_Outptr_result_bytebuffer_to_(s, c)
_Outptr_opt_result_buffer_to_(s,c)
_Outptr_opt_result_bytebuffer_to_(s,c) 代表返回指针指向有s个元素(字节),前c个有效的数组。
函数调用失败后,某些接口约定会认为其输出参数无效。com代码与此不同,下表中的注释形式需要优先使用。该使用哪种形式的com注释请参照上一节。Annotation Description _Result_nullonfailure_ 修改其他注释。如果函数失败,返回结果设置为空。 _Result_zeroonfailure_ 修改其他注释。如果函数失败,返回结果设置为零。 _Outptr_result_nullonfailure_ 如果函数成功则返回指针指向有效缓冲区,否则设置为空。该注释用于非可选类型参数。 _Outptr_opt_result_nullonfailure_ 如果函数成功则返回指针指向有效缓冲区,否则设置为空。该注释用于可选类型参数。 _Outref_result_nullonfailure_ 如果函数成功则返回指针指向有效缓冲区,否则设置为空。该注释用于引用类型参数。
2.4 输出引用类型参数
引用参数常用作输出参数,一个简单的输出引用参数例子是int&,使用_Out_注释。如果输出值为指针,如int*&,则等价的注释应该为_Outptr_ int**,然而该用法是不正规的。为了恰当表达输出指针引用类型参数的语法,需要使用下面的注释形式:Annotation Description _Outref_ 返回结果在函数返回前必须有效且不为空。用于指针的引用类型。 _Outref_result_maybenull_ 返回结果在函数返回前必须有效且可能为空。 _Outref_result_buffer_(s) 返回结果在函数返回前必须有效且不为空,指向s个元素的有效缓冲区。 _Outref_result_bytebuffer_(s) 返回结果在函数返回前必须有效且不为空,指向s个字节的有效缓冲区。 _Outref_result_buffer_to_(s, c) 返回结果在函数返回前必须有效且不为空,指向s个元素的缓冲区,其中前c个元素是有效的。 _Outref_result_bytebuffer_to_(s, c) 返回结果在函数返回前必须有效且不为空,指向s个字节的缓冲区,其中前c个字节是有效的。 _Outref_result_buffer_all_(s) 返回结果在函数返回前必须有效且不为空,指向s个有效元素的缓冲区。 _Outref_result_bytebuffer_all_(s) 返回结果在函数返回前必须有效且不为空,指向s个有效字节的缓冲区。 _Outref_result_buffer_maybenull_(s) 返回结果在函数返回前必须有效且可能为空,指向s个元素的有效缓冲区。 _Outref_result_bytebuffer_maybenull_(s) 返回结果在函数返回前必须有效且可能为空,指向s个字节的有效缓冲区。 _Outref_result_buffer_to_maybenull_(s, c) 返回结果在函数返回前必须有效且可能为空,指向s个元素的缓冲区,其中前c个元素是有效的。 _Outref_result_bytebuffer_to_maybenull_(s,c) 返回结果在函数返回前必须有效且可能为空,指向s个字节的缓冲区,其中前c个字节是有效的。 _Outref_result_buffer_all_maybenull_(s) 返回结果在函数返回前必须有效且可能为空,指向s个有效元素的缓冲区。 _Outref_result_bytebuffer_all_maybenull_(s) 返回结果在函数返回前必须有效且可能为空,指向s个有效字节的缓冲区。
2.5 返回值
函数的返回值,类似于_Out_注释,但是和解引用又不同,它无需考虑指针的概念。下面列出了用于注释返回值的关键字,注释对象包括标量、结构体指针、缓冲区指针等。这些注释和_Out_注释语法类似。_Ret_z_
_Ret_writes_(s)
_Ret_writes_bytes_(s)
_Ret_writes_z_(s)
_Ret_writes_to_(s,c)
_Ret_writes_maybenull_(s)
_Ret_writes_to_maybenull_(s)
_Ret_writes_maybenull_z_(s) _Ret_maybenull_
_Ret_maybenull_z_
_Ret_null_
_Ret_notnull_
_Ret_writes_bytes_to_
_Ret_writes_bytes_maybenull_
_Ret_writes_bytes_to_maybenull_
2.6 其他常见注释
Annotation Description _In_range_(low, hi)
_Out_range_(low, hi)
_Ret_range_(low, hi)
_Deref_in_range_(low, hi)
_Deref_out_range_(low, hi)
_Deref_inout_range_(low, hi)
_Field_range_(low, hi)
用于注释参数、域或返回结果的范围(从low值到hi值)。等价于注释_Satisfies_(_Curr_ >= low && _Curr_ <= hi),和其他注释混用。注意:该注释虽然包含in和out串,但是并不代表_In_和_Out_注释。 _Pre_equal_to_(expr)
_Post_equal_to_(expr)
用于注释当前值是否等于表达式expr.,等价于_Satisfies_(_Curr_ == expr),和其他注释混用。 _Struct_size_bytes_(size) 用于注释结构体和类声明,用于表示一个有效对象可能比声明的类型所占字节更多,所占字节数由size给出,例如:
typedef _Struct_size_bytes_(nSize)
struct MyStruct
{
size_t nSize;
...
}; 复制代码 那么一个MyStruct*类型的参数pM的缓冲区大小会被当做:min(pM->nSize, sizeof(MyStruct)); 复制代码
注意:所有SAL注释是为了更清楚的说明一些属性,编译器只会固定检查某些方面,并不是上述所有要求都进行检测,所以代码的维护和正确使用还是要靠留心这些注释。