The AVR is a RISC architecture, so it has pretty basic hardware handling of interrupts. Most processors mess with the stack during interrupts, though there are a couple, most notably ARM and PowerPC, that use different methods.
In any case, this is what the AVR does for interrupts:
When an interrupt occurs, the processor hardware does these steps, which are not just a simple GOTO:
- Finish the current instruction.
- Disable the global interrupt flag.
- Push the address of the next instruction on the stack.
- Copy the address in the correct interrupt vector (according to the interrupt that occurred) into the program counter.
Now at this point, the hardware has done all it is going to do. The software has to be written correctly to not break things. Typically, the next steps are along these lines.
Push the status register onto the stack. (This must be done first before it is changed).
Push any CPU registers that will (or may be) changed onto the stack. Which registers that need to be saved in this way are defined by the programming model. The programming model is defined by the compiler.
Now the working interrupt code can be run. To answer the case in the question of calling a function, it just does what it always does, push the return value on the stack, then pop it back when done. This doesn't affect any of these previous values that we saved on the stack until now.
- Run ISR working code.
Now we are done and want to return from interrupt. First we have to do software cleanup.
- Pop the CPU registers we pushed in step 6.
- Pop the saved status value back into the status register. After this we have to be careful not to execute any instruction that could change the status register.
Execute the RTI instruction. The hardware does these steps for this instruction:
a. Enable the global interrupt flag. (Note that at least one instruction must be run before the next interrupt will be honored. This prevents heavy interrupts from fully blocking background work.)
b. Pop the saved return address into the PC.
Now we are back to normal code.
Note that there are some points where we have to be very careful, particularly around the status register and saving registers that could be changed. Luckily if you are using a C compiler, all of this is handled under the covers.
Also, you have to watch your stack depth. At any point when interrupts are enabled, an ISR could use more of the stack than is obvious by looking at local code. Of course, this really doesn't come up much unless you are pushing your memory to its limits.
Here is a link describing this process if you want a reference.