The Art of
ASSEMBLY LANGUAGE PROGRAMMING

Chapter Eleven (Part 4)

Table of Content

Chapter Eleven (Part 6) 

CHAPTER ELEVEN:
PROCEDURES AND FUNCTIONS (Part 5)
11.5.10 - Passing Parameters in the Code Stream

11.5.10 Passing Parameters in the Code Stream

Another place where you can pass parameters is in the code stream immediately after the call instruction. The print routine in the UCR Standard Library package provides an excellent example:

                print
byte    "This parameter is in the code stream."
0

Normally a subroutine returns control to the first instruction immediately following the call instruction. Were that to happen here the 80x86 would attempt to interpret the ASCII code for "This..." as an instruction. This would produce undesirable results. Fortunately you can skip over this string when returning from the subroutine.

So how do you gain access to these parameters? Easy. The return address on the stack points at them. Consider the following implementation of print:

MyPrint         proc    near
push    bp
mov     bp
sp
push    bx
push    ax
mov     bx
2[bp]       ;Load return address into BX
PrintLp:        mov     al
cs:[bx]     ;Get next character
cmp     al
0           ;Check for end of string
jz      EndStr
putc                    ;If not end
print this char
inc     bx              ;Move on to the next character
jmp     PrintLp

EndStr:         inc     bx              ;Point at first byte beyond zero
mov     2[bp]
bx       ;Save as new return address
pop     ax
pop     bx
pop     bp
ret
MyPrint         endp

This procedure begins by pushing all the affected registers onto the stack. It then fetches the return address at offset 2[BP] and prints each successive character until encountering a zero byte. Note the presence of the cs: segment override prefix in the mov al cs:[bx] instruction. Since the data is coming from the code segment this prefix guarantees that MyPrint fetches the character data from the proper segment. Upon encountering the zero byte MyPrint points bx at the first byte beyond the zero. This is the address of the first instruction following the zero terminating byte. The CPU uses this value as the new return address. Now the execution of the ret instruction returns control to the instruction following the string.

The above code works great if MyPrint is a near procedure. If you need to call MyPrint from a different segment you will need to create a far procedure. Of course the major difference is that a far return address will be on the stack at that point - you will need to use a far pointer rather than a near pointer. The following implementation of MyPrint handles this case.

MyPrint         proc    far
push    bp
mov     bp
sp
push    bx              ;Preserve ES
AX
and BX
push    ax
push    es

les     bx
2[bp]       ;Load return address into ES:BX
PrintLp:        mov     al
es:[bx]     ;Get next character
cmp     al
0           ;Check for end of string
jz      EndStr
putc                    ;If not end
print this char
inc     bx              ;Move on to the next character
jmp     PrintLp

EndStr:         inc     bx              ;Point at first byte beyond zero
mov     2[bp]
bx       ;Save as new return address
pop     es
pop     ax
pop     bx
pop     bp
ret
MyPrint         endp

Note that this code does not store es back into location [bp+4]. The reason is quite simple - es does not change during the execution of this procedure; storing es into location [bp+4] would not change the value at that location. You will notice that this version of MyPrint fetches each character from location es:[bx] rather than cs:[bx]. This is because the string you're printing is in the caller's segment that might not be the same segment containing MyPrint.

Besides showing how to pass parameters in the code stream the MyPrint routine also exhibits another concept: variable length parameters. The string following the call can be any practical length. The zero terminating byte marks the end of the parameter list. There are two easy ways to handle variable length parameters. Either use some special terminating value (like zero) or you can pass a special length value that tells the subroutine how many parameters you are passing. Both methods have their advantages and disadvantages. Using a special value to terminate a parameter list requires that you choose a value that never appears in the list. For example MyPrint uses zero as the terminating value so it cannot print the NULL character (whose ASCII code is zero). Sometimes this isn't a limitation. Specifying a special length parameter is another mechanism you can use to pass a variable length parameter list. While this doesn't require any special codes or limit the range of possible values that can be passed to a subroutine setting up the length parameter and maintaining the resulting code can be a real nightmare[5].

