2
\$\begingroup\$

When the ATmega starts there is only the Flash Program. Global Variables are stored at startup into the RAM and have a fix address to be accessible? How is this process called? It must be a really important part!

Then the program runs. When a local variable is initialized, the value is read out of the Flash memory? And it is stored first in a register and then on the stack? Why first into a register?

Where can I read about this?

\$\endgroup\$
0

4 Answers 4

5
\$\begingroup\$

The compiler may store the local variables within a function in the registers or on the stack. Accessing the stack is slower than accessing registers, because it requires a memory access, so registers are typically prioritised as a storage location.

Most architectures (AVR included) don't have memory-memory (MM) operand instructions, i.e. they can't operate on two memory locations at once. Typically, you instead have register-memory (RM) operands, register-register (RR) operands, register-immediate (RI) operands, or memory-immediate (MI) operands. The reason is that the processor would have to service two memory accesses (read or write) in a single instruction, and that complicates the design of the processor core.

The method of variable initialisation can vary. Consider the following code:

int foo = 0;

The compiler could allocate an int-sized zero value in the firmware's data block, and read it back. The compiled assembly would look something like this:

lds r24, 0x0060 ; read from data address 0x60 into r24 register
lds r25, 0x0061 ; read from data address 0x61 into r25 register

This assumes an 8-bit AVR MCU with a 16-bit int type. This causes two memory reads, and you've got to allocate two bytes in the firmware just for storing a zero. The assembly instructions themselves are also four bytes long each. So just for this initialisation we would've used 8 bytes of code, two bytes of data in the firmware blob, and performed two memory accesses.

Instead, the compiler can just make the initialisation part of the code, using a load immediate instruction:

ldi r24, 0x00 ; load zero into r24
ldi r25, 0x00 ; load zero into r25

This uses just two bytes per instruction, requires no extra space to store the initial value, and performs no memory accesses at runtime. The initialisation is performed using immediate values stored in the assembly itself.

However, if you initialise a local variable with the value of a mutable (meaning non-constant) global variable, like so:

int bar = 123;

int test() {
   int foo = bar;
   // ...
   return 0;
}

In this case the compiler can't assume that the value of bar will be 123 when the test() function is called, because the global variable value might have been altered. The initial value of the global variable is stored in the firmware's data section, which is loaded into RAM at boot. The compiler then copies the value from the memory address of bar into the registers that represent foo. This ends up looking exactly like our first example, where lds instructions were used to access memory.

However, if the compiler's optimisation pass can prove that the value of bar never changes throughout the program runtime (e.g. if you never set its value in your code) then it can instead treat it as if it were a constant value, and choose to have the foo variable initialised from immediates instead of memory.

Now, imagine you've got a program with lots of local variables. Eventually you'll run out of registers to store things in (this is known as register pressure), so you need to store the extra values elsewhere. That's where the stack comes in. You can push register values onto the stack, then pop them back out of the stack later when you need them again.

The stack is just a location in memory. As such, you can't (usually) directly pull data from memory into the stack in a single instruction. You must first load the value from memory into a register, then push the value in that register onto the stack.

Another complication comes with function calls. Let's say you have a function with a bunch of local variables, and those variables are stored in registers. You then call into another function, which also has a bunch of local variables, which also get stored in registers. What if those register usages collide? Well, if you don't handle this problem, the values that were in your registers before the call end up getting clobbered (i.e. overwritten) during the call, so all your local variable values end up wrong!

To solve this, we specify requirements for register preservation in a calling convention. A calling convention is a specification for how arguments are passed to functions (e.g. using register passing, the stack, etc.), which registers are used for return values, how register preservation is handled, what cleanup needs to be done at the end of a call, and which parts of each of these things the caller and callee are responsible for. This allows the compiler to know whether or not certain register values need to be preserved within a function call, which might mean that it needs to push some register values onto the stack (in what we call a "register save region") at the start of a call and pop them back off the stack afterwards.

When it comes to the actual storage and initialisation of global variables that are stored in memory, the details vary depending on the architecture.

8-bit AVR uses something called a modified Harvard architecture. This type of architecture has separate memory regions for data and code. The program flash on the AVR is split into two sections, and when the device boots it loads these sections into the data and code memory regions. This is a very simple system.

This is in contrast to von Neumann architectures like x86, where all memory is treated the same. Some ARM cores use von Neumann architectures, whereas others use modified Harvard architectures, so things vary a lot on the ARM side depending on which generation of ARM chips you're using. On von Neumann architectures you'll typically find some concept of memory page protection (read/write/execute flags) that allows you to differentiate between readable, writeable, and executable memory (and combinations thereof), and executables will contain separate sections for things like initialised data, uninitialised data, constant data, and executable code, each of which is loaded into a memory region with different memory protection.

\$\endgroup\$
4
\$\begingroup\$

For global variables, the linker generates a binary and the addresses of variables will be determined at that point. As a part of C startup code, contents of initialized variables is copied from flash to SRAM and the contents of uninitialized values are zeroed (depends on startup code, in some cases they might be just left uninitialized so you can't rely them being zero).

For local variables that reside in stack, they are left uninitialized and may have any value that there was in the stack, before the variable is assigned with an value. How that happens is dependent on what instructions you have on the CPU, and it may be done by first loading a register with a literal/immediate value and then that register is stored into SRAM address where the variable is.

You can read the C startup code of the compiler which is for your specific CPU, and you can read the assembly instruction opcode set for your specific CPU.

\$\endgroup\$
1
\$\begingroup\$

Local variables can, as has been mentioned already be in registers or on stack. For variables on stack, the compiler uses the 16 bit Y register (r29:r28) to keep track of where the stack was when the function was called, and uses the "LDD Rd, Y+q" and "STD Y+q, Rr" instructions to load the variables into registers for use/change. That way, the compiler only needs to know in advance which offset q each variable has, and the Y register can stay the same throughout the function. Load/store with Y pointer predecrement or postincrement can also be used, but then the original Y pointer also needs to be store somewhere like stack so stack frame isn't lost.

The reason it has to go through a register to get to RAM is because AVR is a load/store architecture, in that the only instructions that are able to read or write RAM are the load and store instruction families. All math and logic is performed purely on registers, and a memory copy takes the form of a load and store pair of instructions.

\$\endgroup\$
0
\$\begingroup\$

The process is called crt0.

This might be interesting: https://en.wikipedia.org/wiki/Crt0

\$\endgroup\$
1
  • \$\begingroup\$ yes I have never heard about this! It is interesting. Where have you learned that? I dont think yout typed in crt0 by accident in wiki right? \$\endgroup\$
    – jento
    Commented Feb 7, 2023 at 17:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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