检查格式说明符

[原文发表地址] 检查格式说明符

[原文发表时间] 2015/6/22 5:47PM

征询公众的要求, 我们已经在Visual Studio 2015 RTM 中 实现了关于printf/scanf 函数以及C 标准库中其他变体的相关传入参数的检查。您可以在我们的在线编译器中进行尝试。

摘要

在下面的列表中包含了我们引入的所有关于格式类型的警告:

状 态

等 级

序 号

内 容

打开

W1

C4473

'<function>' : 对于要求的格式化字符串而言,参数个数不够

打开

W3

C4474

'<function>' : 对于要求的格式化字符串而言,参数个数过多

打开

W3

C4475

'<function>' : 在格式说明符里面,不能用在类型字段为'<conversion-specifier>'中使用长度修饰符'<length>'

打开

W3

C4476

'<function>' : 在格式说明符里面未知的类型字段为字符的'<conversion-specifier>

打开

W1

C4477

'<function>' : 格式字符串'<format-string>要求类型为'<type的参数,但是variadic  参数<position>有类型'<type>'

打开

W1

C4478

'<function>' : 在同一个格式字符串里面不能混合使用定位的和没有定位的位置标识符

关闭

W4

C4774

'<function>' : 格式字符串在参数<position>中并不需要一个字符串值

打开

W3

C4775

'<function>' : 在函数'<function>'的格式字符串<format-string>使用了非标准的扩展

打开

W1

C4776

'<function>' : 在函数'<function>'的格式字符串中不允许使用‘%<conversion-specifier>

关闭

W4

C4777

'<function>' : 格式字符串'<format-string>要求类型为'<type的参数,但是variadic  参数<position>有类型'<type>'

打开

W3

C4778

'<function>' : 没有结束的格式字符串'<format-string>'

举例1

思考下面的代码片段

wchar_t const * str = ...;// Some string to parse

char buf[10];

wchar_t wbf;

swscanf_s(str, L"%10c %1C", buf, sizeof(buf), &wbf);

 

用cl.exe 编译 并且没有任何额外的标识(默认的警告等级是1)的情况下将会出现三个警告(用特殊的颜色进行区分)。

warning C4477 : 'swscanf_s' : format string '%10c' requires an argument of type 'wchar_t *', but variadic argument 1 has type 'char *'
note: this argument is used by a conversion specifier
note: consider using '%hc' in the format string
note: consider using '%Tc' in the format string
note: consider defining _CRT_STDIO_ISO_WIDE_SPECIFIERS if C99 standard semantics is required

 

第一个警告表明对swscanf_s的调用实参类型与期望的参数类型不匹配。请注意这些相同的实参或许是适用于那些你曾经使用过的不同函数(比如sscanf_s)的格式说明符的。这就是为什么我们要在新引入的警告信息中包含这些函数的原因。如果这些类型转换符结合不同的长度修饰符可以和实际的参数匹配, 我们将会列举这些所有的集合作为建议供您参考。请注意因为类型转换标识符或者参数的类型是需要改变的,因此下面的这些建议或许是不正确的。我们也不会建议其他的类型转换标识符,因为改变类型标识符本身常常会引起语义更改,而这些是需要建立在对程序逻辑结构深刻理解的基础上。

我们把涉及到参数的位置作为相对于variadic 参数开头的参考,而不是相对于所有的参数的开头。我们想要这种编号和 _p functions 定位过的参数中的编号保持一致,因为variadic 参数常常会遵循格式化字符串(除过在_l functions情况之外),这也使得我们的计划实施起来更加容易一些。

warning C4477: 'swscanf_s' : format string '%1C' requires an argument of type 'char *', but variadic argument 3 has type 'wchar_t *'
note: this argument is used by a conversion specifier
note: consider using '%lC' in the format string
note: consider using '%llC' in the format string
note: consider using '%LC' in the format string
note: consider using '%wC' in the format string

在编译期间如果你用/Za 去关闭Microsoft extensions,你将会观察到有关使用非标准的格式说明符(用上边的灰颜色标注的)的建议将不再出现,与此同时将会得到一个新的警告:

warning C4775 : nonstandard extension used in format string '%1C' of function 'swscanf_s'
note: the combination of length modifier '' with type field character 'C' is non standard

 

如果你在64 位机器上进行编译,并且size_t 被定义为typedef unsigned __int64 size_t,你也会得到如下信息:

warning C4477: 'swscanf_s' : format string '%10c' requires an argument of type 'int', but variadic argument 2 has type 'size_t'
note: this argument is used as a buffer size

 

