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

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

printf("%d", 1.9) 

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

这个问题的灵感来自一个用户遇到遗留代码的问题,这些代码存在大量的转换不匹配,这显然没有任何损害,参见 理论上和实践中未定义的行为 。

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

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

我快速浏览了printf源代码 ,但它们对于休闲读者来说非常不透明。

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

一些编译器可以以允许validation参数类型的方式实现变量格式参数; 因为在错误使用上有一个程序陷阱可能比输出看似有效但错误的信息更好,一些平台可能会选择这样做。

由于陷阱的行为超出了C标准的范围,因此任何可能合理陷阱的操作都被归类为调用未定义的行为。

请注意,基于错误格式化实现陷阱的可能性意味着即使在期望类型和实际传递类型具有相同表示的情况下,行为也被视为未定义,除非相同等级的有符号和无符号数字是可互换的,如果值为hold在两者共有的范围内[即如果“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“,这种行为几乎可能违反上述规则。

如果正确使用, printf仅按标准描述的方式工作。 如果使用不正确,则行为未定义。 为什么标准定义了当你使用它时会发生什么?

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

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

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

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

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

我首先要求您了解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位仍将在参数堆栈上。 换句话说,转换规范指示va_arg()的类型( intlong ,whatever),并且相应类型的大小确定va_arg()调整其参数指针的字节数。 虽然它只能在Windows上工作,因为sizeof(int)==sizeof(long) ,将它移植到另一个64位平台可能会带来麻烦,特别是当你有一个int *nptr; 并尝试将%ld*nptr一起使用。 如果您无法访问相邻内存,则可能会出现段错误。 所以可能的具体案例是:

  • 读取相邻存储器,并从该点开始输出混乱
  • 尝试读取相邻存储器,并且由于保护机制存在段错误
  • longint的大小是一样的,所以它才有效
  • 取出的值被截断,输出从该点开始混乱

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

  • longint的大小是一样的,所以它才有效
  • ffffffff ffffffff80000000已打印
  • 程序因对齐错误而崩溃

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