Although passing parameters in the code stream is an ideal way to pass variable length parameter lists you can pass fixed length parameter lists as well. The code stream is an excellent place to pass constants (like the string constants passed to MyPrint) and reference parameters. Consider the following code that expects three parameters by reference:

Calling sequence:

                call    AddEm
word    I
J
K

Procedure:

AddEm           proc    near
push    bp
mov     bp
sp
push    si
push    bx
push    ax
mov     si
[bp+2]      ;Get return address
mov     bx
cs:[si+2]   ;Get address of J
mov     ax
[bx]        ;Get J's value
mov     bx
cs:[si+4]   ;Get address of K
add     ax
[bx]        ;Add in K's value
mov     bx
cs:[si]     ;Get address of I
mov     [bx]
ax        ;Store result
add     si
6           ;Skip past parms
mov     [bp+2]
si      ;Save return address
pop     ax
pop     bx
pop     si
pop     bp
ret
AddEm           endp

This subroutine adds J and K together and stores the result into I. Note that this code uses 16 bit near pointers to pass the addresses of I J and K to AddEm. Therefore I J and K must be in the current data segment. In the example above AddEm is a near procedure. Had it been a far procedure it would have needed to fetch a four byte pointer from the stack rather than a two byte pointer. The following is a far version of AddEm:

AddEm           proc    far
push    bp
mov     bp
sp
push    si
push    bx
push    ax
push    es
les     si
[bp+2]      ;Get far ret adrs into es:si
mov     bx
es:[si+2]   ;Get address of J
mov     ax
[bx]        ;Get J's value
mov     bx
es:[si+4]   ;Get address of K
add     ax
[bx]        ;Add in K's value
mov     bx
es:[si]     ;Get address of I
mov     [bx]
ax        ;Store result
add     si
6           ;Skip past parms
mov     [bp+2]
si      ;Save return address
pop     es
pop     ax
pop     bx
pop     si
pop     bp
ret
AddEm           endp

In both versions of AddEm the pointers to I J and K passed in the code stream are near pointers. Both versions assume that I J and K are all in the current data segment. It is possible to pass far pointers to these variables or even near pointers to some and far pointers to others in the code stream. The following example isn't quite so ambitious it is a near procedure that expects far pointers but it does show some of the major differences. For additional examples see the exercises.

Calling sequence:

                call    AddEm
dword   I
J
K

Code:

AddEm           proc    near
push    bp
mov     bp
sp
push    si
push    bx
push    ax
push    es
mov     si
[bp+2]      ;Get near ret adrs into si
les     bx
cs:[si+2]   ;Get address of J into es:bx
mov     ax
es:[bx]     ;Get J's value
les     bx
cs:[si+4]   ;Get address of K
add     ax
es:[bx]     ;Add in K's value
les     bx
cs:[si]     ;Get address of I
mov     es:[bx]
ax     ;Store result
add     si
12          ;Skip past parms
mov     [bp+2]
si      ;Save return address
pop     es
pop     ax
pop     bx
pop     si
pop     bp
ret
AddEm           endp

Note that there are 12 bytes of parameters in the code stream this time around. This is why this code contains an add si 12 instruction rather than the add si 6 appearing in the other versions.

In the examples given to this point MyPrint expects a pass by value parameter it prints the actual characters following the call and AddEm expects three pass by reference parameters - their addresses follow in the code stream. Of course you can also pass parameters by value-returned by result by name or by lazy evaluation in the code stream as well. The next example is a modification of AddEm that uses pass by result for I pass by value-returned for J and pass by name for K. This version is slightly differerent insofar as it modifies J as well as I in order to justify the use of the value-returned parameter.

; AddEm(Result I:integer; ValueResult J:integer; Name K);
;
;       Computes        I:= J;
;                       J := J+K;
;
; Presumes all pointers in the code stream are near pointers.

AddEm           proc    near
push    bp
mov     bp
sp
push    si                      ;Pointer to parameter block.
push    bx                      ;General pointer.
push    cx                      ;Temp value for I.
push    ax                      ;Temp value for J.

mov     si
[bp+2]              ;Get near ret adrs into si

