Parameter Passing Via Flags Example: The GetDecDigit procedure below reads a character from the keyboard and returns it in the AL register. The subroutine signals that a decimal digit was read by clearing the CF; otherwise the CF is set to indicate an "error condition". ;-----------------------------------------------------GetDecDigit PROC NEAR ; Entry Point mov ah, 01h int 21h If01: cmp al, '0' jb Else01 cmp al, '9' ja Else01 Then01: clc jmp EndIf01 Else01: stc EndIf01: ; ; ; ; ; ; ; get character from keyboard character in AL register is it < '0' yes - not a digit is it > '9' yes - not a digit found digit - clear CF ; not a digit - set CF ret ; and return GetDecDigit ENDP ;------------------------------------------------The following calling sequence tests the Carry Flag, transferring control to a routine to handle the "error" if CF is set. ; get digit call GetDecDigit jc Not_Digit ; Return digit in AL ; If not a digit goto Not_Digit 1 Parameter Passing using Registers Example: The subroutine DisplayChar uses INT 21h Service 02h to display the character in the DL register. DisplayChar PROC NEAR push ax mov ah, 02h int 21h pop ax ret DisplayChar ENDP ; ; ; ; ; Entry Point save AX set INT 21h Service 02h display DL restore AX To display any character, load it into the DL register and call DisplayChar. push dx mov dl, char call DisplayChar pop dx ; ; ; ; save DX move character from memory into DL display it restore DX Example: The subroutine RotateLeft4 rotates the bits in the AX register 4 bits to the left. Its effect is to left rotate the hexadecimal digits in AX. RotateLeft4 PROC NEAR push cx mov cl, 04 rol ax, cl pop cx ret RotateLeft4 ENDP ; Entry Point ; save CX ; initialize CL for rotation ; restore CX The subroutine RotateLeft4 can be used to "rotate" the hexadecimal digits in any word X by first loading AX with the contents of X, calling RotateLeft4, then moving the result back to X (see code segment below). push ax mov ax, x call RotateLeft4 mov x, ax pop ax ; save AX ; move word to "rotate" to AX ; move result to x ; restore AX This code sequence is awkward since AX must first be saved on the stack, the word whose digits are to be rotated moved into the AX register, RotateLeft4 called, the results moved back, and AX restored. 2 Parameter Passing using Global Variables Example: The subroutine MaxOf2 (below) returns the larger of two integers. Declare three word-length variables in the Data Segment, A, B, and C and adopt the convention that MaxOf2 returns the larger of A and B in C. Thus A and B would function as "input" to the subroutine while C would function as "output". Note that there is only one Data Segment declaration; Variables A, B, and C should not be declared in a separately defined data segment. DATA SEGMENT A dw ? B dw ? C dw ? ..... DATA ENDS ; input for MaxOf2 ; input for MaxOf2 ; output for MaxOf2 ..... ;------------------------------------------------MaxOf2 PROC NEAR push ax ; save AX mov ax, A If02: cmp ax, B ; is A >= B ja EndIf02 ; yes - so keep A in AX Then02: mov ax, B ; no - so move B to AX EndIf02: mov C, ax ; move AX to C pop ax ; restore AX ret MaxOf2 ENDP ;-------------------------------------------------The calling sequence for MaxOf2 would have to "load" the variables A and B with the values to find the maximum of. Upon return the maximum value would be extracted from C. 3 Parameter Passing using the Stack - Pass by Value Example: Maximum Procedure ;---------------------------------------------------------------------MaxOf2_A PROC NEAR ; Entry Point push bp ; save the old BP register mov bp, sp ; BP points to top of "argument frame" mov ax, [bp+4] ; get A (top parameter) If03: cmp ax, [bp+6] ; compare with B (2nd parameter) ja EndIf03 ; A is larger Then03: mov ax, [bp+6] ; B is larger EndIf03: pop bp ; restore BP ret 4 ; return and pop off parameters MaxOf2_A ENDP ;-------------------------------------------------------------The calling sequence is push b push a call MaxOf2_A ; push B on stack ; push A on stack ; A is on top, B is 2nd ; larger of A and B is in AX register To understand parameter passing using the stack, let's examine the stack at various points during the execution of the subroutine MaxOf2_A. Start just before MaxOf2_A is called but after "b" and "a" are pushed , the stack has the following structure (note the stack pointer SP “points” to the top of the stack). | | +--------+ | a | <- SP +--------+ | b | +--------+ | | 4 Since "a" was pushed last, it's on top of the stack. When MaxOf2_A is called, the return address (old_ip) is pushed on the stack. | | +--------+ | old_ip | <- SP +--------+ | a | +--------+ | b | +--------+ | | The first two lines of the MaxOf2_A subroutine saves the old value of the BP register then sets BP to point to the top of the stack. The parameters on the stack plus the old_ip and old_bp values make up the "argument frame" for that particular procedure call. | | +--------+ | old_bp | <- SP = BP +--------+ | old_ip | +--------+ | a | <- [bp+4] +--------+ | b | <- [bp+6] +--------+ | | --------^ | argument frame | | | --------- 5 Parameter Passing using the Stack - Pass by Reference Example: Modify the above subroutine to return the largest value using indirection through the stack. First the calling sequence is mov ax,offset c push ax push b push a call MaxOf2_B ; ; ; ; ; move address of C to AX and push it on stack push B on stack push A on stack A is on top, B is 2nd, address of C is 3rd ; larger of A and B returned to C The address of c is pushed onto the stack followed by the values a and b. Note: Since there is no push immediate instruction, technically the instruction push offset c is illegal (although the assembler automatically substitutes an interesting work-around when it detects a push immediate instruction). This is why we use the instruction sequence mov ax, offset c; push ax. The code for the modified subroutine, MaxOf2_B is similar to MaxOf2_A in that is uses [bp+4] and [bp+6] to access the two values on the stack, compares them and puts the larger in AX. Then using [bp+8] to move the address of c to the BX register and executing the instruction mov [bx], ax it uses register indirection to return the value in AX to c. ;---------------------------------------------------------------MaxOf2_B PROC NEAR ; Entry Point push bp ; save the old BP register mov bp, sp ; set BP to point to the top of the stack push ax ; save registers push bx mov ax, [bp+4] ; get A (top parameter) If04: cmp ax, [bp+6] ; compare with B (2nd parameter) ja EndIf04 ; A is larger Then04: mov ax, [bp+6] ; B is larger EndIf04: mov bx, [bp+8] ; put address of C in BX mov [bx], ax ; use indirection to return larger value pop bx ; restore registers pop ax pop bp ; restore BP ret 6 ; return and pop off parameters MaxOf2_B ENDP ;--------------------------------------------------------------- 6 To understand exactly how this method works, again let's examine the stack. Just before the subroutine MaxOf2_B is called the stack contains the values of A and B and the address of C. | | +--------+ | a | <- SP +--------+ | b | +--------+ | addr_c | +--------+ | | After MaxOf2_B is called and after the first three instructions in the subroutine are executed we have | | +--------+ | bx | +--------+ | ax | +--------+ | old_bp | +--------+ | old_ip | +--------+ | a | +--------+ | b | +--------+ | addr_c | +--------+ | | <- SP ---------^ | | argument frame | | | | ---------- <- BP <- [bp+4] <- {bp+6] <- [bp+8] As mentioned above the logic used by MaxOf2_B is similar to that of MaxOf2_A. The first difference is that both AX and BX are saved on the stack. The code to determine the larger of "a" and "b" and move it into the AX register is the same. However, to pass back the larger value to "c, BX is loaded with the contents of [BP+8] which is the address of "c". The allows the register indirect addressing of the "mov [bx], ax" instruction to move the contents of AX to address "c". mov bx, [bp+8] mov [bx], ax ; put address of C in BX ; use indirection to return larger value 7 Allocating Local Variables on the Stack Example: Allocating "local variables" using the Stack. MaxOf2_C PROC NEAR push bp mov bp, sp sub sp, 6 push ax ; Entry Point ; save old BP register ; BP points to top of stack ; free up 3 words on stack ; save registers At this point the stack looks like | | +--------+ | bx | +--------+ | ax | +--------+ | | +--------+ | | +--------+ | | +--------+ | old_bp | +--------+ | old_ip | +--------+ | a | +--------+ | b | +--------+ | addr_c | +--------+ | | <- SP ---------| local variables | | ---------^ | | argument frame | | | | ---------- <- [bp-6] <- [bp-4] <- [bp-2] <- BP <- [bp+4] <- {bp+6] <- [bp+8] Local variables are accessed in the same way parameters are accessed with base + offset indirect addressing using the BP register and negative offsets. For example, the three local variables allocated above can be accessed using [bp-2]. [bp-4], and [bp-6]. However before returning from the subroutine, restore the registers, de-allocate the local variables, restore the BP register, and return pop bx pop ax add sp, 6 pop bp ret 6 MaxOf2_C ENDP ; restore registers ; de-allocate local variables ; restore BP ; return and clear parameters 8 Parameter Passing - Array Parameters (Pass by Reference) Example: Using Pass by Reference for Array Variables : Consider the C++ function SumArray (below) which sums the integers in an array called list returning the sum using a pass-by-reference parameter. We would write such a function as follows void SumArray(int list[], int size, int& sum); { int i; sum = 0; for (i = 0 ; i < size; i++) sum = sum + list[i]; return; } Note that pass by reference is needed if a structured type variable like an array or a string buffer is a parameter. The Intel 80x86 assembler version of this code would have three parameters, the first and third are pass by reference and the second pass by value. Since we adopt the convention of pushing parameters right to left, the Intel assembler calling sequence would be lea ax, Sum push ax push Last lea ax, List push ax call SumArray ; Get address Sum ; and push on stack ; Push Last in stack ; Get address of List ; and push on stack The code for SumArray, where we implement a FOR loop in assembler would be ;----------------------------------------------------SumArray PROC NEAR ; Entry Point push bp mov bp, sp push ax ; Save Registers push bx push cx xor ax, ax ; Clear AX for sum mov bx, [bp+4] ; BX contains address of List mov cx, 1 ; Initialize loop counter CX to 1 For1: cmp cx, [bp+6] ; Is CX > Last ? jg EndFor1 ; Yes - Exit Loop add ax, [bx] ; No - add List component to AX inc bx ; Increment BX by 2 inc bx inc cx ; Increment loop counter jmp For1 EndFor1: mov bx, [bp+8] ; BX contains address of Sum mov [bx], ax ; Return results to Sum pop cx ; Restore Registers pop bx pop ax pop bp ret 6 ; Return and Pop Parameters off Stack SumArray ENDP 9 8.4.1 An Example of a Recursive Procedure - Factorials DATA SEGMENT n dw ? f dw ? DATA ENDS CODE SEGMENT Main PROC FAR ... 3 -> ; Calling Sequence: lea ax f push ax ; Pass by Reference parameter push n ; Pass by Value parameter call Factorial ; Call Factorial Subroutine ... Main ENDP ;-----------------------------------------; Subroutine Code ;------------------------------------------Factorial PROC NEAR push bp ; Save Base Pointer mov bp, sp 2 -> push ax ; Save Registers push bx push dx cmp WORD PTR [bp+4], 0 ; is n = 0? jnz Factorial01 ; mov bx, [bp+6] ; YES - so set f = 1 mov WORD PTR [bx], 1 ; and return jmp FactorialEnd Factorial01: push [bp+6] ; Pass address of f mov ax, [bp+4] ; subtract 1 from n dec ax push ax ; Pass n call Factorial ; Recursive Call 1 -> mov bx, [bp+6] ; Get f mov ax, [bx] mul WORD PTR [bp+4] ; Multiply by n mov bx, [bp+6] ; Save product to f mov [bx],ax FactorialEnd: ; Restore Registers pop dx pop bx pop ax pop bp ret 4 ; return Factorial ENDP ;------------------------------------------CODE ENDS 0 -> END Main 10 Stack | | +------+ | | +------+ | | +------+ | dx | <- SP +------+ | bx | +------+ | ax | +------+ |old bp| <- BP +------+ |old ip| +------+ | 0 | +------+ |addr f| +------+ | dx | +------+ | bx | +------+ | ax | +------+ |old bp| +------+ |old ip| +------+ | 1 | +------+ |addr f| +------+ | dx | +------+ | bx | +------+ | ax | +------+ |old bp| +------+ |old ip| +------+ | 2 | +------+ |addr f| +------+ | | Modular Programming Method #1 Put the subroutine code in same file as main program, usually after the main program code. Since the subroutines are in the same code segment as the main program, all subroutines are type NEAR. Advantages 1. Easy to do Disadvantages 1. 2. Subroutines are not reusable Long source code so assembly takes longer Method #2 A. Place all subroutines in a separate file and use an "include" (i.e. include filename) directive to assemble the file along with the main program code. Specifically Create a separate file (e.g. Subr.asm) containing only the subroutine code. Each subroutine would be enclosed by subroutine_name PROC NEAR and subroutine_name ENDP directives. Do not define Data, Stack or Code segments. Subr PROC NEAR ... Subr ENDP Note : Since the subroutine file contains only subroutine code, it can not be assembled by itself. B. Place the assembler directive Include Subr.asm in the main program code at the point where you want to insert and assemble subroutine code (usually after the main procedure but before the CODE ENDS directive (i.e within the code segment). Advantages 1. 2. easy to do subroutine code is reusable Disadvantages 1. 2. large .OBJ files assembly time is long since "include" file is repeatedly assembled The two approaches above while straight-forward don't really make use of the capabilities of the assembler. The next two do as they allow separate assembling of the subroutines and insertion (linking) of the assembled subroutine code into the main code by the linker. Method #3 Create a file of pre-assembled subroutines (an .obj file) and use the linker to link the subroutines with the main code at link time First create a separate file containing the code for the subroutines. The file will consist of a single code segment (no data segment; no stack segment!). An example of such a file is given below. Note the following: A. There is an ASSUME directive for the CS register only. There is no stack segment and we will deal with (optional) data segments later. B. The PUBLIC directive is used to make the subroutine names known to the linker. Any subroutine name that must be "visible" outside the file must be appear with this directive. C. The last line is the END directive with no starting address. 11 Example CODE SEGMENT ASSUME CS:CODE PUBLIC Subr1, Subr2 ; makes Subr1 & Subr2 visible Subr1 PROC NEAR .... ret Subr1 ENDP Subr2 PROC NEAR ... ret Subr2 ENDP CODE ENDS END ; no starting address A file in this form can be separately assembled. However, it can't be executed (why?). Note that all procedures are NEAR. Next in the main program file, use the EXTRN directive to inform the Assembler which subroutines will be provided at link time. In the example below, the EXTRN directive appears after the ASSUME directive but before the Main procedure code. CODE SEGMENT ASSUME CS:CODE; DS:DATA; SS:MYSTACK EXTRN Subr1:NEAR, Subr2:NEAR Main PROC FAR ... call Subr1 ; address provided at link time call Subr2 ; address provided at link time Main ENDP CODE ENDS END Main If your forget the EXTRN directive, you will get an "Undefined Symbol" error. Note that the EXTRN explicitly states that both subroutines are type NEAR. Assemble this file separately. Finally at link time use the tlink command .obj file of subroutines 9 tlink a:Main+a:SubrFile, a:Main to link subroutines (object file) into the main program code. Note the plus sign (+) is used to include the subroutine object files with the main object file; there are no spaces between the plus sign and the file name. . Advantages 1. 2. faster assembly time as subroutines assembled only once. object code is reusable 12 Procedures with Separate Data Segments ;------------------------------------------------------; ; Routine Name : ReadDecimal ; Description : Reads an unsigned decimal value; Uses INT 21h ; Function 0Ah String Read ; ; At Entry : ; At Exit : AX has integer ; ;------------------------------------------------------DATA SEGMENT BufSize = 5 Buffer db BufSize+1, 0, Bufsize+1 DUP (?) DATA ENDS ;------------------------------------------------------CODE SEGMENT ASSUME CS:CODE, DS:DATA PUBLIC ReadDecimal ReadDecimal PROC NEAR push bp mov bp, sp push bx ; save registers push cx push dx push si push ds ; save old DS register mov ax, data ; Load DS with current data segment address mov ds, ax ; Read String mov ah, 0ah lea dx, Buffer int 21h mov bx, 10d xor ax, ax lea si, Buffer+2 ; ; Put multiplier in BX ; Clear AX for Number ; SI points to first byte in string Get Digit Read1: cmp BYTE PTR [si], '0' jb Done cmp BYTE PTR [si], '9' ja Done ; ; Is digit < '0' ; Is digit > '9' Convert Digit to Integer and Store in CX mov cx, [si] and cx, 0Fh 13 ; Number := Number * 10 + Digit mul add inc jmp bx ax, cx si Read1 Done: pop ds pop si pop dx pop cx pop bx pop bp ret ReadDecimal ENDP CODE ENDS END ; ; ; ; multiply by 10 add digit advance to next character and go again ; Restore DS to old data segment ; Restore registers 14