The Art of
ASSEMBLY LANGUAGE PROGRAMMING

Chapter Eleven (Part 5)

Table of Content

Chapter Eleven (Part 7) 

CHAPTER ELEVEN:
PROCEDURES AND FUNCTIONS (Part 6)
11.5.11 - Passing Parameters via a Parameter Block
11.6 - Function Results
11.6.1 - Returning Function Results in a Register
11.6.2 - Returning Function Results on the Stack
11.6.3 - Returning Function Results in Memory Locations
11.7 - Side Effects

11.5.11 Passing Parameters via a Parameter Block

Another way to pass parameters in memory is through a parameter block. A parameter block is a set of contiguous memory locations containing the parameters. To access such parameters, you would pass the subroutine a pointer to the parameter block. Consider the subroutine from the previous section that adds J and K together, storing the result in I; the code that passes these parameters through a parameter block might be.

Calling sequence:

ParmBlock       dword   I
I               word    ?               ;I, J, and K must appear in
J               word    ?               ; this order.
K               word    ?
                 .
                 .
                 .
                les     bx, ParmBlock
                call    AddEm
                 .
                 .
                 .
AddEm           proc    near
                push    ax
                mov     ax, es:2[bx]    ;Get J's value
                add     ax, es:4[bx]    ;Add in K's value
                mov     es:[bx], ax     ;Store result in I
                pop     ax
                ret
AddEm           endp

Note that you must allocate the three parameters in contiguous memory locations.

This form of parameter passing works well when passing several parameters by reference, because you can initialize pointers to the parameters directly within the assembler. For example, suppose you wanted to create a subroutine rotate to which you pass four parameters by reference. This routine would copy the second parameter to the first, the third to the second, the fourth to the third, and the first to the fourth. Any easy way to accomplish this in assembly is

; Rotate-               On entry, BX points at a parameter block in the data
;               segment that points at four far pointers. This code
;               rotates the data referenced by these pointers.

Rotate          proc    near
                push    es              ;Need to preserve these
                push    si              ; registers
                push    ax

                les     si, [bx+4]      ;Get ptr to 2nd var
                mov     ax, es:[si]     ;Get its value
                les     si, [bx]        ;Get ptr to 1st var
                xchg    ax, es:[si]     ;2nd->1st, 1st->ax
                les     si, [bx+12]     ;Get ptr to 4th var
                xchg    ax, es:[si]     ;1st->4th, 4th->ax
                les     si, [bx+8]      ;Get ptr to 3rd var
                xchg    ax, es:[si]     ;4th->3rd, 3rd->ax
                les     si, [bx+4]      ;Get ptr to 2nd var
                mov     es:[si], ax     ;3rd -> 2nd

                pop     ax
                pop     si
                pop     es
                ret
Rotate          endp

To call this routine, you pass it a pointer to a group of four far pointers in the bx register. For example, suppose you wanted to rotate the first elements of four different arrays, the second elements of those four arrays, and the third elements of those four arrays. You could do this with the following code:

                lea     bx, RotateGrp1
                call    Rotate
                lea     bx, RotateGrp2
                call    Rotate
                lea     bx, RotateGrp3
                call    Rotate
                 .
                 .
                 .
RotateGrp1              dword   ary1[0], ary2[0], ary3[0], ary4[0]
RotateGrp2              dword   ary1[2], ary2[2], ary3[2], ary4[2]
RotateGrp3              dword   ary1[4], ary2[4], ary3[4], ary4[4]

Note that the pointer to the parameter block is itself a parameter. The examples in this section pass this pointer in the registers. However, you can pass this pointer anywhere you would pass any other reference parameter - in registers, in global variables, on the stack, in the code stream, even in another parameter block! Such variations on the theme, however, will be left to your own imagination. As with any parameter, the best place to pass a pointer to a parameter block is in the registers. This text will generally adopt that policy.

Although beginning assembly language programmers rarely use parameter blocks, they certainly have their place. Some of the IBM PC BIOS and MS-DOS functions use this parameter passing mechanism. Parameter blocks, since you can initialize their values during assembly (using byte, word, etc.), provide a fast, efficient way to pass parameters to a procedure.

