【问题标题】:Why and how does GCC compile a function with a missing return statement?为什么以及如何编译缺少 return 语句的函数?
【发布时间】:2018-05-18 20:14:46
【问题描述】:
#include <stdio.h>

char toUpper(char);

int main(void)
{
    char ch, ch2;
    printf("lowercase input : ");
    ch = getchar();
    ch2 = toUpper(ch);
    printf("%c ==> %c\n", ch, ch2);

    return 0;
}

char toUpper(char c)
{
    if(c>='a'&&c<='z')
        c = c - 32;
}

在toUpper函数中,返回类型是char,但是在toUpper()中没有“return”。并用 gcc (GCC) 4.5.1 20100924 (Red Hat 4.5.1-4), fedora-14 编译源代码。

当然,会发出警告:“警告:控制到达非无效函数的结尾”,但是,运行良好。

在使用 gcc 编译期间,该代码发生了什么? 在这种情况下,我想得到一个可靠的答案。 谢谢:)

【问题讨论】:

  • 闻起来像未定义的行为。
  • @ThiefMaster:它 UB。他很幸运,通常放置返回值的寄存器恰好也用于减法。

标签: c linux gcc


【解决方案1】:

发生在你身上的是,当 C 程序被编译成汇编语言时,你的 toUpper 函数最终是这样的,也许:

_toUpper:
LFB4:
        pushq   %rbp
LCFI3:
        movq    %rsp, %rbp
LCFI4:
        movb    %dil, -4(%rbp)
        cmpb    $96, -4(%rbp)
        jle     L8
        cmpb    $122, -4(%rbp)
        jg      L8
        movzbl  -4(%rbp), %eax
        subl    $32, %eax
        movb    %al, -4(%rbp)
L8:
        leave
        ret

32 的减法是在 %eax 寄存器中进行的。在 x86 调用约定中,这是期望返回值所在的寄存器!所以……你很幸运。

但请注意警告。它们的存在是有原因的!

【讨论】:

  • 当然警告并不总是那么容易理解。在这种情况下,它试图说该函数未声明为 void,并且有一种方法可以在没有 return 语句的情况下执行该函数,因此它将返回指定返回槽中发生的任何垃圾。但是这个指定的返回槽在不同的实现中是不同的,所以你不能依赖这个代码。在其他语言(例如 Java、Ada)中,在这种情况下您不会收到警告,您会收到编译时 error。事实证明,来自 C 编译器的许多警告确实表明存在真正的问题。
【解决方案2】:

这取决于Application Binary Interface 以及用于计算的寄存器。

例如在 x86 上,第一个函数参数和返回值存储在 EAX 中,因此 gcc 很可能也使用它来存储计算结果。

【讨论】:

    【解决方案3】:

    本质上,c 被推入稍后应该填充返回值的位置;因为它没有被 return 覆盖,所以它最终作为返回的值。

    请注意,依赖此(在 C 或任何其他语言中,这不是显式语言功能,如 Perl)是一个 Bad Idea™。极端。

    【讨论】:

      【解决方案4】:

      需要理解的重要一点是,忽略 return 语句很少是可诊断的错误。考虑这个函数:

      int f(int x)
      {
          if (x!=42) return x*x;
      }
      

      只要你从不使用 42 的参数调用它,包含这个函数的程序就是完全有效的 C 并且不会调用任何未定义的行为,尽管它调用 UB 如果你调用f(42),随后尝试使用返回值。

      因此,虽然编译器可以为缺少的 return 语句提供警告启发式方法,但如果没有误报或漏报,就不可能这样做。这是无法解决停机问题的结果。

      【讨论】:

      • +1 很高兴澄清这一点。每当 Java 和 Ada 检测到 存在 某个通过函数的路径可能会在没有返回语句的情况下退出时,就会发出关于缺少返回的编译时错误,而不是肯定会采用这样的路径。
      【解决方案5】:

      我无法告诉您平台的具体细节,因为我不知道,但您看到的行为有一个一般性的答案。

      编译具有返回值的某些函数时,编译器将使用约定如何返回该数据。它可以是机器寄存器,也可以是定义的内存位置,例如通过堆栈或其他方式(尽管通常使用机器寄存器)。编译后的代码在执行函数工作时也可以使用该位置(寄存器或其他)。

      如果函数没有返回任何内容,那么编译器将不会生成用返回值显式填充该位置的代码。但是,就像我上面所说的那样,它可能会在函数期间使用该位置。当您编写读取返回值(ch2 = toUpper(ch);) 的代码时,编译器将编写代码,使用其约定如何从常规位置检索返回值。就调用者代码而言,它只会从该位置读取该值,即使那里没有明确写入任何内容。因此你会得到一个值。

      现在看@Ray 的例子,编译器使用EAX 寄存器来存储大写操作的结果。碰巧,这可能是返回值被写入的位置。在调用方 ch2 加载了 EAX 中的值 - 因此是幻像返回。这仅适用于 x86 系列处理器,因为在其他架构上,编译器可能会使用完全不同的方案来决定如何组织约定

      然而,优秀的编译器会尝试根据一组本地条件、代码知识、规则和启发式进行优化。所以需要注意的重要一点是,这只是运气好。编译器可以优化而不做这个或任何事情 - 你不应该回复行为。

      【讨论】:

      • 一个好的编译器会优化整个函数体,因为它没有副作用并且不使用任何计算值......
      • 当然@R,这个故事的寓意是,不管约定如何,即使它有效,你不应该回复它。
      【解决方案6】:

      您应该记住,此类代码可能会因编译器而崩溃。例如,clang 在该函数结束时生成 ud2 指令,您的应用将在运行时崩溃。

      【讨论】:

        【解决方案7】:

        没有局部变量,所以函数结束时栈顶的值就是参数c。退出时堆栈顶部的值是返回值。所以无论 c 持有什么,这就是返回值。

        【讨论】:

          【解决方案8】:

          我试过一个小程序:

          #include <stdio.h>
          int f1() {
          }
          int main() {
              printf("TEST: <%d>\n",  f1());
              printf("TEST: <%d>\n",  f1());
              printf("TEST: <%d>\n",  f1());
              printf("TEST: <%d>\n",  f1());
              printf("TEST: <%d>\n",  f1());
          }
          

          结果:

          测试:

          测试:

          测试:

          测试:

          测试:

          我用过mingw32-gcc编译器,可能会有差异。

          你可以随便玩玩,例如一个字符函数。 只要您不使用结果值,它仍然可以正常工作。

          #include <stdio.h>
          char f1() {
          }
          int main() {
              f1();
          }
          

          但我仍然建议设置 void 函数或提供一些返回值。

          您的函数似乎需要返回:

          char toUpper(char c)
          {
              if(c>='a'&&c<='z')
                  c = c - 32;
              return c;
          }
          

          【讨论】:

            猜你喜欢
            • 2013-05-23
            • 2012-06-07
            • 2017-03-17
            • 1970-01-01
            • 2019-03-20
            • 1970-01-01
            相关资源
            最近更新 更多