2.4 Returning a struct

Let’s consider the following trivial C code:

struct X 
{ 
  int p, q; 
}
 
struct X makeX(int i, int j) 
{ 
  struct X v; 
 
  v.p = i; 
  v.q = j; 
  return v; 
}

This is actually allowed in C. The return value is a structure, which can easily be bigger than eax. In the case of a non-scalar return value, GCC uses the stack to store the return value, and pass a pointer of the reserved area to the subroutine also on the stack!

Let us consider the caller:

struct X w; 
w = makeX(23, 10);

Particularly, we should focus on the invocation. Because the actual return value is more important to the caller than the parameters, it is pushed (reserved first), before any parameters. Next, the parameters are pushed. Last, before the call instruction, the address of the reserved space on the stack is pushed. The C code yields the following assembly language code:

subl $X_size,%esp # reserve space 
movl %esp,%eax 
pushl $10 
pushl $23 
pushl %eax 
call makeX

Inside makeX, the labels are defined as follows:

oldEbp = 0 
retAddr = oldEbp + 4 
retValAddr = retAddr + 4 
p1 = retValAddr + 4 
p2 = p1 + 4 
v = oldEbp - X_size 
lastLocalVar = v

The return statement translates to “copy v to wherever the return value pointer points to. We can call memcpy to get this done:

pushl $X_size 
pushl %ebp 
addl $v,(%esp) 
pushl retValAddr(%ebp) 
call memcpy 
addl $12,%esp

It is important to note that GCC assumes the callee to deallocate the pointer to the area reserved for the return value. Normally, this is a little hard to do, though not impossible. Using familiar instructions, this can be done by the following code at the end of the callee in place of the ret instruction:

popl %eax # retrieve the return address 
addl $4,%esp # free the pointer to the return value area 
jmp *%eax # continue execution to the return address

The Intel x86 instruction set has a short cut to do this. The return instruction, ret, can take an optional operand to specify the number of bytes to deallocate from the stack after the return address is popped. In other words, we can also use the following instruction to accomplish the same:

ret $4

After the return back to the caller, the caller needs to do the following:

addl $8,%esp # reclaim space for params, retValPtr is deallocated by callee 
movl %esp,%eax # esp points to the ret val. 
# use memcpy to copy to w 
pushl X_size # how many bytes 
pushl %eax # from where 
pushl %ebp # to where 
addl $w,(%esp) # assume w is a local var 
call memcpy # w = ... (copy the structure) 
addl $12,%esp

As you can see, returning a structure can be done. However, it comes at a high price. It is usually much better off passing a pointer of the structure to the subroutine. In other words, our makeX function is much more reasonable as follows:

void makeX(struct X *pX, int i, int j) 
{ 
  pX->p = i; 
  pX->q = j; 
}

This subroutine is much more efficient in serveral ways. First, we don’t need to reserve the space for two struct X structures (one for the return value, one for the local variable). Second, we also save two memcpy calls. Last, but not least, it is much easier to manage the stack this way.

One exception to the rule that makes returning structures useful is when the returned value is immediately used as a parameter to another subroutine. Although the efficiency of passing a pointer is still superior, the tedium of needing to declare intermediate variables to store results costs more in terms of ease of programming.