This is a question my dad always used to ask me. "Why doesn't it just run through all the instructions and stop at the end?"
Let's take a look at a pathological example. The following code was compiled in Microchip's C18 compiler for the PIC18:
void main(void)
{
}
It produces the following assembler output:
addr opco instruction
---- ---- -----------
0000 EF63 GOTO 0xc6
0002 F000 NOP
0004 0012 RETURN 0
.
. some instructions removed for brevity
.
00C6 EE15 LFSR 0x1, 0x500
00C8 F000 NOP
00CA EE25 LFSR 0x2, 0x500
00CC F000 NOP
.
. some instructions removed for brevity
.
00D6 EC72 CALL 0xe4, 0 // Call the initialisation code
00D8 F000 NOP //
00DA EC71 CALL 0xe2, 0 // Here we call main()
00DC F000 NOP //
00DE D7FB BRA 0xd6 // Jump back to address 00D6
.
. some instructions removed for brevity
.
00E2 0012 RETURN 0 // This is main()
00E4 0012 RETURN 0 // This is the initialisation code
As you can see, main() is called, and at the end contains a return statement, although we didn't explicitly put it there ourselves. When main returns, the CPU executes the next instruction which is simply a GOTO to go back to the beginning of the code. main() is simply called over and over again.
Now, having said this, this is not the way people would do things usually. I have never written any embedded code which would allow main() to exit like that. Mostly, my code would look something like this:
void main(void)
{
while(1)
{
wait_timer();
do_some_task();
}
}
So I would never normally let main() exit.
"OK ok" you saying. All this is very interesting that the compiler makes sure there's never a last return statement. But what happens if we force the issue? What if I hand coded my assembler, and didn't put a jump back to the beginning?
Well, obviously the CPU would just keep executing the next instructions. Those would look something like this:
addr opco instruction
---- ---- -----------
00E6 FFFF NOP
00E8 FFFF NOP
00EA FFFF NOP
00EB FFFF NOP
.
. some instructions removed for brevity
.
7EE8 FFFF NOP
7FFA FFFF NOP
7FFC FFFF NOP
7FFE FFFF NOP
The next memory address after the last instruction in main() is empty. On a microcontroller with FLASH memory, an empty instruction contains the value 0xFFFF. On a PIC at least, that op code is interpreted as a 'nop', or 'no operation'. It simply does nothing. The CPU would continue executing those nops all the way down the memory to the end.
What's after that?
At the last instruction, the CPU's instruction pointer is 0x7FFe. When the CPU adds 2 to its instruction pointer, it gets 0x8000, which is considered an overflow on a PIC with only 32k FLASH, and so it wraps around back to 0x0000, and the CPU happily continues executing instructions back at the beginning of the code, just as if it had been reset.
You also asked about the need to power down. Basically you can do whatever you want, and it depends on your application.
If you did have an application that only needed to do one thing after power on, and then do nothing else you could just put a while(1); at the end of main() so that the CPU stops doing anything noticeable.
If the application required the CPU to power down, then, depending on the CPU, there will probably be various sleep modes available. However, CPUs have a habit of waking up again, so you'd have to make sure there was no time limit to the sleep, and no Watch Dog Timer active, etc.
You could even organise some external circuitry that would allow the CPU to completely cut its own power when it had finished. See this question: Using a momentary push button as a latching on-off toggle switch.