在这儿的问题是缓冲区 size的大小期望是int 类型的大小,也就是在x64 上占4个字节,但是实际的参数是size_t的类型,也就是在x64 上占用8个字节。在一些情况下,你也会在x86上得到这样的警告,但是在默认情况下这种警告是被关闭的,你需要重新启用它。这样做了之后,当你用/w14777 或者/Wall去编译的时候,不幸地是,C4477在这种特殊的情况下将不会起到任何作用。因为这个值将会被声明为type std::size_t去侦测潜在的问题,然而在这表达式sizeof(buf)的类型是未命名的类型,而且它通常的隐含类型是size_t。

基于这个函数, 一些格式说明符可以从栈中提取多达3个参数。正因为如此, 我们首先感到困惑的是出现的警告表明1个格式说明符%s 期望一个int 类型的参数。为了减少这样的困惑,在上面和下面的红颜色标注的例子中我们特意添加了一些notes说明在指定的环境下才可以使用这样的参数的说明。这些notes 说明了这个转换标识符本身用这个参数来作为一个要求的缓冲区size 或者是一个更宽或者更精准的范畴。

warning C4473 : 'swscanf_s' : not enough arguments passed for format string
note: placeholders and their parameters expect 4 variadic arguments, but 3 were provided
note: the missing variadic argument 4 is required by format string '%1C'
note: this argument is used as a buffer size

 

忽略的variadic参数是和不正确的类型一样是一个需要重大的安全关切问题,因为他们或许会引起你的程序从堆栈中读取一些垃圾数据。为了显示出这些严重的问题,我们创建的作为等级1的C4473 警告。

请注意在Visual Studio 2015更早的一些版本中, C4422 是覆盖C4474 的,C4426是覆盖C4476的。而C4473 常常用C4317来代替 。请注意如果在你的程序 中或者搭建的脚本中正在用旧的编号来禁止或者压制这样的警告,请确保更新到新的编号上。

举例2

请思考我们在”real world “代码中发现的另一个简单的例子:

const char* path= "PATH=%WindowsSdkDir%bin\\%_ARCH% ;%PATH%";

printf_s(path);

 

用默认的flags 来编译这段代码将不会产生有任何针对性的信息,虽然这给你带来虚假的安全感,但是你也很明晰的看到这儿的问题。然而不幸的是,当格式化字符串不是字符串的值,通常给出的警告往往是在有效的使用情形下产生太多的警告, 因此作为一个承诺我们决定在默认情况下关闭这些警告。为了启动它,你可以通过/w14774 or /Wall来编译,然后你将会得到如下警告:

 

warning C4774 : 'printf_s' : format string expected in argument 1 is not a string literal
note: e.g. instead of printf(name); use printf("%s", name); because format specifiers in 'name' may pose a security issue
note: consider using constexpr specifier for named string literals

 

我们推荐在至少在偶然情况下去启动这个警告去侦测到因为non-literal的格式化字符串而导致实际上并没有进行格式化检查的这些地方。 这儿的重要信息是第二条note, 它建议你用敞亮表达式去代替常量。做这些可以允许我们在编译期间去评估‘path’从而可以站在它使用的角度上去执行格式检查。

warning C4476 : 'printf_s' : unknown type field character 'W' in format specifier
warning C4476: 'printf_s' : unknown type field character 'b' in format specifier
warning C4476: 'printf_s' : unknown type field character '_' in format specifier
warning C4476: 'printf_s' : unknown type field character ';' in format specifier
warning C4476: 'printf_s' : unknown type field character 'P' in format specifier
warning C4778: 'printf_s' : unterminated format string '%'

 

这儿的解决方法是简单地用%%来代替%, 但是令人奇怪的是,我们在实际的代码中已经看到过听太多关于这个bug发生的事情。一旦你意识到‘ ’空格是一个有效的printf flag(请看上面关于”;”警告的第四条警告),这个问题经会变得更加的严谨。

举例3

请思考另一个例子

 

struct HTMLElement

{

    const char* tag;

    virtual std::string content() const = 0;

};

 

int n;

HTMLElement* elem = ...;

_tprintf_p(_T("<%hhs>%hhs%n</%1$hhs>"), elem->tag, elem->content().c_str(), &n);

 

这段代码主要在试着打印html 页面的内容,与此同时在打印关闭标签之前统计出我们打印的字符总数。通过涉及到的第一个参数elem->tag的位置格式说明符(“%1$hhs”)来打印关闭标签的。既然我们在a _t 函数中,我们要试着通过写入“hh”来确保把%s 当作一个窄字符串,我们得到的如下所示:

