
Charles B. answered 06/15/19
Software Engineer
Very good question! This question is unfortunately far broader than you think because you control both of those answers, and it depends on what you want to do in particular. You never just want to do something to the top of the stack. You want to do something with data. The top of the stack is a convenient place to put data, but it's meaningless. Data on the other hand, has meaning. Whether that data should be at the top of the stack and whether or not that is the right place to change the data depends on what you're doing. For example, I never want to "Add three to the top of the stack", but I might want to "Find the date for the next quarterly dev/qa lunch (the boss is paying) by adding three to the current month which happens to be stored at the top of the stack", or it could be stored off in memory somewhere and the top of the stack is just pointing to it which means I probably don't want to do something to the top of the stack or I could lose my data (and a free lunch)! But lets set that aside for now.
Firstly, if you want to do something to the data at the top of the stack you use push and pop. Push puts a new value on there and pop takes one off. To just figure out what's on the top of the stack you would pop it to a register (using Intel notation: [cmd] [destination] [source] so to put 42 into rax: mov rax 42):
pop rax
[do stuff to rax]
push rax
Now for a function call: with assembly there are conventions, but how you push parameters for a function call is up to you.
The rsp and the rbp are used to keep track of the stack frame. With rbp referring to the base pointer (the previous top of the stack) and rsp pointing (ideally) always to the current top of the stack.
One of the conventions I'm familiar with is:
push [parameter]
push rip //This is the instruction pointer for the current state of things
push rbp //This keeps track of the old stack frame
mov rbp rsp // This puts the base pointer at the current head of the stack pointing at the, just pushed, old rbp.
Now things are set up, so we do whatever the function was called to do, so to get the single parameter x we would look at the memory address two above the current base pointer.
So to access it you would use something like mov rbp+16 rax because there's one thing below it (the old RIP).
To illustrate, lets say x is 47, and we're somewhere off in the middle of some other call and we decide to call this function that takes parameter 'x':
So we have (numbers on the left represent relative memory location from RBP):
... [lots of stuff pushed to the stack by our awesome program] ...
0 [old RBP] <-- RBP <--RSP
Now we push stuff:
... [lots of stuff pushed to the stack by our awesome program] ...
24 [old RBP] // This was 0 above
16 [47] // Our parameter 'x'
8 [old RIP] // The old instruction pointer
0 [The previous RBP, points to the 'old RBP'] <-- RSP <--RBP
So now we're here. We should push the registers so we don't lose whatever we've got.
So we push a bunch of stuff.
24 [old rbp]
16 [47] // Parameter 'x'
8 [RIP]
0 [previous RBP, points to old RBP] <--RBP
...
?? [last register we pushed]<-- RSP
So now we want to play with X, so it's still just going to be
RBP+16.
There are, broadly speaking, two possibilities for X: x is just a number we need, or x is a memory address for a number we need. The way we mess with it depends on which one 'x' is, and that is entirely up to you.