这是 glibc printf 中的错误吗?
Is this a bug in glibc printf?
使用 glibc 中的 stdint.h
(gcc SUSE Linux 版本 9.2.1,英特尔酷睿 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 中的错误吗?
让我们观察这个特定的片段
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);
它打印出与您观察到的相同的值(正值)。
现在归结为您的问题实际上应该是什么:
What happens when printf
receives a value of a type that isn't in the format specifier?
!
Is this a bug in glibc printf?
没有
printf("%ld\n", INT32_MIN);
… 2147483648
有一种简单的方法可以做到这一点。函数的第二个 integer/pointer 参数应该在 64 位寄存器 RCX 中传递。 INT32_MIN
是一个 32 位 int
,位模式为 0x80000000,因为这是 −2,147,483,648 的二进制补码模式。在 64 位寄存器中传递 32 位值的规则是它在低 32 位中传递,高位不被调用的例程使用。这次调用,0x80000000被放到了低32位,高位刚好被置零。
然后 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
需要一个 int
,所以没有不匹配,并且打印了正确的值。
类似地,问题中的其他 printf
调用具有与转换规范匹配的参数(考虑到 int16_t
的特定定义等在您的 C 实现中;它们可能在其他方面不匹配),所以它们的值被正确打印。
使用 glibc 中的 stdint.h
(gcc SUSE Linux 版本 9.2.1,英特尔酷睿 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 中的错误吗?
让我们观察这个特定的片段
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);
它打印出与您观察到的相同的值(正值)。
现在归结为您的问题实际上应该是什么:
What happens when
printf
receives a value of a type that isn't in the format specifier?
Is this a bug in glibc printf?
没有
printf("%ld\n", INT32_MIN);
…2147483648
有一种简单的方法可以做到这一点。函数的第二个 integer/pointer 参数应该在 64 位寄存器 RCX 中传递。 INT32_MIN
是一个 32 位 int
,位模式为 0x80000000,因为这是 −2,147,483,648 的二进制补码模式。在 64 位寄存器中传递 32 位值的规则是它在低 32 位中传递,高位不被调用的例程使用。这次调用,0x80000000被放到了低32位,高位刚好被置零。
然后 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
需要一个 int
,所以没有不匹配,并且打印了正确的值。
类似地,问题中的其他 printf
调用具有与转换规范匹配的参数(考虑到 int16_t
的特定定义等在您的 C 实现中;它们可能在其他方面不匹配),所以它们的值被正确打印。