0

How is the address of a local variable retrieved in Arduino's ATMEGA328 micro-controller?

Lets say somewhere inside the main we have lines as:

int x = 5; //a variable declared and value of 5 stored in SRAM ??
int  *p; // a pointer is declared
p = &x; // p holds the address of the variable x

What happens above is &x retrieves the address of the variable x.

But how does this happen in a lower level? Which function retrieves the address of a variable and how does it do it? How does that function know the address of the variable x? I'm wondering what happens step by step after the instruction comes to the instruction register.

edit: Another question for int x = 5; is 5 stored at CPU register or SRAM? .

  • 1
    LDS. It doesn't, the compiler does. – Ignacio Vazquez-Abrams Dec 11 '16 at 5:04
  • oh do you mean when the code is running inside the micro-controller the address "was" retrieved already before by the compiler and the instruction code operating inside the microcontroller holds that address already? im sorry i just want to clarify. very new to subject. – user16307 Dec 11 '16 at 5:09
  • Yes, the compiler maintains a symbol table for all symbols in the code. And for any actual dynamic addresses there are indirect opcodes that work with those. – Ignacio Vazquez-Abrams Dec 11 '16 at 5:20
  • 1
    Are you asking: how does a compiler translate C++ code into assembler? This is rather a broad question. Perhaps better suited to Stack Overflow. – Nick Gammon Dec 11 '16 at 5:50
  • But how does this happen in a lower level? - you are asking how a compiler works. This is really nothing to do with the Arduino per se. – Nick Gammon Dec 11 '16 at 5:54
1

Another question for int x = 5; is 5 stored at CPU register or SRAM?

That depends entirely on how the compiler optimizes your code. If you never refer to x in your code the compiler may discard that statement entirely. Or it might use a CPU register. It might use RAM.

1

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
  • That manual says: "Registers concatenated with the X-, Y-, and Z-registers enabling indirect addressing of the whole data space on MCUs with more than 64KB data space, and constant data fetch on MCUs with more than 64KB program space" Does that mean X Y Z registers are never used in an ordinary Arduino UNO since it has ATMEGA328 which has only 2KB RAM? – user16307 Dec 12 '16 at 11:59
  • @user16307: You are quoting the description of the RAMPX, RAMPY and RAMPZ registers, which are not the same as X, Y and Z. The ATmega328P does not have RAMP registers. – Edgar Bonet Dec 12 '16 at 13:26
0

To answer your question you first have to understand what exactly happens when a function is called.

The very first operation, in what is known as the function prologue, a stack frame is allocated. This stack frame contains, amongst other things:

  • The return address of the function
  • Copies of any CPU registers that the function uses
  • Storage space for any local variables

This may be through the use of a dedicated PUSH instruction or by manual manipulation of the stack pointer and writing of data direct to memory, depending on the CPU architecture.

During this process the compiler says "This variable is stored at stack offset X" for each variable. From that point on there is no "variable" as such, only a stack offset. All accesses to the variable are actually accesses to the memory at the location it already knows about within the stack frame.

When you then access the address of a variable you are basically saying "instead of getting me the contents of this memory address, just give me the memory address". Since the compiler already knows what that offset is it just adds it to the address of the stack frame and gives you that.

So if your stack frame is at 0x500 (for example) and the variable x is at offset 0x004:

  • Accessing x gives you the contents of address 0x500 + 0x004
  • Accessing &x gives you the value 0x500 + 0x004

Assigning p to &x creates another variable in the stack frame (which could be at offset 0x006 say). To that variable is assigned the value 0x500 + 0x004 (or 0x504 of course).

However, that is all from the point of view of a non-optimising compiler. The compiler itself may go "There is no need to store this value in memory, I have enough registers to keep it internally". So it doesn't actually allocate anything on the stack for the variable, it just "remembers" it in a register. That makes the function shorter and faster since there are less memory accesses.

There are a couple of variable attributes that affect how these decisions are made. The first is volatile which tells the compiler that "this variable must be stored in RAM and, further to that, don't trust that the value in RAM is the same since I last looked at it". This is essential for variables shared between the main loop and an interrupt context, since the compiler doesn't know if the interrupt is going to change the value of the variable.

volatile int foo;

The second is the register attribute. This suggests to the compiler that it might like to store this variable in a register to make things faster. This flag is seldom seen any more since the optimiser generally does a much better job of working out which variables would fit better in a register than a human can. It's very much a deprecated attribute now.

register int foo;
  • I'm mostly asking what happens inside the uC after the compilation. When the micro-controller running the program counter(PC) starts to fetch instruction codes to Instruction register So you see there is a movement here. Im talking about this movement inside the uC. I can understand how the program data ca be sent to a CPU register by the micro code. It decodes the data from the instruction register and does its job. But what kind of instruction makes the micro code to retrieve the address of a variable stored and how does it do is mystery to me. But I will reread your answer again and again – user16307 Dec 11 '16 at 12:53
  • There is no instruction that retrieves the address of a variable. There is no variable. There is only the address at which the compiler has placed the contents of the variable (the offset in the stack frame). It's what you do with that address that counts. You can use it as a number (it's just a number after all) or you can retrieve the value from memory at that address. – Majenko Dec 11 '16 at 13:29
  • The address is the variable. The variable is the address. We just give it a name for our own convenience. The compiler just knows it as (SP+4) (for example). – Majenko Dec 11 '16 at 13:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.