Chapter 5.1-5.6 Notes

Document last modified: 

Overview

The importance of algorithm abstraction and modularity is such that virtually all high-level languages realize these notions, generally as procedures and functions with parameters. Translation from Pascal or C++ algorithms to Assembly can be reliably done once a few basic rules for passing parameters are established. Though there are numerous methods to pass data to a procedure, the simplest and the one discussed here is the use of global variables and register parameters to interface the calling and the called functions. The more general and useful method of passing parameters using value and reference parameters (via the stack) will also be discussed later as this method is most often used by high level languages.

Macros are another method of implementing modularity and abstraction in Assembler (and C, spreadsheets, and many languages). Though functions and macros are both abstraction techniques, macros are very different from a function in use.

Functions

An Assembly and a C++ (or Pascal, etc.) function operates identically when executing on the hardware. You are already familiar with using GetDec$/PutDec$/PutStrng functions for performing input and output in a fashion similar to that used in C++ cin/cout. These functions require parameters to be passed in registers. For example, to print a string:
 
Print a String using Putstrng
           Extrn       PutStrng:Far

Data    Segment
           Prompt     db     'Enter a positive number:'
Data    Ends
               :
           Lea           Di, Prompt
           Mov          Cx, 24
           Call            PutStrng

Since we are already familiar with implementing functions in C++, it is to our advantage to build upon that knowledge when implementing assembler functions. The following C++ and Assembly program call a function f that has zero parameters but each produces the same results. It is instructive to follow the execution of the C++ first and compare that with the Assembler.
 
C++ versus Assembly Functions
int x;       // Global x
1. void main(void) {       
2.    f();
3.    x = 4; // Side effect
4. }
5. void f(void) {
6.    x = 5; // Side effect 
7. }
.386
      .model flat, stdcall

      include v:\common\user\c335\masm32\include\kernel32.inc
      include v:\common\user\c335\masm32\include\masm32.inc
      includelib v:\common\user\c335\masm32\lib\kernel32.lib
      includelib v:\common\user\c335\masm32\lib\masm32.lib
.data
      X       Dw    ?  ;; Global X

.code
1.  main Proc    near

2.       Call    f
3.       Mov     X, 4  ;; Side effect

4.       Push 	 0                          ; Stop    
5.     	 Call 	 ExitProcess

6.  main Endp
7.  f 	 proc	 near
8.	 Mov	 X, 5  ;; Side effect
9.       Ret
10. f    Endp
         End      main

Call/Ret - Function Execution

An execution trace of the C++ and Assembler programs above reveils the following execution sequence. Notice that the Call f transfers control to the function f, the Ret returns control back to the caller, the main function.
 
C++ and Assembler Execution Sequence
C++ 1, 2, 5, 6, 7, 3, 4
Assembler 1, 2, 7, 8, 9, 3, 4, 5
Calling a Function - Calling a function generally requires the following steps (specifically for Call f):
  1. Save the return location  - When executing Call f, the IP points to the next instruction after the Call f. For 16-bit execution, IP = 000E16 must be saved on the stack before the function begins execution. The effect is:
  2. Execute first instruction of function - By setting IP to the location (offset) of the first instruction of the function, that instruction will be fetched and executed next. The first instruction of function f is at offset 001216, the effect for 16-bit  is:
Execution of Call f using 16-bit
Assembly Listing Stack, Sp, and IP
     
   Address
     0005  C7 06 0000r 0004               Mov     X, 4
     000B  E8 0012                        Call    f
     
     000E  B4 0000                        Push    0
     0010  CD 21                          Call    ExitProcess
                                    main  Endp
     
                                    f     Proc    Near
     0012  C7 06 0000r 0005               Mov     X, 5
     0018  C3                             Ret
     0019                           f     Endp
After Call f
       ____
      |____|
Sp -> |000E|
      |    |
Ip =  0012

After Ret
       ____
Sp -> |____|
      |000E|
      |    |
Ip =  000E
 

