【问题标题】:How does returning values from a function work?从函数返回值是如何工作的?
【发布时间】:2012-03-28 01:33:10
【问题描述】:

我最近遇到了一个严重的错误,我忘记在函数中返回一个值。问题是,即使没有返回任何内容,它在 Linux/Windows 下也能正常工作,并且只在 Mac 下崩溃。当我打开所有编译器警告时,我发现了这个错误。

所以这里是一个简单的例子:

#include <iostream>

class A{
public:
    A(int p1, int p2, int p3): v1(p1), v2(p2), v3(p3)
    {
    }

    int v1;
    int v2;
    int v3;
};

A* getA(){
    A* p = new A(1,2,3);
//  return p;
}

int main(){

    A* a = getA();

    std::cerr << "A: v1=" << a->v1 << " v2=" << a->v2 << " v3=" << a->v3 << std::endl;  

    return 0;
}

我的问题是如何在 Linux/Windows 下运行而不会崩溃?底层的值返回是怎么做的?

【问题讨论】:

  • 您对组装的感觉如何,或者至少是寄存器和跳跃的概念?
  • 你有未定义的行为,所以任何事情都可能发生。包括你看到的。崩溃不是已定义的程序行为的一部分,更不用说必需的程序行为了!
  • @Corbin 自从我学习计算机科学以来,我对汇编程序和这些概念有了基本的了解。
  • @Kerrek 我知道这会导致未定义的行为,但我想知道为什么它会巧合地起作用?
  • (讽刺的是,你可以实际上从main中省略return 0;,因为它是隐含的。)

标签: c++ stack


【解决方案1】:

在英特尔架构上,简单值(整数和指针)通常在eax 寄存器中返回。该寄存器(除其他外)还用作在内存中移动值时的临时存储和计算期间的操作数。因此,该寄存器中剩余的任何值都将被视为返回值,在您的情况下,结果正是您想要返回的值。

【讨论】:

  • 为什么缺少的return 语句不是编译器的错误?
  • @user384706 因为它是 C++ 标准 (6.6.3/2) 允许的。我自己想知道为什么他们在地球上保持这种方式。
  • @hamstergene:我认为因为它需要编译器,给定int foo() { bar(); }(例如),以确定bar() 是否抛出。原则上可能,在实践中可能不值得 C++。 (并不是说它不能用其他语言完成,确实如此。)
  • @hamstergene:作为 GManNickG,问题在于评估一个函数是否在所有可能的路径上返回实际上非常困难。例如,如果您有一个永远不会返回的函数(如abort),Eclipse 就会出错。为了提供良好的警告,gcc 引入了__attribute__((noreturn)) 来标记这些函数,但即便如此,它也不是微不足道的,而且由于它是特定于编译器的,你不能指望你使用的库有它们,所以很难强加它。另一方面,如果你像我一样使用-Werror -Wall -Wextra,没问题;)
【解决方案2】:

可能幸运的是,'a' 留在了恰好用于返回单指针结果的寄存器中,类似这样。

调用/约定和函数结果返回取决于体系结构,因此您的代码可以在 Windows/Linux 上运行但不能在 Mac 上运行也就不足为奇了。

【讨论】:

  • 为什么缺少的return语句不是编译器的错误?
  • 检查每条控制路径可能很困难,因此编译器不需要检查,但大多数会在捕获它时发出警告。
【解决方案3】:

编译器返回值有两种主要方式:

  1. 在返回之前将值放入寄存器,然后
  2. 让调用者传递一个堆栈内存块作为返回值,并将值写入该块[more info]

#1 通常与适合寄存器的任何东西一起使用; #2 适用于其他所有内容(大型结构、数组等)。

在您的情况下,编译器使用#1 返回new 和返回您的函数。在 Linux 和 Windows 上,编译器在将返回值写入指针变量和从函数返回之间,没有对寄存器执行任何值扭曲操作;在 Mac 上,确实如此。因此,您看到的结果有所不同:在第一种情况下,返回寄存器中的 剩余值 恰好与您 想要 的值位于同一内部无论如何都要返回。

