【问题标题】:Why does printf("%f",0); give undefined behavior?为什么 printf("%f",0);给出未定义的行为?
【发布时间】:2016-11-30 12:55:05
【问题描述】:

声明

printf("%f\n",0.0f);

打印 0。

然而,声明

printf("%f\n",0);

打印随机值。

我意识到我表现出某种未定义的行为,但我无法弄清楚具体原因。

所有位为 0 的浮点值仍然是有效的 float,值为 0。
floatint 在我的机器上大小相同(如果这甚至相关) .

为什么在printf 中使用整数文字而不是浮点文字会导致这种行为?

附:如果我使用,可以看到相同的行为

int i = 0;
printf("%f\n", i);

【问题讨论】:

  • printf 期待一个 double,而你给它一个 intfloatint 在您的机器上可能是相同的大小,但是当推入可变参数列表时,0.0f 实际上会转换为 double(并且 printf 预计会这样)。简而言之,根据您使用的说明符和您提供的参数,您没有履行与printf 的交易。
  • 可变参数函数不会自动将函数参数转换为相应参数的类型,因为它们不能。与具有原型的非可变参数函数不同,编译器无法获得必要的信息。
  • 噢……“可变参数。”我刚学了一个新词……
  • 接下来要尝试的是传递 (uint64_t)0 而不是 0 并查看是否仍然会出现随机行为(假设 doubleuint64_t 具有相同的大小和对齐方式)。由于在不同的寄存器中传递了不同的类型,因此在某些平台(例如 x86_64)上,输出可能仍然是随机的。

标签: c++ c printf implicit-conversion undefined-behavior


【解决方案1】:

我不确定是什么令人困惑。

您的格式字符串需要double;您改为提供int

这两种类型是否具有相同的位宽完全无关紧要,只是它可以帮助您避免像这样的损坏代码导致硬内存违规异常。

【讨论】:

  • @Voo:不幸的是,格式字符串修饰符 is 被命名,但我仍然不明白为什么你会认为 int 在这里可以接受。跨度>
  • @Voo: "(也可以作为有效的浮点模式)" 为什么int 可以作为有效的浮点模式?二进制补码和各种浮点编码几乎没有共同之处。
  • 这很令人困惑,因为对于 大多数 库函数,将整数文字 0 提供给类型为 double 的参数将做正确的事情。对于初学者来说,编译器不会对%[efg] 寻址的printf 参数槽进行相同的转换。
  • @Voo:如果您对这可能发生的可怕错误感兴趣,请考虑在 x86-64 SysV ABI 上,浮点参数在与整数参数不同的寄存器集中传递。
  • @LightnessRacesinOrbit 我认为总是适合讨论为什么某事物是 UB,这通常涉及讨论允许的实现范围以及实际发生的情况在常见情况下。
【解决方案2】:

"%f" 格式需要double 类型的参数。你给它一个int 类型的参数。这就是行为未定义的原因。

该标准不保证全位为零是0.0(尽管经常是)或任何double 值的有效表示,或者intdouble 的大小相同(记住它是double,而不是float),或者,即使它们的大小相同,它们也以相同的方式作为参数传递给可变参数函数。

它可能会在您的系统上“工作”。这是未定义行为的最坏症状,因为它使诊断错误变得困难。

N15707.21.6.1 第 9 段:

... 如果任何参数不是对应的正确类型 转换规范,行为未定义。

float 类型的参数被提升为double,这就是printf("%f\n",0.0f) 起作用的原因。比int 窄的整数类型的参数被提升为intunsigned int。这些提升规则(由 N1570 6.5.2.2 第 6 段指定)在 printf("%f\n", 0) 的情况下没有帮助。