mov     bx
cs:[si+2]           ;Get address of J into bx
mov     ax
es:[bx]             ;Create local copy of J.
mov     cx
ax                  ;Do I:=J;

call    word ptr cs:[si+4]      ;Call thunk to get K's adrs
add     ax
[bx]                ;Compute J := J + K

mov     bx
cs:[si]             ;Get address of I and store
mov     [bx]
cx                ; I away.

mov     bx
cs:[si+2]           ;Get J's address and store
mov     [bx]
ax                ; J's value away.

add     si
6                   ;Skip past parms
mov     [bp+2]
si              ;Save return address
pop     ax
pop     cx
pop     bx
pop     si
pop     bp
ret
AddEm           endp

Example calling sequences:

; AddEm(I
J
K)

call    AddEm
word    I
J
KThunk

; AddEm(I
J
A[I])

call    AddEm
word    I
J
AThunk
.
.
.
KThunk          proc    near
lea     bx
K
ret
KThunk          endp

AThunk          proc    near
mov     bx
I
shl     bx
1
lea     bx
A[bx]
ret
AThunk          endp

Note: had you passed I by reference rather than by result in this example the call

		AddEm(I
J
A[i])

would have produced different results. Can you explain why?

Passing parameters in the code stream lets you perform some really clever tasks. The following example is considerably more complex than the others in this section but it demonstrates the power of passing parameters in the code stream and despite the complexity of this example how they can simplify your programming tasks.

The following two routines implement a for/next statement similar to that in BASIC in assembly language. The calling sequence for these routines is the following:

                call    ForStmt
word    <<LoopControlVar»
<<StartValue»
<<EndValue»
.
.
<< loop body statements»
.
.
call    Next

This code sets the loop control variable (whose near address you pass as the first parameter by reference) to the starting value (passed by value as the second parameter). It then begins execution of the loop body. Upon executing the call to Next this program would increment the loop control variable and then compare it to the ending value. If it is less than or equal to the ending value control would return to the beginning of the loop body (the first statement following the word directive). Otherwise it would continue execution with the first statement past the call to Next.

Now you're probably wondering "How on earth does control transfer to the beginning of the loop body?" After all there is no label at that statement and there is no control transfer instruction instruction that jumps to the first statement after the word directive. Well it turns out you can do this with a little tricky stack manipulation. Consider what the stack will look like upon entry into the ForStmt routine after pushing bp onto the stack:

Normally the ForStmt routine would pop bp and return with a ret instruction which removes ForStmt's activation record from the stack. Suppose instead ForStmt executes the following instructions:

                add     word ptr 2[b]
2        ;Skip the parameters.
push    [bp+2]                  ;Make a copy of the rtn adrs.
mov     bp
[bp]                ;Restore bp's value.
ret                             ;Return to caller.

Just before the ret instruction above the stack has the entries shown below:

Upon executing the ret instruction ForStmt will return to the proper return address but it will leave its activation record on the stack!

After executing the statements in the loop body the program calls the Next routine. Upon initial entry into Next (and setting up bp) the stack contains the entries appearing below[6]:

The important thing to see here is that ForStmt's return address that points at the first statement past the word directive is still on the stack and available to Next at offset [bp+6]. Next can use this return address to gain access to the parameters and return to the appropriate spot if necessary. Next increments the loop control variable and compares it to the ending value. If the loop control variable's value is less than the ending value Next pops its return address off the stack and returns through ForStmt's return address. If the loop control variable is greater than the ending value Next returns through its own return address and removes ForStmt's activation record from the stack. The following is the code for Next and ForStmt:

                .xlist
include         stdlib.a
includelib      stdlib.lib
.list

dseg            segment para public 'data'
I               word    ?
J               word    ?
dseg            ends

cseg            segment para public 'code'
assume  cs:cseg
ds:dseg

wp              textequ <word ptr>

ForStmt         proc    near
push    bp
mov     bp
sp
push    ax
push    bx
mov     bx
[bp+2]      ;Get return address
mov     ax
cs:[bx+2]   ;Get starting value
mov     bx
cs:[bx]     ;Get address of var
mov     [bx]
ax        ;var := starting value
add     wp [bp+2]
6    ;Skip over parameters
pop     bx
pop     ax
push    [bp+2]          ;Copy return address
mov     bp
[bp]        ;Restore bp
ret                     ;Leave Act Rec on stack
ForStmt         endp