Function Return - Returning from a function is the reverse of the Call requiring the following step:

  1. Restore the return location  - When executing the 16-bit Ret, the IP points to the next instruction after the Ret, IP = 0019. The return location must be restored from the stack to IP. Since IP points to the next instruction to fetch, the effect of restoring IP=000E16 is to resume execution at offset 000E16:

Parameters

Parameters allow the same code of a function to produce a range of results dependent upon the parameter value. For example, the sqrt(4) is 2 and the sqrt(25) is 5. This supports reuse of generic code rather than implementing unique code for each possible case (e.g. sqrt4() and sqrt25()). The following examines two simple methods of implementing parameters, global variables and registers. Both methods have problems that will be identified in the discussion.


Global Variables - It should be remembered that all variables declared in the data segment and all CPU registers are accessible globally, from within any function in the algorithm. While this may seem wonderfully useful, reflect upon writing a large C++ program with no local variables or function parameters, a globally defined variable can be accessed in any function allowing accidental change to occur undetected. When globals are used to exchange data, the same variable is accessed in one or more functions. The effects of function execution are not localized to that function, since global variables can be changed in any function as a hidden side-effect of the function's execution. When global variables are used to exchange data between functions, the likelihood of creating unwanted side-effects increases directly with the number of assignment statements and the number of global variables. The potential for side-effects increases exponentially as the number of global variables increase, creating a daunting challenge for a programmer to produce reliable programs.

While global variables and the globally accessible CPU registers share many of the same problems when used to exchange data between assembly procedures, there are two strong reasons for using registers over global variables: 1) As part of the CPU, the same registers are known by the same name everywhere and are shared by all functions/programs/etc. making it possible to pass data between two separately written functions/programs. Global variable names are normally limited to the source program and data segment where defined. 2) The Push/Pop mechanism can be used to preserve register values from unwanted side-effects.

For a simple example of global variables as parameters see the program above, variable x in the C++ and X in the Assembler are globals, accessible from all functions. To illustrate the difference between the use of global variables in the data segment and register parameters, programs in C++ and Assembler using global variables and another using register parameters are listed below. Each of the included examples compute N!. All of the factorial functions return a value in the Ax register, Ax is used by convention for returning a word result from a function.
 
Factorial Function Example in C++ and Assembler using Globals
// Determine sum of 4! and 5! 
// using global variables
1. #include <iostream.h>

2. int Sum, N;  // Global variable N   
                // Factorial parameter 

3. int Factorial(void) {

4.   int F = 1;
5.   while (N > 1) {
6.    F = F * N;        //
7.    N = N - 1;        // Side-effect N
8.   };                 // 

9.   return F;
10. } // Factorial 
11. void main(void) {
12.  N = 4;
13.  Sum = Factorial();  //
14.  N = 5;              // Side-effect N
15.  Sum =+ Factorial(); //
16.  cout << N;            
17. }                          

.386
      .model flat, stdcall

      include v:\common\user\c335\masm32\include\kernel32.inc
      include v:\common\user\c335\masm32\include\masm32.inc
      includelib v:\common\user\c335\masm32\lib\kernel32.lib
      includelib v:\common\user\c335\masm32\lib\masm32.lib

      PutDec 	proto
.data
        N    dd   ?  ;; Global N parameter to 
        Sum  dd   ?  ;; Factorial function
.code

;; int Factorial()
;;                    - Assumptions
;;  Parameters        - N positive integer (global) 
;;  Returns           - eAx factorial value
;;  Registers Altered - eAx, flags

1. Factorial Proc      Near      ;; int Factorial()
2.           Push      eDx       ;; Save eDx