请注意,如果您将常量 0 传递给需要 double 参数的非可变参数函数,则假设函数的原型是可见的,则行为已明确定义。例如,sqrt(0)(在#include <math.h> 之后)隐式地将参数0int 转换为double——因为编译器可以从sqrt 的声明中看到它需要一个double 参数。它没有printf 的此类信息。像 printf 这样的可变函数很特殊,在编写对它们的调用时需要更加小心。

【讨论】:

  • 这里有几个优秀的核心点。首先,它是double 而不是float,所以OP 的宽度假设可能不成立(可能不成立)。其次,整数零和浮点零具有相同位模式的假设也不成立。干得好
  • @LucasTrzesniewski:好的,但我看不出我的回答是如何引出这个问题的。我确实声明 float 被提升为 double 没有解释原因,但这不是重点。
  • @robertbristow-johnson:编译器不需要为 printf 提供特殊的钩子,尽管 gcc 确实有一些钩子,因此它可以诊断错误(if格式字符串是文字)。编译器可以从<stdio.h> 中看到printf 的声明,它告诉它第一个参数是const char*,其余的由, ... 指示。不,%f 用于double(并且float 提升为double),%lf 用于long double。 C 标准没有提到堆栈。它仅在正确调用printf 时指定其行为。
  • @robertbristow-johnson:在过去的发呆中,“lint”经常执行 gcc 现在执行的一些额外检查。传递给printffloat 提升为double;这没有什么神奇之处,它只是调用可变参数函数的语言规则。 printf 本身通过格式字符串知道调用者 声称 传递给它的内容;如果该声明不正确,则行为未定义。
  • 小修正:l 长度修饰符“对以下aAeEfF、@987654376 没有影响@ 或 G 转换说明符”,long double 转换的长度修饰符是 L。 (@robertbristow-johnson 可能也有兴趣)
【解决方案3】:

为什么使用整数文字而不是浮点文字会导致这种行为?

因为printf() 除了const char* formatstring 作为第一个参数之外没有类型参数。它使用 c 样式的省略号 (...) 来表示所有其余部分。

它只是根据格式字符串中给出的格式类型决定如何解释那里传递的值。

您会遇到与尝试时相同的未定义行为

 int i = 0;
 const double* pf = (const double*)(&i);
 printf("%f\n",*pf); // dereferencing the pointer is UB

【讨论】:

  • printf 的某些特定实现可能会以这种方式工作(除了传递的项目是值,而不是地址)。 C 标准没有指定 如何 printf 和其他可变参数函数的工作方式,它只是指定了它们的行为。特别是,没有提到堆栈帧。
  • 一个小问题:printf 确实有 one 类型参数,格式字符串,类型为 const char*。顺便说一句,这个问题被标记为 C 和 C++,而 C 真的更相关;我可能不会以reinterpret_cast 为例。
  • 只是一个有趣的观察:相同的未定义行为,很可能是由于相同的机制,但在细节上略有不同:在问题中传递一个 int,UB 发生在 within i> printf 在尝试将 int 解释为 double 时 - 在您的示例中,它在取消引用 pf 时已经发生在 outside ...
  • @Aconcagua 添加了说明。
  • 此代码示例是 UB,用于严格违反别名,与问题所问的问题完全不同。例如,您完全忽略了浮点数在不同寄存器中传递给整数的可能性。
【解决方案4】:

通常,当您调用需要 double 的函数,但您提供了 int 时,编译器会自动为您转换为 doubleprintf 不会发生这种情况,因为函数原型中没有指定参数的类型 - 编译器不知道应该应用转换。

【讨论】:

  • 另外,printf() 特别是的设计使其参数可以是任何类型。您必须知道格式字符串中的每个元素所期望的类型,并且必须正确提供。
  • @MikeRobinson:嗯,任何原始 C 类型。这是所有可能类型的一个非常非常小的子集。
【解决方案5】:

首先,正如其他几个答案中提到的那样,但在我看来,并没有清楚地说明:它确实大多数上下文中提供整数,其中库函数采用 doublefloat 参数。编译器会自动插入转换。例如,sqrt(0) 定义明确,其行为与sqrt((double)0) 完全相同,对于此处使用的任何其他整数类型表达式也是如此。

printf 不同。它是不同的,因为它需要可变数量的参数。它的函数原型是

extern int printf(const char *fmt, ...);

因此,当你写

printf(message, 0);

编译器没有关于printf 预计第二个参数是什么类型的任何信息。它只有参数表达式的类型,即int,可以通过。因此,与大多数库函数不同,程序员有责任确保参数列表与格式字符串的期望相匹配。

(现代编译器可以查看格式字符串并告诉您类型不匹配,但他们不会开始插入转换来完成您的意思,因为更好的当您注意到时,代码现在应该会中断,而不是几年后使用不太有用的编译器重新构建时。)

现在,问题的另一半是:鉴于 (int)0 和 (float)0.0 在大多数现代系统上都表示为 32 位,所有这些都为零,为什么它仍然不起作用,意外地? C 标准只是说“这不是必须的,你自己来做”,但让我说明它不起作用的两个最常见的原因;这可能会帮助您了解为什么它不是必需的。

首先,由于历史原因,当您通过变量参数列表传递 float 时,它会提升double,在大多数现代系统上,它是 64 em> 位宽。所以printf("%f", 0) 只将 32 个零位传递给期望其中 64 个的被调用者。

第二个同样重要的原因是浮点函数参数可能在与整数参数不同的位置中传递。例如,大多数 CPU 对整数和浮点值都有单独的寄存器文件,因此如果参数 0 到 4 是整数,则参数 0 到 4 进入寄存器 r0 到 r4,但如果它们是浮点值,则可能是 f0 到 f4。所以printf("%f", 0) 在寄存器 f1 中查找该零,但它根本不存在。

【讨论】:

  • 是否有任何架构使用寄存器来实现可变参数功能,甚至在那些将它们用于普通功能的架构中?我认为这就是需要正确声明可变参数函数的原因,即使可以使用() 声明其他函数[除了具有浮点/短/字符参数的函数]。
  • @Random832 如今,可变参数和普通函数的调用约定之间的唯一区别是可能向可变参数提供了一些额外的数据,例如提供的参数的真实数量。否则,一切都在与正常功能完全相同的位置。例如参见x86-64.org/documentation/abi.pdf 的第 3.2 节,其中对可变参数的唯一特殊处理是在 AL 中传递的提示。 (是的,这意味着va_arg 的实现比以前复杂得多。)
  • @Random832:我一直认为原因是在某些架构上,通过使用特殊指令可以更有效地实现具有已知数量和类型参数的函数。
  • @celtschk 您可能会想到 SPARC 和 IA64 上的“注册窗口”,它本应加速使用 少量 参数(唉,在实践中,他们做的恰恰相反)。它们不需要编译器对可变参数函数调用进行特殊处理,因为在任何一个调用点的参数数量始终是编译时常量,无论被调用者是否为可变参数。
  • @zwol:不,我在想 8086 的 ret n 指令,其中 n 是硬编码整数,因此不适用于可变参数函数。但是我不知道是否有任何 C 编译器真正利用了它(非 C 编译器肯定有)。
【解决方案6】:

使用不匹配的 printf() 说明符 "%f" 和类型 (int) 0 会导致未定义的行为。

如果转换规范无效,则行为未定义。 C11dr §7.21.6.1 9

UB 的候选原因。

  1. 每个规范都是 UB,而且编译很麻烦 - 'nuf 说。

  2. doubleint 的大小不同。

  3. doubleint 可以使用不同的堆栈传递它们的值(一般与 FPU 堆栈。)

  4. double 0.0 可能不是由全零位模式定义的。 (罕见)

【讨论】:

    【解决方案7】:

    "%f\n" 仅在第二个 printf() 参数的类型为 double 时才保证可预测的结果。接下来,可变参数函数的额外参数是默认参数提升的主题。整数参数属于整数提升,它永远不会导致浮点类型值。并且float参数被提升为double

    最重要的是:标准允许第二个参数为 or floatdouble,仅此而已。

    【讨论】:

      【解决方案8】:

      为什么它是正式的 UB 现在已经在几个答案中讨论过。

      您获得此行为的原因是平台相关的,但可能是以下原因:

      • printf 期望其参数根据标准可变参数传播。这意味着float 将是double,任何小于int 的都将是int
      • 您正在传递一个int,函数需要一个double。您的 int 可能是 32 位,您的 double 可能是 64 位。这意味着从参数应该位于的位置开始的四个堆栈字节是0,但后面的四个字节具有任意内容。这就是用于构造显示的值的内容。

      【讨论】:

        【解决方案9】:

        这是从编译器警告中学习的绝佳机会之一。

        $ gcc -Wall -Wextra -pedantic fnord.c 
        fnord.c: In function ‘main’:
        fnord.c:8:2: warning: format ‘%f’ expects argument of type ‘double’, but argument 2 has type ‘int’ [-Wformat=]
          printf("%f\n",0);
          ^
        

        $ clang -Weverything -pedantic fnord.c 
        fnord.c:8:16: warning: format specifies type 'double' but the argument has type 'int' [-Wformat]
                printf("%f\n",0);
                        ~~    ^
                        %d
        1 warning generated.
        

        所以,printf 正在产生未定义的行为,因为您传递给它的参数类型不兼容。

        【讨论】:

          【解决方案10】:

          此“未确定值”问题的主要原因在于将传递给printf 变量参数部分的int 值的指针转换为double 类型的指针va_arg 宏执行.

          这会导致引用一个未完全初始化的内存区域,该内存区域作为参数传递给 printf,因为double 大小的内存缓冲区大于int 的大小。

          因此,当此指针被取消引用时,它会返回一个未确定的值,或者更好的是一个“值”,其中部分包含作为参数传递给printf 的值,其余部分可能来自另一个堆栈缓冲区甚至是代码区域(引发内存错误异常),真正的缓冲区溢出


          它可以考虑“printf”和“va_arg”的简化代码实现的这些特定部分......

          printf

          va_list arg;
          ....
          case('%f')
                va_arg ( arg, double ); //va_arg is a macro, and so you can pass it the "type" that will be used for casting the int pointer argument of printf..
          .... 
          


          双值参数代码案例在 vprintf(考虑 gnu impl.)中的真正实现 管理是:

          if (__ldbl_is_dbl)
          {
             args_value[cnt].pa_double = va_arg (ap_save, double);
             ...
          }
          



          va_arg

          char *p = (double *) &arg + sizeof arg;  //printf parameters area pointer
          
          double i2 = *((double *)p); //casting to double because va_arg(arg, double)
             p += sizeof (double);
          



          参考文献

          1. gnu project glibc implementation of "printf"(vprintf))
          2. example of semplification code of printf
          3. example of semplification code of va_arg

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-02-24
            • 2019-11-16
            • 1970-01-01
            • 2018-07-03
            • 2021-10-30
            • 1970-01-01
            相关资源
            最近更新 更多