Next            proc near
push    bp
mov     bp
sp
push    ax
push    bx
mov     bx
[bp+6]      ;ForStmt's rtn adrs
mov     ax
cs:[bx-2]   ;Ending value
mov     bx
cs:[bx-6]   ;Ptr to loop ctrl var
inc     wp [bx]         ;Bump up loop ctrl
cmp     ax
[bx]        ;Is end val < loop ctrl?
jl      QuitLoop

; If we get here
the loop control variable is less than or equal
; to the ending value. So we need to repeat the loop one more time.
; Copy ForStmt's return address over our own and then return

; leaving ForStmt's activation record intact.

mov     ax
[bp+6]      ;ForStmt's return address
mov     [bp+2]
ax      ;Overwrite our return address
pop     bx
pop     ax
pop     bp              ;Return to start of loop body
ret

; If we get here
the loop control variable is greater than the
; ending value
so we need to quit the loop (by returning to Next's
; return address) and remove ForStmt's activation record.

QuitLoop:       pop     bx
pop     ax
pop     bp
ret     4
Next            endp

Main            proc
mov     ax
dseg
mov     ds
ax
mov     es
ax
meminit

call    ForStmt
word    I
1
5
call    ForStmt
word    J
2
4
printf
byte    "I=%d
J=%d\n"
0
dword   I
J

call    Next            ;End of J loop
call    Next            ;End of I loop
print
byte    "All Done!"
cr
lf
0

Quit:           ExitPgm
Main            endp
cseg            ends
sseg            segment para stack 'stack'
stk             byte    1024 dup ("stack ")
sseg            ends
zzzzzzseg       segment para public 'zzzzzz'
LastBytes       byte    16 dup (?)
zzzzzzseg       ends
end     Main

The example code in the main program shows that these for loops nest exactly as you would expect in a high level language like BASIC Pascal or C. Of course this is not a particularly good way to construct a for loop in assembly language. It is many times slower than using the standard loop generation techniques (see Chapter Ten for more details on that). Of course if you don't care about speed this is a perfectly good way to implement a loop. It is certainly easier to read and understand than the traditional methods for creating a for loop. For another (more efficient) implementation of the for loop check out the ForLp macros in Chapter Eight.

The code stream is a very convenient place to pass parameters. The UCR Standard Library makes considerable use of this parameter passing mechanism to make it easy to call certain routines. Printf is perhaps the most complex example but other examples (especially in the string library) abound.

Despite the convenience there are some disadvantages to passing parameters in the code stream. First if you fail to provide the exact number of parameters the procedure requires the subroutine will get very confused. Consider the UCR Standard Library print routine. It prints a string of characters up to a zero terminating byte and then returns control to the first instruction following the zero terminating byte. If you leave off the zero terminating byte the print routine happily prints the following opcode bytes as ASCII characters until it finds a zero byte. Since zero bytes often appear in the middle of an instruction the print routine might return control into the middle of some other instruction. This will probably crash the machine. Inserting an extra zero which occurs more often than you might think is another problem programmers have with the print routine. In such a case the print routine would return upon encountering the first zero byte and attempt to execute the following ASCII characters as machine code. Once again this usually crashes the machine.

Another problem with passing parameters in the code stream is that it takes a little longer to access such parameters. Passing parameters in the registers in global variables or on the stack is slightly more efficient especially in short routines. Nevertheless accessing parameters in the code stream isn't extremely slow so the convenience of such parameters may outweigh the cost. Furthermore many routines (print is a good example) are so slow anyway that a few extra microseconds won't make any difference.


[5] Especially if the parameter list changes frequently.

[6] Assuming the loop does not push anything onto the stack or pop anything off the stack. Should either case occur the ForStmt/Next loop would not work properly.

Chapter Eleven (Part 4)

Table of Content

Chapter Eleven (Part 6) 

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