3.           Mov       eAx, 1    ;; F = 1;
4.  While1:  Cmp       N, 1      ;; while (N > 1) {
5.           Jg        Do1       ;;   F = F * N;
6.           Jmp       EndWhile1 ;;   N = N - 1; 
7.     Do1:  
8.           Mul       N         ;;  ______________ 
9.           Dec       N         ;; |Side-effect N |
10.          Jmp       While1    ;; |______________|
11. EndWhile1: 
12.          Pop       eDx       ;; Restore Dx
13.          Ret                 ;; eAx has result
14. Factorial Endp

15. Main   Proc     Near
16.        Mov      N, 4
17.        Call     Factorial
18.        Mov      Sum, eAx     ;; Sum = Factorial

19.        Mov      N, 5
20.        Call     Factorial
21.        Add      Sum, eAx     ;; Sum =+ Factorial;
                                 ;;  ________________
22.        Mov      eAx, N       ;; | N side-effected|
23.        Call     PutDec       ;; |________________|

24.        Push     0            ;; Stop    
25.        Call     ExitProcess

26. Main   Endp
27.        End      Main


Register Parameters -When registers are used to pass parameters to a procedure or function, it again creates the opportunity for unwanted side effects. However, using Push and Pop to save and restore registers used within the procedure one can eliminate at least side-effect to registers. The C++ algorithms reflects the use of parameters although, as we will see in later chapters, C++ passes parameters on the stack rather than in registers (with only a few registers the number of possible parameters would be severely limited). The Assembler example instead uses registers to pass data to the function. The key weaknesses of register parameters are:

In the following the C++ and Assembly programs both compute 4! + 5!. In the C++ example, 4 and 5 are passed as parameters, in the Assembler example, eBx register is used to pass the parameters 4 and 5 to the Factorial function, the result is returned in eAx register. In the Assembler program at line:
18. The value 4 is moved to parameter eBx.
20. The returned result eAx = 4! is used.
21. The value 5 is moved to the parameter eBx.
23. The returned result eAx = 5! is used.
 3.  The value of parameter in eBx is saved on stack.
10. The parameter eBx is side-effected (changed) in function.
13. The original value of parameter eBx is restored.
Factorial Function Example in C++ and in Assembler using Registers
// Determine sum of 4! and 5!  
// using parameters

1.  #include <iostream.h>

2.  int Sum, N;                 
                         

3.  void main(void) {
4.     N = 4;
5.     Sum = Factorial(N);
6.     N = 5;
7.     Sum =+ Factorial(N);  
8.     cout << N;            
9. }         

10. int Factorial( int N ) {

11.  int F=1;
12.  while (N > 1) {
13.   F = F * N;   // ______________ 
14.   N = N - 1;   //|No Side-effect|
15.  };            //|______________|
16.  return F;
17.} //Factorial 













                  
.386
      .model flat, stdcall

      include v:\common\user\c335\masm32\include\kernel32.inc
      include v:\common\user\c335\masm32\include\masm32.inc
      includelib v:\common\user\c335\masm32\lib\kernel32.lib
      includelib v:\common\user\c335\masm32\lib\masm32.lib

      PutDec 	proto
.data
        Sum  dd   ?  

.code

;; int Factorial()
;;                    - Assumptions
;;  Parameters        - ebx positive integer (register) 
;;  Returns           - eAx factorial value
;;  Registers Altered - eAx, flags

1. Factorial Proc      Near      ;; int Factorial()
2.           Push      eDx       ;; Save eDx, eBx
3.	     Push      eBx

4.           Mov       eAx, 1    ;; F = 1;
5.  While1:  Cmp       eBx, 1    ;; while (eBx > 1) {
6.           Jg        Do1       ;;   F = F * eBx;
7.           Jmp       EndWhile1 ;;   eBx = eBx - 1; 
8.     Do1:  
9.           Mul       eBx       ;;  ________________ 
10.          Dec       eBx       ;; |Side-effect eBx |
11.          Jmp       While1    ;; |________________|
12. EndWhile1: 
13.          Pop       eBx
14.          Pop       eDx       ;; Restore eDx, eBx
15.          Ret                 ;; eAx has result
16. Factorial Endp

17. Main   Proc     Near
18.        Mov      eBx, 4
19.        Call     Factorial
20.        Mov      Sum, eAx      ;; Sum = Factorial

21.        Mov      eBx, 5
22.        Call     Factorial
23.        Add      Sum, eAx      ;; Sum =+ Factorial;
                             
24.        Mov      eAx, Sum     
25.        Call     PutDec        ;; cout << Sum

26.        Push     0             ;; Stop    
27.        Call     ExitProcess

28. Main   Endp
29.        End      Main

Saving/Restoring Registers

We should not expect register values to change after calling PutStrng, PutDec, or most other of the text supplied functions. One convention to prevent register side-effects is to push any register values that will be altered at the beginning of the function and pop the values back into the same registers at the end of the function. For example, the following function returns 5*10 in the eAx register. Execution alters e Ax, eBx, eDx and flag registers but restores all but eAx, which holds the result of the function execution.
 
Mult   Proc   Near
       Push   eBx        ; Save registers
       Push   eDx
       PushFD

       Mov    eAx, 5
       Mov    eBx, 10    ; eBx altered
       Mul    eBx        ; eDx, eAx and flags altered

       PopFD		 ; Restore registers in reverse order
       Pop    eDx
       Pop    eBx
       Ret
Mult   Endp

Conventions for Returning Function Results

Functions generally return a single result for each execution. The standard method followed in this course and for most high-level languages such as C++ is the following: It is of course possible to use any other registers to return results but that is considered nonstandard use. The factorial functions examined above use Ax to return a word result.

Common errors - One common error is to Push/Pop eAx on entry and exit to the function, the undesirable effect is that eAx may change during the function execution but is restored to its original value.

Near/Far Functions

Functions can be defined as either Near or Far. The following distinguishes between the two:

Local Variables/External Functions

The main points of interest here is the use of local variables via a separate DATA segment for the function and external function. The main program, main.asm calls the external function, factorial.asm, each are in separate files.

Local variables - Local variables are accessible only in the file where defined. In the example below, both file have separate .data definitions, each independent and each data accessible only within the file defined.

External functions - External functions are defined in a separate file, assembled separately, and linked to a calling program. In the following, main.asm calls the factorial function in the factorial.asm file. 

Use of Local Data Segment and External Function
; main.asm

.386
  .model flat, stdcall

  include v:\common\user\c335\masm32\include\kernel32.inc
  include v:\common\user\c335\masm32\include\masm32.inc
  includelib v:\common\user\c335\masm32\lib\kernel32.lib
  includelib v:\common\user\c335\masm32\lib\masm32.lib

  PutDec    proto
  Factorial proto
.data
  Sum       dd   ?  

.code

 Main   Proc     Near
        Mov      eBx, 4
        Call     Factorial
        Mov      Sum, eAx      ;; Sum = Factorial

        Mov      eBx, 5
        Call     Factorial
        Add      Sum, eAx      ;; Sum =+ Factorial;
                             
        Mov      eAx, Sum     
        Call     PutDec        ;; cout << Sum

        Push     0             ;; Stop    
        Call     ExitProcess

 Main   Endp
        End      Main
; factorial.asm

.386
  .model flat, stdcall

  include v:\common\user\c335\masm32\include\kernel32.inc
  include v:\common\user\c335\masm32\include\masm32.inc
  includelib v:\common\user\c335\masm32\lib\kernel32.lib
  includelib v:\common\user\c335\masm32\lib\masm32.lib

.data
      OverFlow	db	?
.code

 Factorial Proc      Near      ;; int Factorial()
           Push      eDx       ;; Save eDx, eBx

	   Mov	     OverFlow, 0
           Mov       eAx, 1    ;; F = 1;
  While1:  Cmp       eBx, 1    ;; while (eBx > 1 && OverFlow == 0) {
           Jg        And1       
	   Jmp	     EndWhile1 
     And1: Cmp	     OverFlow, 0
           Je        Do1       ;;   F = F * eBx;
           Jmp       EndWhile1 ;;   eBx = eBx - 1; 
   Do1:  
           Mul       eBx
      IfA: Jo        ThenA
	   Jmp	     EndifA
      ThenA:
	   Mov	     OverFlow, 1
      EndifA:
			       ;;  ________________ 
           Dec       eBx       ;; |Side-effect eBx |
           Jmp       While1    ;; |________________|
  EndWhile1: 
           Pop       eDx       ;; Restore Dx
           Ret                 ;; eAx has result
 Factorial Endp
           End
Document last modified: