A function call is a control transfer with rules for passing arguments, creating function-specific execution state, returning a value, and resuming the caller.

Learning Question

When C code calls a function, what happens below the source-level expression?

In C, a function call can look simple:

int value = add(2, 3);

At the source level, this means:

Call add with the arguments 2 and 3, then store the returned value in value.

At the machine level, several things must happen:

  1. Put argument values where the called function expects them.
  2. Transfer control to the called function.
  3. Let the called function run with its own execution state.
  4. Place the return value where the caller expects it.
  5. Return control to the instruction after the call.

The first mental model is:

A function call is not only a jump. It is a controlled handoff between caller and callee, organized by calling-convention rules and stack-frame state.

Caller and Callee

Two roles matter in a function call.

The caller is the function that makes the call.

The callee is the function being called.

In this example:

int main(void)
{
    int value = add(2, 3);
    printf("%d\n", value);
    return 0;
}

main is the caller when it calls add.

add is the callee.

During that call, main must be able to pause its own work, transfer control to add, receive a result, and continue afterward.

That is more structured than an ordinary jump.

Calling Convention

A calling convention is a set of rules for how functions call each other at the machine level.

It answers questions such as:

  • where argument values go
  • where the return value goes
  • which registers the caller may assume are preserved
  • which registers the callee may overwrite
  • how the stack is aligned
  • how control returns to the caller

This collection does not need the full calling convention yet.

But the idea matters because caller and callee must agree.

If the caller puts an argument in one place and the callee expects it somewhere else, the function call cannot work correctly.

Passing Arguments in the Example

The C call is:

int value = add(2, 3);

One possible GCC -O0 x86-64 assembly sequence in main includes:

movl    $3, %esi
movl    $2, %edi
call    add
movl    %eax, -4(%rbp)

The two movl instructions place argument values into registers:

movl    $3, %esi
movl    $2, %edi

On this platform, the first integer argument is passed in %edi, and the second integer argument is passed in %esi.

So the call prepares:

C ArgumentValueExample Location Before call
first argument to add2%edi
second argument to add3%esi

Do not treat these registers as universal function-call registers for every platform.

They are part of this x86-64 calling convention.

The durable lesson is:

A source-level argument list must become concrete argument placement before control transfers to the callee.

The call Instruction

After the arguments are prepared, the caller executes:

call    add

At a simplified level, call add does two things:

  1. Records where execution should resume after add returns.
  2. Transfers control to the machine-code location for add.

On x86-64, the return address is placed on the stack by the call instruction.

That return address tells the machine where to continue in the caller after the callee returns.

This is why a function call is not just “go to another label.”

The caller must be able to come back.

Entering the Callee

The generated assembly for add begins like this in the example:

add:
    endbr64
    pushq   %rbp
    movq    %rsp, %rbp

The add: label marks the entry point of the generated function.

The endbr64 instruction is platform/toolchain-related entry code in this example, not the core idea of C function calls.

The important stack-frame setup is:

pushq   %rbp
movq    %rsp, %rbp

At a high level, this saves the previous frame base and establishes a frame base for the current call to add.

The details will vary by compiler and optimization level.

The key idea is:

The callee may set up a stack frame so its generated code has a stable frame of reference for local storage and saved state.

Separate Calls Need Separate State

Each active function call needs its own execution state.

If main calls add, then main and add are not sharing one undifferentiated set of local variables.

main has its own current state.

add has its own current state while it is active.

If a function calls another function, the caller’s work must be resumable later.

Stack frames help support that structure.

At a simplified level:

main frame exists
main calls add
add frame exists while add runs
add returns
main continues using its own frame

This is why the stack matters for function calls.

It supports nested, active function executions.

The Callee Uses Its Arguments

Inside add, the example stores the incoming argument values into stack-frame locations:

movl    %edi, -20(%rbp)
movl    %esi, -24(%rbp)

Then it computes the result:

movl    -20(%rbp), %edx
movl    -24(%rbp), %eax
addl    %edx, %eax
movl    %eax, -4(%rbp)

This connects the previous chapters:

  • chapter 3 showed that expressions become instruction sequences
  • chapter 4 showed that registers are working space
  • chapter 5 showed that stack-frame offsets can store values for a function call

The function call chapter adds the missing context:

Those instructions run inside one active call of add, after main transfers control to it.

Returning a Value

The C function ends with:

return result;

The example assembly includes:

movl    -4(%rbp), %eax
popq    %rbp
ret

The instruction:

movl    -4(%rbp), %eax

places the return value in %eax.

On this platform, an int return value is returned in %eax.

Then:

popq    %rbp
ret

restores the previous frame base and returns control to the caller.

At a simplified level, ret uses the return address saved by the call mechanism to continue execution after the original call instruction.

The Caller Receives the Result

After add returns, main continues at the instruction after:

call    add

The example then stores the return value:

movl    %eax, -4(%rbp)

This corresponds to the C source:

int value = add(2, 3);

The returned integer value is in %eax, and the caller stores it in the stack-frame location used for value in this generated code.

This completes the call relationship:

caller places arguments
caller calls callee
callee computes value
callee places return value
callee returns
caller reads return value

Function Calls Are Control Flow

A function call changes which instruction executes next.

Before the call, the CPU is executing instructions in main.

During the call, the CPU executes instructions in add.

After ret, the CPU resumes instructions in main.

This is lower-level control flow.

C makes it look structured:

int value = add(2, 3);
printf("%d\n", value);

Assembly reveals the control transfer and return.

That does not make the C view wrong.

It means the C view is source-level structure built on lower-level control-flow mechanisms.

Function Calls Are Not Always Visible the Same Way

The example uses -O0, which keeps the call structure easy to inspect.

With optimization enabled, the compiler may:

  • keep argument values in registers without stack copies
  • omit a traditional frame pointer
  • inline a small function so no call instruction remains
  • remove unused local storage
  • use different instruction sequences while preserving behavior

So the exact assembly shape is not a permanent rule.

The durable rule is:

When a real call remains, caller and callee must still agree on argument passing, return values, preserved state, and control return.

What This Chapter Does Not Explain Yet

This chapter explains the basic machine-level shape of a function call.

It does not yet fully explain:

  • every x86-64 calling-convention rule
  • stack alignment
  • caller-saved and callee-saved registers in detail
  • recursion
  • inlining
  • dynamic linking
  • function pointers
  • system calls

Those topics build on the same boundary:

C function calls are source-level constructs implemented by lower-level argument placement, control transfer, stack-frame state, return-value placement, and return control.

Core Mental Model

Keep these boundaries separate:

  • A C function call is source-level syntax and meaning.
  • The caller prepares argument values according to calling rules.
  • The call instruction transfers control and records where to return.
  • The callee may set up a stack frame for its own active execution.
  • The callee places the return value where the caller expects it.
  • The ret instruction returns control to the caller.
  • Separate active calls need separate execution state.

When reading assembly around a call, ask:

Where are the arguments placed, where does control transfer, where is the return value placed, and where does execution resume?

Final Summary

A C function call is implemented by a disciplined machine-level handoff between caller and callee.

Assembly makes that handoff visible: arguments are placed, control transfers with call, the callee runs with its own frame state, the result is placed in the expected return location, and ret resumes the caller.