为什么参数与printf未定义的行为中的转换说明符不匹配?

时间:2015-11-11 10:27:06

标签: c++ c printf undefined-behavior

在C(n1570 7.21.6.1/10)和C ++(通过包含C标准库)中,未定义的行为是为printf提供其类型与其转换规范不匹配的参数。一个简单的例子:

printf("%d", 1.9)

格式字符串指定int,而参数是浮点类型。

这个问题的灵感来自一个用户遇到遗留代码的问题,这些代码存在大量的转换不匹配,这显然没有任何损害,参见undefined behaviour in theory and in practice

声称格式不匹配UB起初看起来很激烈。很明显,输出可能是错误的,这取决于精确的不匹配,参数类型,字节序,可能的堆栈布局和其他问题。正如一位评论员指出的那样,这也延伸到后来的(甚至是之前的?)论点。但这远不是一般的UB。就个人而言,除了预期的错误输出外,我从未遇到任何其他问题。

冒险猜测,我会排除对齐问题。我能想象的是,提供一个格式字符串,使printf期望大数据与小的实际参数一起使printf读取超出栈,但我对var args机制和特定printf实现细节缺乏深入的了解以验证那。

我快速浏览了printf sources,但对于随意的读者来说,它们是非常不透明的。

因此,我的问题是:printf中错误匹配转换说明符和参数的具体危险是什么,使其成为UB?

5 个答案:

答案 0 :(得分:11)

printf只有在您正确使用标准时才能按标准描述。如果使用不正确,则行为未定义。为什么标准定义了当你使用它时会发生什么?

具体来说,在某些体系结构中,浮点参数在不同的寄存器中传递给整数参数,因此在printf内部,当它试图找到与格式说明符匹配的int时,它会在相应的寄存器中找到垃圾。由于这些细节超出了标准的范围,因此没有办法处理这种不当行为,除非说它未定义。

有关错误可能出错的示例,使用格式说明符"%p"但传递浮点类型可能意味着printf尝试从寄存器或堆栈位置读取指针尚未设置为有效值,并且可能包含陷阱表示,这将导致程序中止。

答案 1 :(得分:3)

举个例子:假设您的体系结构的过程调用标准说浮点参数在浮点寄存器中传递。但是printf认为你传递了一个整数,因为%d格式说明符。所以它期望在调用堆栈上有一个参数,它不存在。现在一切都会发生。

答案 2 :(得分:3)

任何printf格式/参数不匹配都会导致错误的输出,因此一旦你这样做就不能依赖任何东西。很难说除了垃圾输出之外哪些会产生可怕的后果,因为它完全不依赖于您正在编译的平台的细节以及printf实现的实际细节。

将无效参数传递给printf格式为%s的实例可能导致无效指针被取消引用。但是更简单的类型的无效参数(例如intdouble)可能会导致对齐错误,并产生类似的后果。

答案 3 :(得分:2)

我首先要求您了解,对于64位版本的OS X,Linux,BSD克隆以及各种Unix风格,long为64位,如果不是已经知道了。但是,64位Windows将long保持为32位。

这与printf()和UB的转换规范有什么关系?

在内部,printf()将使用va_arg()宏。如果在64位Linux上使用%ld并仅传递int,则将从相邻内存中检索其他32位。如果在64位Linux上使用%d并传递long,则其他32位仍将在参数堆栈上。换句话说,转换规范指示int的类型(longva_arg(),无论如何),相应类型的大小确定{{1}的字节数调整其参数指针。虽然它将在va_arg()上仅在Windows上运行,但将其移植到另一个64位平台可能会带来麻烦,尤其是当您拥有sizeof(int)==sizeof(long)并尝试将int *nptr;%ld一起使用时}。如果您无法访问相邻内存,则可能会出现段错误。所以可能的具体案例是:

  • 读取相邻内存,并从
  • 上的那个点开始输出
  • 尝试读取相邻内存,并且由于保护机制存在段错误
  • *nptrlong的大小相同,所以它只是起作用
  • 取出的值被截断,输出从
  • 上的那个点开始搞乱

我不确定在一些平台上对齐是否是一个问题,但如果是,则取决于传递函数参数的实现。一些带有短参数列表的“智能”编译器特定int可能会完全绕过printf(),并将传递的数据表示为字节串而不是堆栈。如果发生这种情况,va_arg()有三种可能性:

  • printf("%x %lx\n", LONG_MAX, INT_MIN);long的大小相同,所以它只是起作用
  • int已打印
  • 程序由于对齐错误而崩溃

至于为什么C标准说它会导致未定义的行为,它没有准确指定ffffffff ffffffff80000000如何工作,如何在内存中传递和表示函数参数,或va_arg()的显式大小,int或其他原始数据类型,因为它不会不必要地限制实现。因此,无论发生什么,C标准都无法预测。只要看一下上面的例子,就应该说明这个事实,我无法想象还有哪些其他实现可能会完全不同。

答案 4 :(得分:2)

某些编译器可能以允许的方式实现变量格式参数 要验证的参数类型;因为程序陷阱不正确 使用可能比输出看起来有效但错误的更好 信息,一些平台可能会选择这样做。

因为陷阱的行为超出了C标准的范围,所以任何动作 可能合理陷阱的类别被归类为调用未定义的行为。

请注意,基于错误格式化实现陷阱的可能性意味着即使在期望类型和实际传递类型具有相同表示的情况下,行为也被视为未定义,除非相同等级的有符号和无符号数字是可互换的。他们持有的价值在两者共同的范围内[即如果“long”保持23,则可以使用“%lX”输出,但不能使用“%X”输出,即使“int”和“long”具有相同的大小]。

另请注意,C89委员会引入了一个法定规则,该规则至今仍然存在,即使“int”和“long”具有相同的格式,代码:

long foo=23;
int *u = &foo;
(*u)++;

调用未定义的行为,因为它导致写入类型为“long”的信息被读作类型“int”(如果类型为“unsigned int”,行为也将是Undefined)。由于“%X”格式说明符会导致数据被读取为“unsigned int”类型,因此将数据作为“long”类型传递几乎肯定会导致数据存储在“long”的某处,但随后将其读作“unsigned”类型int“,这种行为几乎可能违反上述规则。