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
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.
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.
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.
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.