D-rk
D-rk

Reputation: 5919

How does returning values from a function work?

I recently had a serious bug, where I forgot to return a value in a function. The problem was that even though nothing was returned it worked fine under Linux/Windows and only crashed under Mac. I discovered the bug when I turned on all compiler warnings.

So here is a simple example:

#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;
}

My question is how can this work under Linux/Windows without crashing? How is the returning of values done on lower level?

Upvotes: 6

Views: 3105

Answers (8)

Bojan Komazec
Bojan Komazec

Reputation: 9526

Regarding the following statement from n3242 draft C++ Standard, paragraph 6.6.3.2, your example yields undefined behavior:

Flowing off the end of a function is equivalent to a return with no value; this results in undefined behavior in a value-returning function.

The best way to see what actually happens is to check the assembly code generated by the given compiler on a given architecture. For the following code:

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

int main()
{
    int n = foo(1, 2);
}

...VS2010 compiler (in Debug mode, on Intel 32-bit machine) generates the following assembly:

#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  
}

The result of addition operation in foo() is stored in eax register (accumulator) and its content is used as a return value of the function, moved to variable n.

eax is used to store a return value (pointer) in the following example as well:

#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;
    }
}

Assembly code:

#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 compiler issues warning 4716 in both examples. By default this warning is promoted to an error.

Upvotes: 0

Luchian Grigore
Luchian Grigore

Reputation: 258548

First off, you need to slightly modify your example to get it to compile. The function must have at least an execution path that returns a value.

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

Second, it's obviously undefined behavior, which means anything can happen, but I guess this answer won't satisfy you.

Third, in Windows it works in Debug mode, but if you compile under Release, it doesn't.

The following is compiled under 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 

The second instruction, the call to operator new, moves into eax the pointer to the newly created instance.

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

The calling context expects eax to contain the returned value, but it does not, it contains the last pointer allocated by new, which is incidentally, p.

So that's why it works.

Upvotes: 2

Sergey Kalinichenko
Sergey Kalinichenko

Reputation: 726479

There are two major ways for a compiler to return a value:

  1. Put a value in a register before returning, and
  2. Have the caller pass a block of stack memory for the return value, and write the value into that block [more info]

The #1 is usually used with anything that fits into a register; #2 is for everything else (large structs, arrays, et cetera).

In your case, the compiler uses #1 both for the return of new and for the return of your function. On Linux and Windows, the compiler did not perform any value-distorting operations on the register with the returned value between writing it into the pointer variable and returning from your function; on Mac, it did. Hence the difference in the results that you see: in the first case, the left-over value in the return register happened to co-inside with the value that you wanted to return anyway.

Upvotes: 2

Flot2011
Flot2011

Reputation: 4671

When popping values from the stack in IBM PC architecture there is no physical destruction of the old values ​​of data stored there. They just become unavailable through the operation of the stack, but still remain in the same memory cell.

Of course, the previous values ​​of these data will be destroyed during the subsequent pushing of new data on the stack.

So probably you are just lucky enough, and nothing is added to stack during your function's call and return surrounding code.

Upvotes: 0

Corbin
Corbin

Reputation: 33437

As Kerrek SB mentioned, your code has ventured into the realm of undefined behavior.

Basically, your code is going to compile down to assembly. In assembly, there's no concept of a function requiring a return type, there's just an expectation. I'm the most comfortable with MIPS, so I shall use MIPS to illustrate.

Assume you have the following code:

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

This is going to be translated to something like:

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

To add 5 and 4, the code would be called something like:

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

Note that unlike C, there's no explicit requirement that $v0 contain the return value, just an expectation. So, what happens if you don't actually push anything into $v0? Well, $v0 always has some value, so the value will be whatever it last was.

Note: This post makes some simplifications. Also, you're computer is likely not running MIPS... But hopefully the example holds, and if you learned assembly at a university, MIPS might be what you know anyway.

Upvotes: 1

hamstergene
hamstergene

Reputation: 24429

On Intel architecture, simple values (integers and pointers) are usually returned in eax register. This register (among others) is also used as temporary storage when moving values in memory and as operand during calculations. So whatever value left in that register is treated as the return value, and in your case it turned out to be exactly what you wanted to be returned.

Upvotes: 8

Martin James
Martin James

Reputation: 24847

Probably by luck, 'a' left in a register that happens to be used for returning single pointer results, something like that.

The calling/ conventions and function result returns are architecture-dependent, so it's not surprising that your code works on Windows/Linux but not on a Mac.

Upvotes: 3

Jurlie
Jurlie

Reputation: 1014

The way of returning of value from the function depends on architecture and the type of value. It could be done thru registers or thru stack. Typically in the x86 architecture the value is returned in EAX register if it is an integral type: char, int or pointer. When you don't specify the return value, that value is undefined. This is only your luck that your code sometimes worked correctly.

Upvotes: 0

Related Questions