Of course, you can pass parameters by value, reference, value-returned, result, or by name in a parameter block. The following piece of code is a modification of the Rotate procedure above where the first parameter is passed by value (its value appears inside the parameter block), the second is passed by reference, the third by value-returned, and the fourth by name (there is no pass by result since Rotate needs to read and write all values). For simplicity, this code uses near pointers and assumes all variables appear in the data segment:

; Rotate-       On entry, DI points at a parameter block in the data
;               segment that points at four pointers. The first is
;               a value parameter, the second is passed by reference,
;               the third is passed by value/return, the fourth is
;               passed by name.

Rotate          proc    near
                push    si              ;Used to access ref parms
                push    ax              ;Temporary
                push    bx              ;Used by pass by name parm
                push    cx              ;Local copy of val/ret parm

                mov     si, [di+4]      ;Get a copy of val/ret parm
                mov     cx, [si]

                mov     ax, [di]        ;Get 1st (value) parm
                call    word ptr [di+6] ;Get ptr to 4th var
                xchg    ax, [bx]        ;1st->4th, 4th->ax
                xchg    ax, cx          ;4th->3rd, 3rd->ax
                mov     bx, [di+2]      ;Get adrs of 2nd (ref) parm
                xchg    ax, [bx]        ;3rd->2nd, 2nd->ax
                mov     [di], ax        ;2nd->1st

                mov     bx, [di+4]      ;Get ptr to val/ret parm
                mov     [bx], cx        ;Save val/ret parm away.

                pop     cx
                pop     bx
                pop     ax
                pop     si
                ret
Rotate          endp

A reasonable example of a call to this routine might be:

I               word    10
J               word    15
K               word    20
RotateBlk       word    25, I, J, KThunk
                 .
                 .
                 .
                lea     di, RotateBlk
                call    Rotate
                 .
                 .
                 .
KThunk          proc    near
                lea     bx, K
                ret
KThunk          endp
11.6 Function Results

Functions return a result, which is nothing more than a result parameter. In assembly language, there are very few differences between a procedure and a function. That is probably why there aren't any "func" or "endf" directives. Functions and procedures are usually different in HLLs, function calls appear only in expressions, subroutine calls as statements[7]. Assembly language doesn't distinguish between them.

You can return function results in the same places you pass and return parameters. Typically, however, a function returns only a single value (or single data structure) as the function result. The methods and locations used to return function results is the subject of the next three sections.

11.6.1 Returning Function Results in a Register

Like parameters, the 80x86's registers are the best place to return function results. The getc routine in the UCR Standard Library is a good example of a function that returns a value in one of the CPU's registers. It reads a character from the keyboard and returns the ASCII code for that character in the al register. Generally, functions return their results in the following registers:

Use                     First                     Last 
Bytes:                  al, ah, dl, dh, cl, ch, bl, bh 
Words:                  ax, dx, cx, si, di, bx 
Double words:           dx:ax                           On pre-80386
                        eax, edx, ecx, esi, edi, ebx    On 80386 and later.
16-bitOffsets:          bx, si, di, dx
32-bit Offsets          ebx, esi , edi, eax, ecx, edx
Segmented Pointers:     es:di, es:bx, dx:ax, es:si      Do not use DS.

Once again, this table represents general guidelines. If you're so inclined, you could return a double word value in (cl, dh, al, bh). If you're returning a function result in some registers, you shouldn't save and restore those registers. Doing so would defeat the whole purpose of the function.

11.6.2 Returning Function Results on the Stack

Another good place where you can return function results is on the stack. The idea here is to push some dummy values onto the stack to create space for the function result. The function, before leaving, stores its result into this location. When the function returns to the caller, it pops everything off the stack except this function result. Many HLLs use this technique (although most HLLs on the IBM PC return function results in the registers). The following code sequences show how values can be returned on the stack:

        function PasFunc(i,j,k:integer):integer;
        begin
                PasFunc := i+j+k;
        end;

        m := PasFunc(2,n,l);

In assembly:

PasFunc_rtn     equ     10[bp]
PasFunc_i       equ     8[bp]
PasFunc_j       equ     6[bp]
PasFunc_k       equ     4[bp]

