-1

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?

Sandeep
  • 1,245
  • 1
  • 13
  • 33
  • 3
    Does this answer your question? [Return a \`struct\` from a function in C](https://stackoverflow.com/questions/9653072/return-a-struct-from-a-function-in-c) – Progman Nov 19 '22 at 13:27
  • The lifetime of the local variable `bill` ends as the function returns. But the returned value is accessible to the caller. If it wasn't that would defeat the idea of a return value/object. It's quite possible that the implied copy doesn't take place. But that is by the by. – Persixty Nov 21 '22 at 17:58
  • Conceptually, `return bill` makes a copy of `bill` and hads it to the caller. For the caller, this copy is a temporary. `bill` itself is deleted, and the caller does with the copy what it wants. – n. m. could be an AI Nov 21 '22 at 18:20

2 Answers2

1

The stack frame is "deleted" right before the function returns. Usually the return value is stored from the stack into a register and then the function returns. Let's look under the hood a bit to see what's really going on. I'm going to omit the actual disassembly of your code since it's a bit overwhelming, but I'll summarize the key points here. Typically a function written in C does the following (I'm using x86 Assembly as an example, the process is similar on other architectures but the register names will be different)

First, to use a function, it must be CALLed.

CALL bar

Doing so pushes the contents of the rip register on the stack (which can be thought of as representing "what line of code we're going to run next.")

bar:
push rbp
mov rbp,rsp

Most functions written by a C compiler start out like this. The purpose of this is to create a stack frame. The contents of rbp are stored on the stack for safekeeping. Then, we copy the value of the stack pointer (rsp) to rbp. The reason C does this is simple: rsp can be altered by certain instructions such as push,pop,call, and ret, where rbp is not. In addition, the free space above the stack that hasn't been used yet can be used by the calling function.

Next, our local variables are stored onto the stack. One of them was the 56 we passed to bar. C chose to store the value 56 into the esi register prior to calling the function.

mov     DWORD PTR [rbp-12], esi

This basically means "take the contents of the esi register and store them 12 bytes before the address pointed to by rbp. This is guaranteed to be free space, thanks to the push rbp mov rbp,rsp sequence from earlier.

Once the function does what it needs to do, the return value is stored in rax and then we do the exit sequence.

pop rbp
ret

As for the stack frame, it wasn't actually "deleted" per se. Those values are temporarily still there until they are overwritten by another function. However, for all intents and purposes, they are considered deleted, as that stack space is now considered "free" and can be used by anything (such as hardware interrupts etc.) Therefore, after a function returns, there is no guarantee that any of its local values are still there if you try to access them. (Not that C would let you access them without inline assembly, but what I'm saying is you shouldn't even try.)

puppydrum64
  • 1,598
  • 2
  • 15
1

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

Chris Dodd
  • 119,907
  • 13
  • 134
  • 226