函数的返回值是如何工作的

How does returning values from a function work?

本文关键字:工作 返回值 函数 何工作      更新时间:2023-10-16

我最近遇到了一个严重的错误,忘记在函数中返回值。问题是,即使没有返回任何东西,它在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下工作而不崩溃?如何在较低级别上返回值?

在英特尔体系结构上,简单值(整数和指针)通常在eax寄存器中返回。当在内存中移动值时,此寄存器(以及其他寄存器)也用作临时存储器,并在计算过程中用作操作数。因此,寄存器中剩下的任何值都被视为返回值,在您的情况下,它正是您想要返回的值。

可能是运气好,"a"留在了一个碰巧用于返回单指针结果的寄存器中,诸如此类。

调用/约定和函数结果返回依赖于体系结构,因此您的代码在Windows/Linux上工作而在Mac上不工作也就不足为奇了。

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

  1. 在返回之前,将一个值放入寄存器中,并且
  2. 让调用者为返回值传递一块堆栈内存,并将该值写入该块[更多信息]

#1通常与任何适合寄存器的内容一起使用#2表示其他一切(大型结构、数组等)。

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

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

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

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

第三,在Windows中,它在调试模式下工作,但如果您在Release下编译,它就不会。

以下是在调试下编译的:

    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

所以这就是它工作的原因。

正如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可能就是你所知道的。

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

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

当然,前面的值​​这些数据中的一个将在随后向堆栈上推送新数据的过程中被销毁。

因此,您可能很幸运,在函数调用和返回周围代码的过程中,没有向堆栈添加任何内容。

关于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编译器(在调试模式下,在英特尔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编译器在两个示例中都发出警告4716。默认情况下,此警告升级为错误。