warning C4475 : '_printf_p' : length modifier 'hh' cannot be used with type field character 's' in format specifier
warning C4776: '%n' is not allowed in the format string of function '_printf_p'
warning C4478: '_printf_p' : positional and non-positional placeholders cannot be mixed in the same format string

 

这第一个警告是说“hh”对%s而言不是一个有效的长度修饰符。第二个警告告诉你在这个函数中不允许%n. 最后一条警告提示你不能混合位置参数和非位置参数。

举例4

当我们测试这些警告的时候,我们注意到许多的开发者用下面的代码去打印一个指针类型的值:

 

const char* ptr = ...; // Some pointer

printf("%08X", ptr);

 

我们不确定为什么用这个标准的%p 格式标识符是普遍的,但是正因为他这样的普遍以致于我们有必要去详细的阐述它。在x86机器上编译下面的代码将会给出你如下的警告:

warning C4477: 'printf' : format string '%08X' requires an argument of type 'unsigned int', but variadic argument 1 has type 'const char *'

 

在x64 上编译将会附加地产生如下:

warning C4313: 'printf': '%X' in format string conflicts with argument 1 of type 'const char *'

 

警告C4313是一个存在中的警告,他用来侦测整型/指针型 大小不匹配问题。我们可以在x86 上通过转化指针型到和指针类型相同大小的整型来消除这种警告:

printf("%08X", reinterpret_cast<intptr_t>(ptr));

 但是这种方法在x64 上将不会起作用,因为那里的intptr_t 是8字节,而unsigned int 是4个字节。

 

warning C4477: 'printf' : format string '%08X' requires an argument of type 'unsigned int', but variadic argument 1 has type 'intptr_t'
note: consider using '%IX' in the format string

 

使用这种方法也会导致在x64 上同%p打印出一些被截断的值 比如说9515CED0 而不是000000E29515CED0.

为了在x86 上得到类似潜在的截断的值相关的警告,你需要显式激活默认关闭的警告C4477(比如在编译行中添加/w14777):

 

warning C4777 : 'printf' : format string '%08X' requires an argument of type 'unsigned int', but variadic argument 1 has type 'intptr_t'
note: the sizes of types 'intptr_t' and 'unsigned int' might differ on other platforms
note: consider using '%IX' in the format string

 

警告C4777的内容是和C4477 完全相同的。但是当期望类型以及实际类型是和目标平台相关联的这种混乱的语境下,例如,int vs. long or double vs long double在许多微软作为目前平台的架构上是一套完全相同的值的集合,同时在技术上是以不同的类型搭建的。在测试的过程中, 我们发现诸如此类的不匹配的数量相对于更加严重的不匹配问题的数量而言,是非常高,其比例可达到10:1. 因此我们决定把这两种情况区分开来。在默认的情况下关闭这种混论的事件。

下面note 的建议是用“I”长度修饰符

 

printf("%08IX\n", reinterpret_cast<intptr_t>(ptr));

对于上面的指针而言产生一个正确的值E29515CED0, 但是这个输出却不是以0为扩展来显示x64 上有的更大数量的位数。为了避免这个,我们必须用宽字节范畴来打印:

const size_t MACH_PTR_SIZE = sizeof(void*);

printf("%0*IX\n", 2*MACH_PTR_SIZE, reinterpret_cast<intptr_t>(ptr));

 

令人惊奇的是却出现了另一个警告(在x86 上模糊不清的C4777 和x64 上精准的C4477):

 

warning C4777: 'printf' : format string '%0*IX' requires an argument of type 'int', but variadic argument 1 has type 'size_t'
note: this argument is used as a field width
note: the sizes of types 'size_t' and 'int' might differ on other platforms

 

这表明字段宽度必须是int类型的,而不是size_t。做如下的修改:

printf("%0*IX == %p\n", int(2*MACH_PTR_SIZE), reinterpret_cast<intptr_t>(ptr), ptr);

最终我们解决了所有的警告,并且在x86 和x64 上用同一个方法%p (在我们的实现过程中)打印出这个指针。如果你更喜欢标准长度修饰符的话,你可以用%tX 和 ptrdiff_来代替%IX 和intptr_t

 

接下来呢?

目前,格式说明符的检查仅仅是CRT函数预先定义的一个集合,对用户定义的函数而言还不是有效的,但是对于相似的检查还是有意义的。如果有足够的兴趣,我们将会考虑拓展这些警告到用户定义的函数上。我们也非常乐意听到你希望编译器去侦测到关于printf/scanf函数集的其他的bug,请随心所欲发邮件给我(来自微软的yuriysol)或者在这篇文章的后面进行评论并且提供你想到的任何反馈意见。谢谢!