Search code examples
callstackfunction-call

Understanding how call return works


I am somewhat confused regarding exactly when the context of a called function gets deleted. I have read that the stackframe of the called function is popped off when it returns. I am trying to apply that knowledge to the following scenario where function foo calls function bar, which return a structure. The code could look something like this:

//...

struct Bill
{
   float amount;
   int id;
   char address[100];
};

//...

Bill bar(int);

//...

void foo() {
    // ...
    Bill billRecord = bar(56);
    //...
}

Bill bar(int i) {
    //...
    Bill bill = {123.56, 2347890, "123 Main Street"};
    //...
    return bill;
}

The memory for bill object in the function bar is from the stackframe of bar, which is popped off when bar returns. It appears to be still valid when the assignment of the returned structure is made to billRecord in foo.


So does it mean that the stackframe for bar is not deleted the instant it returns but only after the value returned by bar is used in foo?


Solution

  • You're right, there's this "hole" between when bar returns and foo copies the return value somewhere with an assignment operation. The way this works is that there is notionally some 'return value' space where the return value lives while being returned. So from the execution model, there are two copies of the return value -- from the local in bar to the return space and from the return space to billRecord in foo.

    Exactly how this works depends on the calling conventions. On x86_64, the "return value space" is in registers for small return values and in some memory controlled by the caller for larger return values. If the return value is larger than two registers worth, then the caller must pass a 'hidden' extra argument with a pointer to the space where the return value should be stored. bar will then copy its local variable into that space before deleting its stack frame and returning.

    So when compiling foo the compiler knows it needs to provide that extra hidden argument and knows it needs to allocate some space for it. If it is smart (and you enable optimization) it will simply re-use the space for billRecord for this (passing a pointer to billRecord as the hidden argument), and the assignment in foo will then be a noop (as it knows bar will do all the work)1`.

    If the compiler is smart when compiling bar it might do "return value optimization" and, realizing it is just going to return the local variable bill, allocate that local var in the return value space it got from its caller, rather than in its own stack frame.


    1Of course, it can only do this if it knows there's no way for bar to access billRecord directly. This requires what is known as "escape analysis" -- if the location of billRecord "escapes" from foo (for example, by taking its address and storing it somewhere or passing it as an argument somewhere), this optimization can't be done and it will need to allocate additional space in its stack frame for the return space in addition to that used by billRecord