PasFunc         proc    near
                push    bp
                mov     bp, sp
                push    ax
                mov     ax, PasFunc_i
                add     ax, PasFunc_j
                add     ax, PasFunc_k
                mov     PasFunc_rtn, ax
                pop     ax
                pop     bp
                ret     6
PasFunc         endp

Calling sequence:

                push    ax              ;Space for function return result
                mov     ax, 2
                push    ax
                push    n
                push    l
                call    PasFunc
                pop     ax              ;Get function return result

On an 80286 or later processor you could also use the code:

                push    ax              ;Space for function return result
                push    2
                push    n
                push    l
                call    PasFunc
                pop     ax              ;Get function return result

Although the caller pushed eight bytes of data onto the stack, PasFunc only removes six. The first "parameter" on the stack is the function result. The function must leave this value on the stack when it returns.

11.6.3 Returning Function Results in Memory Locations

Another reasonable place to return function results is in a known memory location. You can return function values in global variables or you can return a pointer (presumably in a register or a register pair) to a parameter block. This process is virtually identical to passing parameters to a procedure or function in global variables or via a parameter block.

Returning parameters via a pointer to a parameter block is an excellent way to return large data structures as function results. If a function returns an entire array, the best way to return this array is to allocate some storage, store the data into this area, and leave it up to the calling routine to deallocate the storage. Most high level languages that allow you to return large data structures as function results use this technique.

Of course, there is very little difference between returning a function result in memory and the pass by result parameter passing mechanism. See "Pass by Result" for more details.

11.7 Side Effects

A side effect is any computation or operation by a procedure that isn't the primary purpose of that procedure. For example, if you elect not to preserve all affected registers within a procedure, the modification of those registers is a side effect of that procedure. Side effect programming, that is, the practice of using a procedure's side effects, is very dangerous. All too often a programmer will rely on a side effect of a procedure. Later modifications may change the side effect, invalidating all code relying on that side effect. This can make your programs hard to debug and maintain. Therefore, you should avoid side effect programming.

Perhaps some examples of side effect programming will help enlighten you to the difficulties you may encounter. The following procedure zeros out an array. For efficiency reasons, it makes the caller responsible for preserving necessary registers. As a result, one side effect of this procedure is that the bx and cx registers are modified. In particular, the cx register contains zero upon return.

ClrArray        proc    near
                lea     bx, array
                mov     cx, 32
ClrLoop:        mov     word ptr [bx], 0
                inc     bx
                inc     bx
                loop    ClrLoop
                ret
ClrArray        endp

If your code expects cx to contain zero after the execution of this subroutine, you would be relying on a side effect of the ClrArray procedure. The main purpose behind this code is zeroing out an array, not setting the cx register to zero. Later, if you modify the ClrArray procedure to the following, your code that depends upon cx containing zero would no longer work properly:

ClrArray        proc    near
                lea     bx, array
ClrLoop:        mov     word ptr [bx], 0
                inc     bx
                inc     bx
                cmp     bx, offset array+32
                jne     ClrLoop
                ret
ClrArray        endp

So how can you avoid the pitfalls of side effect programming in your procedures? By carefully structuring your code and paying close attention to exactly how your calling code and the subservient procedures interface with one another. These rules can help you avoid problems with side effect programming:

These rules, like all other rules, were meant to be broken. Good programming practices are often sacrificed on the altar of efficiency. There is nothing wrong with breaking these rules as often as you feel necessary. However, your code will be difficult to debug and maintain if you violate these rules often. But such is the price of efficiency[8]. Until you gain enough experience to make a judicious choice about the use of side effects in your programs, you should avoid them. More often than not, the use of a side effect will cause more problems than it solves.


[7] "C" is an exception to this rule. C's procedures and functions are all called functions. PL/I is another exception. In PL/I, they're all called procedures.

[8] This is not just a snide remark. Expert programmers who have to wring the last bit of performance out of a section of code often resort to poor programming practices in order to achieve their goals. They are prepared, however, to deal with the problems that are often encountered in such situations and they are a lot more careful when dealing with such code.

Chapter Eleven (Part 5)

Table of Content

Chapter Eleven (Part 7) 

Chapter Eleven: Procedures and Functions (Part 6)
27 SEP 1996