LectureNotes 13
LectureNotes 13
13
Subroutines
Program Flow
Till now we have accumulated the very basic tools of assembly language
programming. A very important weapon in our arsenal is the conditional jump
instruction. During the course of last two chapters we used these tools to write two very
useful algorithms of sorting and multiplication. The multiplication algorithm is useful
even though there is a MUL instruction in the 8088 instruction set, which can multiply
8bit and 16bit operands. This is because of the extensibility of our algorithm, as it is
not limited to 16bits and can do 32bit or 64bit multiplication with minor changes.
Both of these algorithms will be used a number of times in any program of a
reasonable size and complexity. An application does not only need to multiply at a
single point in code; it multiplies at a number of places. If multiplication or sorting is
needed at 100 places in code, copying it 100 times is a totally infeasible solution.
Maintaining such a code is an impossible task.
The straightforward solution to this problem using the concepts we have acquainted
till now is to write the code at one place with a label, and whenever we need to sort we
jump to this label. But there is problem with this logic, and the problem is that after
sorting is complete how the processor will know where to go back. The immediate
answer is to jump back to a label following the jump to bubble sort. But we have
jumped to bubble sort from 100 places in code. Which of the 100 positions in code
should we jump back? Jump back at the first invocation, but jump has a single fixed
target. How will the second invocation work? The second jump to bubble sort will never
have control back at the next line.
Instruction are tied to one another forming an execution thread, just like a knitted
thread where pieces of cotton of different sizes are twisted together to form a thread.
This thread of execution is our program. The jump instruction breaks this thread
permanently, making a permanent diversion, like a turn on a highway. The conditional
jump selects one of the two possible directions, like right or left turn on a road. So there
is no concept of returning.
However there are roundabouts on roads as well that take us back from where we
started after having traveled on the boundary of the round. This is the concept of a
temporary diversion. Two or more permanent diversions can take us back from where
we started, just like two or more road turns can take us back to the starting point, but
they are still permanent diversions in their nature.
We need some way to implement the concept of temporary diversion in assembly
language. We want to create a roundabout of bubble sort, another roundabout of our
multiplication algorithm, so that we can enter into the roundabout whenever we need it
and return back to wherever we left from after completing the round.
Program
Bubble Sort
Swap
Key point in the above discussion is returning to where we left from, like a loop in a
knitted thread. Diversion should be temporary and not permanent. The code of bubble
sort written at one place, multiply at another, and we temporarily divert to that place,
thus avoiding a repetition of code at a 100 places.
Example 5.1
01 ; bubble sort algorithm as a subroutine
02 [org 0x0100]
03 jmp start
04
05 data: dw 60, 55, 45, 50, 40, 35, 25, 30, 10, 0
06 swap: db 0
07
08 bubblesort: dec cx ; last element not compared
09 shl cx, 1 ; turn into byte count
10
11 mainloop: mov si, 0 ; initialize array index to zero
12 mov byte [swap], 0 ; reset swap flag to no swaps
13
14 innerloop: mov ax, [bx+si] ; load number in ax
15 cmp ax, [bx+si+2] ; compare with next number
16 jbe noswap ; no swap if already in order
17
18 mov dx, [bx+si+2] ; load second element in dx
19 mov [bx+si], dx ; store first number in second
20 mov [bx+si+2], ax ; store second number in first
21 mov byte [swap], 1 ; flag that a swap has been done
22
23 noswap: add si, 2 ; advance si to next index
24 cmp si, cx ; are we at last index
25 jne innerloop ; if not compare next two
26
27 cmp byte [swap], 1 ; check if a swap has been done
28 je mainloop ; if yes make another pass
29
30 ret ; go back to where we came from
31
32 start: mov bx, data ; send start of array in bx
33 mov cx, 10 ; send count of elements in cx
34 call bubblesort ; call our subroutine
35
36 mov ax, 0x4c00 ; terminate program
37 int 0x21
08-09 The routine has received the count of elements in CX. Since it makes
one less comparison than the number of elements it decrements it.
Then it multiplies it by two since this a word array and each element
14 takes two bytes. Left shifting has been used to multiply by two.
Base+index+offset addressing has been used. BX holds the start of
array, SI the offset into it and an offset of 2 when the next element is
32-37
to be read. BX can be directly changed but then a separate counter
would be needed, as SI is directly compared with CX in our case.
The code starting from the start label is our main program
analogous to the main in the C language. BX and CX hold our
parameters for the bubblesort subroutine and the CALL is made to
invoke the subroutine.
Inside the debugger we observe the same unsigned data that we are so used to now.
The number 0103 is passed via BX to the subroutine which is the start of our data and
the number 000A via CX which is the number of elements in our data. If we step over
the CALL instruction we see our data sorted in a single step and we are at the
termination instructions. The processor has jumped to the bubblesort routine, executed
it to completion, and returned back from it but the process was hidden due to the step
over command. If however we trace into the CALL instruction, we land at the first
instruction of our routine. At the end of the routine, when the RET instruction is
executed, we immediately land back to our termination instructions, to be precise the
instruction following the CALL.
Also observe that with the CALL instruction SP is decremented by two from FFFE to
FFFC, and the stack windows shows 0150 at its top. As the RET is executed SP is
recovered and the 0150 is also removed from the stack. Match it with the address of the
instruction following the CALL which is 0150 as well. The 0150 removed from the stack
by the RET instruction has been loaded into the IP register thereby resuming execution
from address 0150. CALL placed where to return on the stack for the RET instruction.
The stack is automatically used with the CALL and RET instructions. Stack will be
explained in detail later, however the idea is that the one who is departing stores the
address to return at a known place. This is the place using which CALL and RET
coordinate. How this placed is actually used by the CALL and RET instructions will be
described after the stack is discussed.
After emphasizing reusability so much, it is time for another example which uses the
same bubblesort routine on two different arrays of different sizes.
Example 5.2
01 ; bubble sort subroutine called twice
02 [org 0x0100]
03 jmp start
04
05 data: dw 60, 55, 45, 50, 40, 35, 25, 30, 10, 0
06 data2: dw 328, 329, 898, 8923, 8293, 2345, 10, 877, 355, 98
07 dw 888, 533, 2000, 1020, 30, 200, 761, 167, 90, 5
08 swap: db 0
09
10 bubblesort: dec cx ; last element not compared
11 shl cx, 1 ; turn into byte count
12
13 mainloop: mov si, 0 ; initialize array index to zero
14 mov byte [swap], 0 ; reset swap flag to no swaps
15
16 innerloop: mov ax, [bx+si] ; load number in ax
17 cmp ax, [bx+si+2] ; compare with next number
18 jbe noswap ; no swap if already in order
19
20 mov dx, [bx+si+2] ; load second element in dx
21 mov [bx+si], dx ; store first number in second
22 mov [bx+si+2], ax ; store second number in first
23 mov byte [swap], 1 ; flag that a swap has been done
24
25 noswap: add si, 2 ; advance si to next index
26 cmp si, cx ; are we at last index
27 jne innerloop ; if not compare next two
28
29 cmp byte [swap], 1 ; check if a swap has been done
30 je mainloop ; if yes make another pass
31
32 ret ; go back to where we came from
33
34 start: mov bx, data ; send start of array in bx
35 mov cx, 10 ; send count of elements in cx
36 call bubblesort ; call our subroutine
37
38 mov bx, data2 ; send start of array in bx
39 mov cx, 20 ; send count of elements in cx
40 call bubblesort ; call our subroutine again
41
42 mov ax, 0x4c00 ; terminate program
43 int 0x21
05-07 There are two different data arrays declared. One of 10 elements and
the other of 20 elements. The second array is declared on two lines,
where the second line is continuation of the first. No additional label
34-40 is needed since they are situated consecutively in memory.
The other change is in the main where the bubblesort subroutine is
called twice, once on the first array and once on the second.
Inside the debugger observe that stepping over the first call, the first array is sorted
and stepping over the second call the second array is sorted. If however we step in SP is
decremented and the stack holds 0178 which is the address of the instruction following
the call. The RET consumes that 0178 and restores SP. The next CALL places 0181 on
the stack and SP is again decremented. The RET consumes this number and execution
resumes from the instruction at 0181. This is the coordinated function of CALL and
RET using the stack.
In both of the above examples, there is a shortcoming. The subroutine to sort the
elements is destroying the registers AX, CX, DX, and SI. That means that the caller of
this routine has to make sure that it does not hold any important data in these
registers before calling this function, because after the call has returned the registers
will be containing meaningless data for the caller. With a program containing
thousands of subroutines expecting the caller to remember the set of modified registers
for each subroutine is unrealistic and unreasonable. Also registers are limited in
number, and restricting the caller on the use of register will make the caller’s job very
tough. This shortcoming will be removed using the very important system stack.