Skip to content

Instantly share code, notes, and snippets.

@iankronquist
Last active July 21, 2016 20:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iankronquist/70db0748e9384c28c541fea1eb984f41 to your computer and use it in GitHub Desktop.
Save iankronquist/70db0748e9384c28c541fea1eb984f41 to your computer and use it in GitHub Desktop.

We're going to focus on x86 because it's the easiest to understand. I'll then explain how x86_64 is more efficient.

So the stack is where local variables are stored. It is just a chunk of memory available for the program. The top of the stack is stored in a special register called the stack pointer, or esp on x86. It grows from higher addresses to lower addresses. To put a value on the stack you can use the push instruction.

push 42  # Push the literal number 42 to the stack
push eax # Push the value in the eax register to the stack. The value of eax
         # does not change!
push esp # You should be able to guess what this does.

To take a value off the stack you can use the pop instruction

pop eax # Take the value at the top of the stack and put it in the eax
        # register. Then add 4 bytes to the stack pointer. The previous value
        # of the eax register is destroyed.

Now, to go to somewhere in the program you can use the jmp or jump instruction. It's basically a goto. You could use this for calling functions, but you wouldn't know where you came from. You can store the address of the code you came from on the stack with push, so calling a function could be expressed like this:

# Let some_function be the address of some function
push esp  # Save the location of the code you were executing
jmp  some_function  # Go directly to some_function and execute code from there.

Fortunately x86 has an assembly instruction that does both of these in one step. It's called call

# Does the same thing as above.
call some_function

Now, once we are done executing, we need to get back to where we came from. We can use a combination of pop and jmp.

call some_function

some_function:
	# work here
	pop eax
	jmp eax

Fortunately there is a single instruction which does pop and jmp at the same time without overwriting any of the registers. It is called ret.

I need to introduce one more instruction really quick, called mov. It's like the cp command for registers.

mov eax, ebx  # Move the value in ebx into eax

Now, how do we pass arguments to the function? On x86 using the c decl/Posix ABI calling convention, arguments are passed by the stack. The return value is always stored in the eax register. So if you have a function like this:

int func(int a, int b) {
	return a + b;
}

int main() {
	return func(1,2);
}

That could be compiled into this:

main:
	push 1
	push 2
	call some_function
	add esp, 8
	ret

some_function:
	mov eax, [esp+4] # Put the number 2, which is the second argument, into the
	                 # eax register. The brackets mean "get the value at this
	                 # memory address" just like dereferencing the pointer.
	mov esi, [esp+8] # Put the number 1, which is the first argument, into the
	                 # esi register.
	add eax, esi     # eax = eax + esi
	ret

So before the function is called, the stack looks like this:

Address | value | location of esp
0x16: [...]
0x12: [ 1 ]
0x8:  [ 2 ] <- esp

And then once it's called:

0x16: [...]
0x12: [ 1 ]
0x8:  [ 2 ]
0x4:  [ address of some_function ] <- esp

After the return:

0x16: [...]
0x12: [ 1 ]
0x8:  [ 2 ] <- esp

After adding space back to the stack

0x16: [...] <- esp

Now, on x86, arguments are not passed via the stack. The first and second pointer or integer arguments are passed in the edi and esi registers respectively and any following arguments are passed via the stack. Structs are passed by the stack and floats are passed by the floating point registers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment