Return Values
How a function hands its result back to the caller in eax/rax.
Return Values
Arguments flow into a function through rdi, rsi, and friends. The result flows back out through a single register: rax. This is the other half of the calling-convention contract, and it is wonderfully simple. When a function computes an integer or pointer result, it leaves that value in rax, and the caller knows to pick it up from there the moment the call returns.
For int-sized results you will usually see the 32-bit name eax, the lower half of rax. You have actually seen this since Chapter 1: the square function ended with its answer in eax. Now we look at the handoff from both sides.
The Callee Puts the Result in eax
Here is about the simplest function that returns something:
#include <iostream>
int getAnswer()
{
return 42;
}
int main()
{
int result{getAnswer()};
std::cout << result << '\n';
return 0;
}
At -O0, getAnswer is almost nothing but the return:
getAnswer():
push rbp
mov rbp, rsp
mov eax, 42
pop rbp
ret
Strip the prologue and epilogue and one instruction remains: mov eax, 42. That is the entire job of "return 42": place 42 in eax, then ret. The return statement in C++ compiles, in essence, to "move the result into eax (or rax) and execute ret." There is no separate "return value" mechanism in the hardware; the convention simply declares that rax is where results live.
The Caller Reads eax After the call
Now watch main collect the result:
main:
push rbp
mov rbp, rsp
sub rsp, 16
call getAnswer()
mov DWORD PTR -4[rbp], eax
mov eax, DWORD PTR -4[rbp]
mov esi, eax
...
The instruction immediately after call getAnswer() is mov DWORD PTR -4[rbp], eax. The caller takes whatever is in eax, the return value, and stores it into the local slot for result. This is the universal pattern: a value read out of rax/eax right after a call is the return value being collected. Just as "moves into the argument registers, then call" marks arguments going in, "call, then a read of eax" marks the result coming out.
(The subsequent mov eax, DWORD PTR -4[rbp] is just -O0 reloading result to pass it onward to std::cout, the same store-then-reload plumbing you have seen throughout.)
Returning a Computed Value
The constant 42 is a special case. More often the result is computed, and the compiler arranges for the final value to land in eax by the time ret runs. Recall compute from the prologue lesson; its last three instructions were:
mov edx, DWORD PTR -4[rbp]
mov eax, DWORD PTR -8[rbp]
add eax, edx
pop rbp
ret
The function ends with add eax, edx, which leaves the sum in eax, and then immediately returns. The compiler did not need a special "load the return value" step; it simply ensured the last arithmetic instruction wrote its result into eax. Whenever you reach a ret, you can assume the integer return value is already sitting in eax/rax, because the convention guarantees the compiler put it there.
Size Follows the Type
Which name you see depends on the return type's size, exactly as with arguments:
- A
boolorcharresult usesal(the low byte ofrax). - A
shortusesax. - An
intuseseax. - A
longor a pointer uses the fullrax.
They are all the same physical register, just different widths. So if you see a function finishing with a value in rax rather than eax, that is a hint the return type is 64-bit (a long, or a pointer). (Floating-point results are the exception: a double or float comes back in xmm0, not rax, mirroring how floating-point arguments use the xmm registers.)
Why eax Is Caller-Saved
It is no accident that rax is on the caller-saved list from the calling-convention lesson. A register that exists to carry results out obviously cannot be expected to survive a call unchanged; producing the result is the whole point. So the convention marks it volatile, and a caller that needs some other value cannot keep it in rax across a call. Seeing it this way ties the two lessons together: the argument registers and rax are caller-saved precisely because they are the channel through which a call communicates.
A Glance at the Optimizer
With -O2, getAnswer collapses to exactly what you would hope:
getAnswer():
mov eax, 42
ret
The frame is gone; only the essential "put 42 in eax, return" survives. The return mechanism is so minimal that even unoptimized code was nearly there; optimization just removes the surrounding frame.
Key Takeaways
- Integer and pointer return values come back in
rax(eaxfor anint); the size follows the type (al,ax,eax,rax). - A
returncompiles to "put the value inrax/eax, thenret"; there is no separate hardware return mechanism, only the convention. - The callee leaves the result in
eaxbeforeret; the caller readseaxright after thecall. - "A read of
eaximmediately after acall" is the recognisable pattern for collecting a return value. raxis caller-saved precisely because it is the channel results travel through.- Floating-point results return in
xmm0, notrax.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Return Values - Quiz
Test your understanding of the lesson.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!