【讨论】:

    【解决方案4】:

    首先,您需要稍微修改您的示例以使其编译。该函数必须至少有一个返回值的执行路径。

    A* getA(){
        if(false)
            return NULL;
        A* p = new A(1,2,3);
    //  return p;
    }
    

    其次,这显然是未定义的行为,这意味着任何事情都可能发生,但我想这个答案不会让你满意。

    第三,在 Windows 中它在 Debug 模式下工作,但如果你在 Release 下编译,它就不行。

    下面是在Debug下编译的:

        A* p = new A(1,2,3);
    00021535  push        0Ch  
    00021537  call        operator new (211FEh) 
    0002153C  add         esp,4 
    0002153F  mov         dword ptr [ebp-0E0h],eax 
    00021545  mov         dword ptr [ebp-4],0 
    0002154C  cmp         dword ptr [ebp-0E0h],0 
    00021553  je          getA+7Eh (2156Eh) 
    00021555  push        3    
    00021557  push        2    
    00021559  push        1    
    0002155B  mov         ecx,dword ptr [ebp-0E0h] 
    00021561  call        A::A (21271h) 
    00021566  mov         dword ptr [ebp-0F4h],eax 
    0002156C  jmp         getA+88h (21578h) 
    0002156E  mov         dword ptr [ebp-0F4h],0 
    00021578  mov         eax,dword ptr [ebp-0F4h] 
    0002157E  mov         dword ptr [ebp-0ECh],eax 
    00021584  mov         dword ptr [ebp-4],0FFFFFFFFh 
    0002158B  mov         ecx,dword ptr [ebp-0ECh] 
    00021591  mov         dword ptr [ebp-14h],ecx 
    

    第二条指令,对operator new 的调用,移动到指向新创建实例的指针eax

        A* a = getA();
    0010484E  call        getA (1012ADh) 
    00104853  mov         dword ptr [a],eax 
    

    调用上下文期望eax包含返回值,但它没有,它包含new分配的最后一个指针,顺便说一句,p

    这就是它起作用的原因。

    【讨论】:

    • 我的代码在我的 linux 机器上编译而无需修改。但这并不重要。但是我有两个问题:编译后如何查看生成的汇编代码?你能帮我理解第一个汇编代码sn-p吗?那里到底做了什么?
    • @Dirk 我正在使用 Visual Studio,您只需右键单击并选择 show dissasembly。至于第二个问题,它是在eax指向的地址上构造一个新的A对象,这个对象是通过调用new来初始化的。我无法解释更多,我的 asm 知识有限。
    【解决方案5】:

    正如 Kerrek SB 所述,您的代码已冒险进入未定义行为的领域。

    基本上,您的代码将编译为汇编。在汇编中,没有函数需要返回类型的概念,只有一个期望。我对 MIPS 最熟悉,所以我会用 MIPS 来说明。

    假设你有以下代码:

    int add(x, y)
    {
        return x + y;
    }
    

    这将被翻译成类似的东西:

    add:
        add $v0, $a0, $a1 #add $a0 and $a1 and store it in $v0
        jr $ra #jump back to where ever this code was jumped to from
    

    要添加 5 和 4,代码将被称为:

    addi $a0, $0, 5 # 5 is the first param
    addi $a1, $0, 4 # 4 is the second param
    jal add
    # $v0 now contains 9
    

    请注意,与 C 不同,没有明确要求 $v0 包含返回值,只是一个期望。那么,如果你实际上没有将任何东西压入 $v0 会发生什么?好吧,$v0 总是有 一些 值,所以该值将是它最后的值。

    注意:这篇文章做了一些简化。此外,您的计算机可能没有运行 MIPS……但希望该示例能够成立,如果您在大学学习过汇编,那么 MIPS 可能就是您所知道的。

    【讨论】:

    • 为什么缺少的return 语句不是编译器的错误?
    • 我的猜测是,最初起草 C 时,他们不想费心进行代码分析来确定函数是否总是返回(这可能是函数中的复杂分析循环和 ifs 等等,其中包含返回)。然后,它可能从那时起就一直存在,因为它从未成为一个明确的错误。这是一个警告,你通常应该保持警告。
    【解决方案6】:

    函数返回值的方式取决于架构和值的类型。它可以通过寄存器或堆栈来完成。 通常在 x86 架构中,如果它是整数类型,则在 EAX 寄存器中返回值:char、int 或指针。 当您不指定返回值时,该值是未定义的。这只是您的运气,您的代码有时可以正常工作。

    【讨论】:

    • TBH,令我惊讶的是,如果不使用它的结果,'a' 的构造并不会完全优化掉。如果在不知道结果的情况下被问到,我会猜到在 main() 中尝试取消引用时 OP 代码会出现段错误/AV。
    • 在调试模式下无法优化。毫不奇怪:-)。您描述的行为应该在发布模式下发生,并开启优化。
    【解决方案7】:

    在 IBM PC 架构中从堆栈中弹出值时,不会对存储在那里的旧数据值进行物理破坏。它们只是通过堆栈的操作变得不可用,但仍保留在同一个存储单元中。

    当然,这些数据之前的值会在后续新数据入栈的过程中被销毁。

    所以你可能只是足够幸运,在你的函数调用和返回周围代码期间没有任何东西被添加到堆栈中。

    【讨论】:

      【解决方案8】:

      关于 n3242 草案 C++ 标准第 6.6.3.2 段中的以下语句,您的示例产生 未定义的行为

      从函数的末尾流出相当于没有返回 价值;这会导致值返回中的未定义行为 功能。

      了解实际情况的最佳方法是检查给定编译器在给定架构上生成的汇编代码。对于以下代码:

      #pragma warning(default:4716)
      int foo(int a, int b)
      {
          int c = a + b;
      }
      
      int main()
      {
          int n = foo(1, 2);
      }
      

      ...VS2010 编译器(在调试模式下,在 Intel 32 位机器上)生成以下程序集:

      #pragma warning(default:4716)
      int foo(int a, int b)
      {
      011C1490  push        ebp  
      011C1491  mov         ebp,esp  
      011C1493  sub         esp,0CCh  
      011C1499  push        ebx  
      011C149A  push        esi  
      011C149B  push        edi  
      011C149C  lea         edi,[ebp-0CCh]  
      011C14A2  mov         ecx,33h  
      011C14A7  mov         eax,0CCCCCCCCh  
      011C14AC  rep stos    dword ptr es:[edi]  
          int c = a + b;
      011C14AE  mov         eax,dword ptr [a]  
      011C14B1  add         eax,dword ptr [b]  
      011C14B4  mov         dword ptr [c],eax  
      }
      ...
      int main()
      {
      011C14D0  push        ebp  
      011C14D1  mov         ebp,esp  
      011C14D3  sub         esp,0CCh  
      011C14D9  push        ebx  
      011C14DA  push        esi  
      011C14DB  push        edi  
      011C14DC  lea         edi,[ebp-0CCh]  
      011C14E2  mov         ecx,33h  
      011C14E7  mov         eax,0CCCCCCCCh  
      011C14EC  rep stos    dword ptr es:[edi]  
          int n = foo(1, 2);
      011C14EE  push        2  
      011C14F0  push        1  
      011C14F2  call        foo (11C1122h)  
      011C14F7  add         esp,8  
      011C14FA  mov         dword ptr [n],eax  
      }
      

      foo()中加法运算的结果存储在eax寄存器(累加器)中,其内容作为函数的返回值,移动到变量n中。

      eax 在以下示例中也用于存储返回值(指针):

      #pragma warning(default:4716)
      int* foo(int a)
      {
          int* p = new int(a);
      }
      
      int main()
      {
          int* pn = foo(1);
      
          if(pn)
          {
              int n = *pn;
              delete pn;
          }
      }
      

      汇编代码:

      #pragma warning(default:4716)
      int* foo(int a)
      {
      000C1520  push        ebp  
      000C1521  mov         ebp,esp  
      000C1523  sub         esp,0DCh  
      000C1529  push        ebx  
      000C152A  push        esi  
      000C152B  push        edi  
      000C152C  lea         edi,[ebp-0DCh]  
      000C1532  mov         ecx,37h  
      000C1537  mov         eax,0CCCCCCCCh  
      000C153C  rep stos    dword ptr es:[edi]  
          int* p = new int(a);
      000C153E  push        4  
      000C1540  call        operator new (0C1253h)  
      000C1545  add         esp,4  
      000C1548  mov         dword ptr [ebp-0D4h],eax  
      000C154E  cmp         dword ptr [ebp-0D4h],0  
      000C1555  je          foo+50h (0C1570h)  
      000C1557  mov         eax,dword ptr [ebp-0D4h]  
      000C155D  mov         ecx,dword ptr [a]  
      000C1560  mov         dword ptr [eax],ecx  
      000C1562  mov         edx,dword ptr [ebp-0D4h]  
      000C1568  mov         dword ptr [ebp-0DCh],edx  
      000C156E  jmp         foo+5Ah (0C157Ah)  
      std::operator<<<std::char_traits<char> >:
      000C1570  mov         dword ptr [ebp-0DCh],0  
      000C157A  mov         eax,dword ptr [ebp-0DCh]  
      000C1580  mov         dword ptr [p],eax  
      }
      ...
      int main()
      {
      000C1610  push        ebp  
      000C1611  mov         ebp,esp  
      000C1613  sub         esp,0E4h  
      000C1619  push        ebx  
      000C161A  push        esi  
      000C161B  push        edi  
      000C161C  lea         edi,[ebp-0E4h]  
      000C1622  mov         ecx,39h  
      000C1627  mov         eax,0CCCCCCCCh  
      000C162C  rep stos    dword ptr es:[edi]  
          int* pn = foo(1);
      000C162E  push        1  
      000C1630  call        foo (0C124Eh)  
      000C1635  add         esp,4  
      000C1638  mov         dword ptr [pn],eax  
      
          if(pn)
      000C163B  cmp         dword ptr [pn],0  
      000C163F  je          main+51h (0C1661h)  
          {
              int n = *pn;
      000C1641  mov         eax,dword ptr [pn]  
      000C1644  mov         ecx,dword ptr [eax]  
      000C1646  mov         dword ptr [n],ecx  
              delete pn;
      000C1649  mov         eax,dword ptr [pn]  
      000C164C  mov         dword ptr [ebp-0E0h],eax  
      000C1652  mov         ecx,dword ptr [ebp-0E0h]  
      000C1658  push        ecx  
      000C1659  call        operator delete (0C1249h)  
      000C165E  add         esp,4  
          }
      }
      

      两个示例中的 VS2010 编译器问题 warning 4716。默认情况下,此警告会升级为错误。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-04-25
        • 2016-06-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多