I learned that data can be pushed on top of the stack, peeked at the top and popped off it and that it contains function parameters, return addresses, local variables and such. Say a function has three local variables which are pushed on the stack, how does the program access the two ones that are not on the top? Sorry if that's a dumb question.
There's a stack pointer and the program is aware of where it's pointing and what are the parameters. So, it can access the parameters using SP + offset (the offset is known from the size of the parameters). It's a little bit more complex, but that's the idea.
The "stack" analogy is only partially accurate. You have the basic idea correct, but there's more to it than just that.
In reality, the "stack" is just a flat chunk of memory living out its life. Think of it like a bunch of empty slots. When you add something onto the stack (pushing), you fill up a slot. Nothing moves; you just use up the next available chunk of space. There's a counter that the program has which keeps track of which space is available next. When you push, the counter goes one way; when you pop, it goes the other way. So if your slots are numbered (have "addresses") you just need to know the number or address of the one that is currently the "top" of the stack.
Now. Suppose you are a function, and you have two parameters. You know the first parameter takes a certain number of slots in the stack. To get to the second parameter, all you have to do is take the "top" number from the stack, adjust it by the size of the first parameter, and you'll get the slot number of the second parameter. Since the stack is just flat memory, you can read that like any other piece of memory.
Basically, the stack is a convention that is used by the program, but not actually enforced. You can in reality access any of the stack "slots" at a whim. The pushing/popping bit is just an organizational guideline.