Just as an experiment, I tried the following:
void bar(int *);
void foo()
{
int x = 5;
int *p;
p = &x;
bar(p);
}
And here is what gcc translated that into (comments mine):
__SP_H__ = 0x3e
__SP_L__ = 0x3d
__tmp_reg__ = 0
foo:
push r28 ; save r28
push r29 ; save r29
rcall . ; make room on the stack
in r28, __SP_L__ ; copy SP into r29:r28 = Y: LSB...
in r29, __SP_H__ ; ... and MSB
ldi r24, lo8(5) ; store x in a register: LSB...
ldi r25, 0 ; ... and MSB
std Y+2, r25 ; copy x into the stack: MSB
std Y+1, r24 ; ... and LSB
movw r24, r28 ; r25:r24 = Y
adiw r24, 1 ; r25:r24 = Y+1 = &x
call bar ; call bar(&x);
pop __tmp_reg__ ; release the two bytes...
pop __tmp_reg__ ; ... of stack space
pop r29 ; restore r29
pop r28 ; restore r28
ret ; return
Expanded comments:
The instruction rcall is normally used to call another function. Here
it's calling the following instruction, so it doesn't affect the program
flow. It's only a trick: as rcall saves the return address into the
stack, it makes the stack grow by two bytes, and is used here as an
optimized way of reserving to bytes of stack space for storing x.
Next you see the stack pointer being copied into the r29:r28 register
pair, also known as “Y pointer”. The stack pointer is a CPU register
holding the address of the top of the stack, namely of the first free
slot. As the stack grows towards the bottom of the RAM, at this point
it looks like this:
address | SP = Y | Y+1 | Y+2 | Y+3 | Y+4 | Y+5 | Y+6 |...
--------------------------------------------------------------
data | (free) | room for x | saved Y | return addr. | ...
Next you see the value of x being placed first in a register pair
(r25:r24), then on the stack at the addresses Y+1 and Y+2.
The instruction mowv copies a register pair, so now you have a copy of
the stack pointer in the pair r25:r24. The instruction adiw adds an
immediate value (here, 1) to a register pair, so that now r25:r24 holds
the address of x. Finally, the function bar() is called: per the
calling convention, the argument is to be passed in the r25:r24 pair.
A few final remarks:
- I passed the pointer to another function just to make gcc believe that
I am actually using that pointer, otherwise it would optimize out
the whole code into a function that does noting but return.
- You don't need to create a pointer variable to hold the address of
x: calling just bar(&x) compiles into the very same assembly.
- Most local variables are assigned to CPU registers. Only because I am
using the address of
x did this variable get stored in the stack.
- The compiler output is very sensitive to the surrounding code, the
compiler version, the optimization options, and so on. So you should
consider all this as only an example.
- If you do not completely understand what each instruction is doing,
you should now take a look at the AVR Instruction Set Manual
LDS. It doesn't, the compiler does.