这是 glibc printf 中的错误吗?
Posted
技术标签:
【中文标题】这是 glibc printf 中的错误吗?【英文标题】:Is this a bug in glibc printf? 【发布时间】:2021-12-30 15:07:44 【问题描述】:使用 glibc 中的stdint.h
(gcc SUSE Linux 版本 9.2.1,Intel Core I7 处理器)我在直接打印 INT32_MIN
时遇到了一个最奇怪的行为:
#include <stdio.h>
#include <stdint.h>
void main(void)
printf("%d\n", INT16_MIN);
int a = INT16_MIN;
printf("%d\n", a);
printf("%ld\n", INT32_MIN);
long b = INT32_MIN;
printf("%ld\n", b);
printf("%ld\n", INT64_MIN);
long c = INT64_MIN;
printf("%ld\n", c);
哪个输出:
-32768
-32768
2147483648
-2147483648
-9223372036854775808
-9223372036854775808
此外,如果我尝试
printf("%ld\n", -INT32_MIN);
我得到相同的结果,但编译器 warning: integer overflow in expression '-2147483648' of type 'int' results in '-2147483648' [-Woverflow]
。
并不是说这对任何现有程序都非常糟糕,实际上它看起来很无害,但这是旧 printf 中的一个错误吗?
【问题讨论】:
我很确定这是未定义的行为,'%ld'
会期待 long int
iirc。
但是当我从long b
打印时它可以工作,还有编译器警告,当使用%d
INT32_MIN
时仍然存在。
这是未定义的行为,它可以为所欲为。它可能是对的,也可能是大错特错。是的,如果您打印它期望的格式,它会起作用,我不确定为什么这会表明 UB 是一个错误?
啊,好吧,你们是对的,这很奇怪,但它未按标准定义,%d
工作正常,谢谢!
请注意,-INT32_MIN
也是未定义的行为:有符号整数溢出。
【参考方案1】:
这是 glibc printf 中的错误吗?
没有。
printf("%ld\n", INT32_MIN);
…2147483648
有一种简单的方法可以做到这一点。函数的第二个整数/指针参数应在 64 位寄存器 RCX 中传递。 INT32_MIN
是 32 位 int
,位模式为 0x80000000,因为这是 -2,147,483,648 的二进制补码模式。在 64 位寄存器中传递 32 位值的规则是在低 32 位中传递,被调用例程不使用高位。对于这个调用,0x80000000 被放入了低 32 位,而高位恰好设置为 0。
然后printf
检查格式字符串并期望 64 位 long
。所以它会在 RCX 中寻找一个 64 位整数。当然,传递64位整数的规则是使用整个寄存器,所以printf
取所有64位,0x0000000080000000。这是 +2,147,483,468 的位模式,所以 printf
打印 2147483648
。
当然,C 标准没有定义行为,因此可能会发生其他事情,但这可能是您观察到的实例中发生的情况。
printf("%d\n", INT16_MIN);
…-32768
由于 int
在您的 C 实现中是 32 位的,因此 int16_t
的值 INT16_MIN
会自动提升为 int
以进行函数调用,因此这会传递一个 int
,而 %d
需要一个 @ 987654339@,所以没有不匹配,打印出正确的值。
同样,问题中的其他 printf
调用具有与转换规范匹配的参数(鉴于 int16_t
的特定定义以及您的 C 实现中的此类;它们在其他调用中可能不匹配),因此它们的值可以正确打印.
【讨论】:
“并且高位恰好设置为零。”:根据 OP 的信息,这很有可能,因为大多数将填充低 32 位 (ECX
) 的指令都有边将高 32 位归零的效果:***.com/questions/11177137/…
太棒了...我永远不会想到:编译器在 32 位整数的上下文中解析 INT32_MAX
内容(因为没有指定 L
或 LL
修饰符)并生成您提到的位模式,但将其存储为未转换的 64 位寄存器(因为没有人指定在这种情况下应该做什么),在这种特殊情况下,这只会导致 (负)原创。解释了为什么其他案例运行良好,而这个特定案例却没有。关于“未定义行为”的微妙之处的教训,感谢您的精彩回答。
在我上面的评论中,我指的是INT32_MIN
,而不是INT32_MAX
。
为了完成答案,@NateEldredge 对我的问题发表了评论,解释 -INT32_MIN
也是未定义的行为,因此解释了溢出警告但打印结果相同:-INT32_MIN
is -(-2,147,483,468 ) = +2,147,483,468,变形为 -2,147,483,468,因此根据您的回答打印为 +2,147,483,468。
如上所述,编译器警告-Wformat
应启用,因为它会标记此转换问题。【参考方案2】:
我们来观察一下这个具体的sn-p
long a = INT32_MIN;
printf("%ld\n", a); // #1
printf("%ld\n", INT32_MIN); // #2
它打印以下内容(根据您的示例,我自己没有尝试过,它取决于实现,稍后我会注意到):
-2147483648
2147483648
现在,在#1 中,我们将long
传递给printf
。该函数使用可变参数列表并根据指定的格式,我们的参数值将被解释为signed long
,因此我们最终得到值-2147483648
。
对于#2,我们的情况有些不同。如果您查看 INT32_MIN
是如何定义的,您会发现它是一个普通的旧 C 宏,例如:
# define INT32_MIN (-2147483647-1)
所以,我们的路线
printf("%ld\n", INT32_MIN);
其实是
printf("%ld\n", (-2147483647-1));
参数类型不是长整数,而是整数(参见this for details)!对比一下
int b = INT32_MIN;
printf("%ld\n", b);
它会打印出您观察到的相同值(正值)。
现在归结为您的问题实际上应该是什么:
当
printf
收到一个不在格式说明符中的类型的值时会发生什么?
Undefined behavior!
【讨论】:
啊,好吧,你们是对的,这很奇怪,但它没有按标准定义,%d
工作正常,谢谢!
printf
in printf("%ld\n", a);
不接受void *
,参数中也没有。
我的错,我错误地认为可变参数将被解释为 void* 并且函数 impl 将负责解释。以上是关于这是 glibc printf 中的错误吗?的主要内容,如果未能解决你的问题,请参考以下文章
glibc: Parsing a Template String 如何解析printf格式化