这是 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打印时它可以工作,还有编译器警告,当使用%dINT32_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 内容(因为没有指定 LLL 修饰符)并生成您提到的位模式,但将其存储为未转换的 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 中的错误吗?的主要内容,如果未能解决你的问题,请参考以下文章

C中的“中止陷阱:6”错误?

glibc: Parsing a Template String 如何解析printf格式化

我的 C 程序中的字符输入错误?

linux 操作系统下能用fprintf() 及fscanf()等函数吗?它们与c语言中的用法一样吗?

linux环境中golang使用glibc吗?

四则运算