Copy Link
Add to Bookmark
Report
NULL mag Issue 04 11 Assembly Tutorial part 4
Introduction to subroutines
Subroutines are functions. They are reusable pieces of code that can be
called by your program to perform various repeatable tasks. Subroutines are
declared using labels just like we've used before (eg. _start:) however
we don't use the JMP instruction to get to them - instead we use a new
instruction CALL. We also don't use the JMP instruction to return to our
program after we have run the function. To return to our program from
a subroutine we use the instruction RET instead. Why don't we JMP to
subroutines?
The great thing about writing a subroutine is that we can reuse it. If we
want to be able to use the subroutine from anywhere in the code we would
have to write some logic to determine where in the code we had jumped from
and where we should jump back to. This would litter our code with unwanted
labels. If we use CALL and RET however, assembly handles this problem for
us using something called the stack. Introduction to the stack
The stack is a special type of memory. It's the same type of memory that
we've used before however it's special in how it is used by our program.
The stack is what is call Last In First Out memory (LIFO). You can think of
the stack like a stack of plates in your kitchen. The last plate you put on
the stack is also the first plate you will take off the stack next time you
use a plate.
The stack in assembly is not storing plates though, its storing values.
You can store a lot of things on the stack such as variables, addresses
or other programs. We need to use the stack when we call subroutines to
temporarily store values that will be restored later.
Any register that your function needs to use should have it's current
value put on the stack for safe keeping using the PUSH instruction. Then
after the function has finished it's logic, these registers can have
their original values restored using the POP instruction. This means that
any values in the registers will be the same before and after you've
called your function. If we take care of this in our subroutine we can
call functions without worrying about what changes they're making to our
registers.
The CALL and RET instructions also use the stack. When you CALL a
subroutine, the address you called it from in your program is pushed onto
the stack. This address is then popped off the stack by RET and the program
jumps back to that place in your code. This is why you should always JMP to
labels but you should CALL functions.
; Hello World Program (Subroutines) Compile with: nasm -f elf
; helloworld-len.asm Link with (64 bit systems require elf_i386 option): ld
; -m elf_i386 helloworld-len.o -o helloworld-len Run with: ./helloworld-len
SECTION .data
msg db 'Hello, brave new world!', 0Ah
SECTION .tex
global _start
_start:
mov eax, msg ; move the address of our message string into EAX call
strlen ; call our function to calculate the length of the string
mov edx, eax ; our function leaves the result in EAX mov ecx, msg ;
this is all the same as before mov ebx, 1 mov eax, 4 int 80h
mov ebx, 0 mov eax, 1 int 80h
strlen: ; this is our first function declaration
push ebx ; push the value in EBX onto the stack to preserve it while we use
; EBX in this function
mov ebx, eax
; move the address in EAX into EBX (Both point to the same
;segment in memory)
nextchar: ; this is the same as lesson3
cmp byte [eax], 0
jz finished
inc eax
jmp nextchar
finished:
sub eax, ebx
pop ebx ; pop the value on the stack back into EBX
ret ; return to where the function was called
~$ nasm -f elf helloworld-len.asm
~$ ld -m elf_i386 helloworld-len.o -o helloworld-len
~$ ./helloworld-len
Hello, brave new world!