The Stack: Push and Pop

CS 301: Assembly Language Programming Lecture, Dr. Lawlor

Like C++ variables, registers are actually available in several sizes:

Curiously, you can write a 64-bit value into rax, then read off the low 32 bits from eax, or the low 16 bitx from ax, or the low 8 bits from al--it's just one register, but they keep on extending it!

rax: 64-bit
eax: 32-bit
ax: 16-bit
ah al

For example,

mov rcx,0xf00d00d2beefc03; load a big 64-bit constant
mov eax,ecx; pull out low 32 bits (0x2beefc03)

(Try this in NetRun now!)

Here's the full list of x86 registers.  The 64 bit registers are shown in red.  "Scratch" registers any function is allowed to overwrite, and use for anything you want without asking anybody.  "Preserved" registers have to be put back ("save" the register) if you use them.

Notes Type
Values are returned from functions in this register. 
rax eax ax ah and al
Typical scratch register.  Some instructions also use it as a counter. scratch
rcx ecx cx ch and cl
Scratch register. scratch
rdx edx dx dh and dl
Preserved register: don't use it without saving it! preserved
rbx ebx bx bh and bl
The stack pointer.  Points to the top of the stack (details below) preserved rsp esp sp spl
Preserved register.  Sometimes used to store the old value of the stack pointer, or the "base". preserved rbp ebp bp bpl
Scratch register.  Also used to pass function argument #2 in 64-bit Linux scratch rsi esi si sil
Scratch register.  Function argument #1 in 64-bit Linux scratch rdi edi di dil
Scratch register.  These were added in 64-bit mode, so they have numbers, not names.
scratch r8 r8d r8w r8b
Scratch register. scratch r9 r9d r9w r9b
Scratch register. scratch r10 r10d r10w r10b
Scratch register. scratch r11 r11d r11w r11b
Preserved register. preserved r12 r12d r12w r12b
Preserved register. preserved r13 r13d r13w r13b
Preserved register. preserved r14 r14d r14w r14b
Preserved register. preserved r15 r15d r15w r15b

The Stack

"The Stack" is a frequently-used area of memory designed for functions to use as temporary storage.  This is normally where you store values while calling another function: you can't store values in the scratch registers, because the function could change them.  

The easiest and most common way to use the stack is with the dedicated "push" and "pop" instructions.

For example, this loads 3 into rax and returns.  It's a kinda roundabout way to return a 3, but it lets you use rax for something else until you need it.

push 3
pop rax

(Try this in NetRun now!)

For a more complicated example, this loads 23 into rax, and then 17 into rcx:

push 17
push 23
pop rax
pop rcx

(Try this in NetRun now!)

After the first "push", the stack just has one value:
After the second "push", the stack has two values:
    17  23
So the first "pop" picks up the 23, and puts it in rax, leaving the stack with one value:
The second "pop" picks up that value, puts it in rcx, leaving the stack clean.  If the stack was not clean, everything actually works fine except "ret", which jumps to whatever is on the top of the stack.  Let me say that again:


If you do not pop *exactly* the same number of times as you push, your program will crash.

So be careful with your pushes and pops!

Saving Registers with Push and Pop

You can use push and pop to save registers at the start and end of your function.  For example, "rbp" is a preserved register, so you need to save its value before you can use it:

push rbp ; save old copy of this register

mov rbp,23
mov rax,rbp

pop rbp ; restore main's copy from the stack

(Try this in NetRun now!)

Main might be storing something important in rbp, and will complain if you just change it, but as long as you put it back exactly how it was before you return, main is perfectly happy letting you use it!  Without the push and pop, main will be annoyed that you messed with its stuff, which in a real program often means a strange and difficult to debug crash.

If you have multiple registers to save and restore, be sure to pop them in the *opposite* order they were pushed:

push rbp ; save old copy of this register
push r15

mov rbp,23
mov rax,rbp

pop r15 ; restore main's copy from the stack
pop rbp

(Try this in NetRun now!)

One big advantage to saved registers: you can call other functions, and know that the registers values won't change (because they'll be saved).  All the scratch registers, by contrast, are likely to get overwritten by any function you call. 

When I'm writing a long function that calls a bunch of stuff, I tend to work mostly in saved registers, which I push and pop at the start and end of my function to keep main from getting annoyed.

You can also save a scratch register, to keep some other function from messing with it.  You do this by pushing your value before calling a function, then popping it afterwards to bring your copy back:

mov rax,17; say I want to keep this value while calling a function...
push rax; save rax to the stack

mov rdi,3 ; now call the function
extern print_long
call print_long

pop rax; restore rax afterwards, and safely return 17
(Try this in NetRun now!)

Again, you can save as many registers as you want, but you need to pop them in the opposite order--otherwise you've flipped their values around!

For a short function where I only call a few other functions, I tend to work in scratch registers, and save the few things I need before calling other functions.