OK, here's a full(ish) story. Obviously compilers are free to implement C in any way they want, but this is usually what they do:
Your variables and data are split between three areas of memory.
Static and global variables are in one area; behind the scenes there is no difference between a static variable and a global variable (the compiler merely pretends it doesn't know about static variables when they're not in scope). This area of memory is utterly static. Its size and the locations of all the things in it are fixed at compile time and never change. In typical PCs they'll be addressed via the processor's data segment register (ds, which holds the segment part of a pointer). Global and static variables therefore lead to assembler instructions like
mov ds:[1234], 45
Local variables go on the stack. This is merely an area of memory that grows and shrinks like a pile of papers. When a function is called, its return address is put on the stack so the processor can find its way back to the calling routine, together with the local variables and any parameters of the function. Since you can't slide papers out from the middle of the pile, only lift them from the top, it stands to reason that variables in the stack space stay there until the end of the function. The compiler doesn't have to do anything clever at the end of the function. It merely returns the stack pointer (the thing that indicates where the current end of the stack is) to the value it was at the start, before the function was called. This automatically frees everything. Typically in a PC stack variables will be addressed by the stack segment register ss (holds the segment bit of a pointer) and often the "bp" register holding a start address within the stack, for the area of memory used by the current function. Stack variables therefore give instructions like
mov ss:[bp+6], 45
The area of the stack that's in use obviously grows and shrinks, but it mustn't grow into memory that's already in use. This is why compilers provide an option to set the (maximum) size of the stack.
Any left-over memory is designated "heap", and this is what you're using when you malloc and free things. Heap management is obviously much more complicated than global variable or local variable management, because you can allocate and free at random, and the heap can become fragmented. It will also fill up with unfreed things if you're not careful (i.e. things you've finished with, and to which you have no remaining reference, but which you've never told anyone you no longer need). Hence Microsoft's garbage collected heap system in Visual C++, where freeing happens automatically when there is no longer a pointer pointing to some bit of the heap.
And incidentally, that is the other thing about heap stuff; regardless as to how you make it happen, at assembler level, it will always be referred to via a pointer operation:
(1) les si, ds:[1234] (take an address from the global or static variable at ds:[1234] and put address in registers es:si); you could also use a local variable here.
(2) mov es:[si], 45 (now take number 45 and put it in the heap at address referred to by es:[si])
Since Microsoft visual C++ supports both a garbage collected heap and a normal heap, I have no idea how the two interact. If anyone can explain, I'd be very grateful.