[Chapter Eight][Previous] [Next] [Art of Assembly][Randall Hyde]

Art of Assembly: Chapter Eight


8.14 - Macros
8.14.1 - Procedural Macros
8.14.2 - Macros vs. 80x86 Procedures
8.14.3 - The LOCAL Directive
8.14.4 - The EXITM Directive
8.14.5 - Macro Parameter Expansion and Macro Operators
8.14.6 - A Sample Macro to Implement For Loops

8.14 Macros


A macro is like a procedure that inserts a block of statements at various points in your program during assembly. There are three general types of macros that MASM supports: procedural macros, functional macros, and looping macros. Along with conditional assembly, these tools provide the traditional if, loop, procedure, and function constructs found in many high level languages. Unlike the assembly instructions you write, the conditional assembly and macro language constructs execute during assembly. The conditional assembly and macros statements do not exist when your assembly language program is running. The purpose of these statements is to control which statements MASM assembles into your final ".exe" file. While the conditional assembly directives select or omit certain statements for assembly, the macro directives let you emit repetitive sequences of instructions to an assembly language file like high level language procedures and loops let you repetitively execute sequences of high level language statements.


8.14.1 Procedural Macros


The following sequence defines a macro:




name             macro  {parameter1 {parameter2 {,...}}}

              <statements>
              
                endm

Name must be a valid and unique symbol in the source file. You will use this identifier to invoke the macro. The (optional) parameter names are placeholders for values you specify when you invoke the macro; the braces above denote the optional items, they should not actually appear in your source code. These parameter names are local to the macro and may appear elsewhere in the program.

Example of a macro definition:





COPY            macro   Dest, Source
                mov     ax, Source
                mov     Dest, ax
                endm

This macro will copy the word at the source address to the word at the destination address. The symbols Dest and Source are local to the macro and may appear elsewhere in the program.

Note that MASM does not immediately assemble the instructions between the macro and endm directives when MASM encounters the macro. Instead, the assembler stores the text corresponding to the macro into a special table (called the symbol table). MASM inserts these instructions into your program when you invoke the macro.

To invoke (use) a macro, simply specify the macro name as a MASM mnemonic. When you do this, MASM will insert the statements between the macro and endm directives into your code at the point of the macro invocation. If your macro has parameters, MASM will substitute the actual parameters appearing as operands for the formal parameters appearing in the macro definition. MASM does a straight textual substitution, just as though you had created text equates for the parameters.

Consider the following code that uses the COPY macro defined above:





                call    SetUpX
                copy    Y, X
                add     Y, 5 

This program segment will issue a call to SetUpX (which, presumably, does something to the variable X) then invokes the COPY macro, that copies the value in the variable X into the variable Y. Finally, it adds five to the value contained in variable Y.

Note that this instruction sequence is absolutely identical to:





                call    SetUpX
                mov     ax, X
                mov     Y, ax
                add     Y, 5 

In some instances using macros can save a considerable amount of typing in your programs. For example, suppose you want to access elements of various two dimensional arrays. As you may recall, the formula to compute the row-major address for an array element is

element address = base address + (First Index * Row Size + Second Index) * element size

Suppose you want write some assembly code that achieves the same result as the following C code:





int a[16][7], b[16][7], x[7][16];
int i,j;

        for (i=0; i<16; i = i + 1)
                for (j=0; j < 7; j = j + 1)
                        x[j][i] = a[i][j]*b[15-i][j];

The 80x86 code for this sequence is rather complex because of the number of array accesses. The complete code is





                .386                    ;Uses some 286 & 386 instrs.
                option  segment:use16   ;Required for real mode programs
                 .
                 .
                 .
a               sword   16 dup (7 dup (?))
b               sword   16 dup (7 dup (?))
x               sword   7 dup (16 dup (?))
                 .
                 .
                 .
i               textequ <cx>            ;Hold I in CX register.
j               textequ <dx>            ;Hold J in DX register.

                mov     I, 0            ;Initialize I loop index with zero.
ForILp:         cmp     I, 16           ;Is I less than 16?
                jnl     ForIDone        ;If so, fall into body of I loop.

                mov     J, 0            ;Initialize J loop index with zero.
ForJLp:         cmp     J, 7            ;Is J less than 7?
                jnl     ForJDone        ;If so, fall into body of J loop.

                imul    bx, I, 7        ;Compute index for a[i][j].
                add     bx, J
                add     bx, bx          ;Element size is two bytes.
                mov     ax, A[bx]       ;Get a[i][j]

                mov     bx, 15          ;Compute index for b[15-I][j].
                sub     bx, I
                imul    bx, 7
                add     bx, J
                add     bx, bx          ;Element size is two bytes.
                imul    ax, b[bx]       ;Compute a[i][j] * b[16-i][j]

                imul    bx, J, 16       ;Compute index for X[J][I]
                add     bx, I
                add     bx, bx
                mov     X[bx], ax       ;Store away result.

                inc     J               ;Next loop iteration.
                jmp     ForJLp

ForJDone:       inc     I               ;Next I loop iteration.
                jmp     ForILp

ForIDone:                               ;Done with nested loop.

This is a lot of code for only five C/C++ statements! If you take a close look at this code, you'll notice that a large number of the statements simply compute the index into the three arrays. Furthermore, the code sequences that compute these array indices are very similar. If they were exactly the same, it would be obvious we could write a macro to replace the three array index computations. Since these index computations are not identical, one might wonder if it is possible to create a macro that will simplify this code. The answer is yes; by using macro parameters it is very easy to write such a macro. Consider the following code:





i               textequ <cx>            ;Hold I in CX register.
j               textequ <dx>            ;Hold J in DX register.

NDX2            macro   Index1, Index2, RowSize
                imul    bx, Index1, RowSize
                add     bx, Index2
                add     bx, bx
                endm

                mov     I, 0            ;Initialize I loop index with zero.
ForILp:         cmp     I, 16           ;Is I less than 16?
                jnl     ForIDone        ;If so, fall into body of I loop.

                mov     J, 0            ;Initialize J loop index with zero.
ForJLp:         cmp     J, 7            ;Is J less than 7?
                jnl     ForJDone        ;If so, fall into body of J loop.

                NDX2    I, J, 7
                mov     ax, A[bx]       ;Get a[i][j]

                mov     bx, 15          ;Compute index for b[15-I][j].
                sub     bx, I
                NDX2    bx, J, 7
                imul    ax, b[bx]       ;Compute a[i][j] * b[15-i][j]

                NDX2    J, I, 16
                mov     X[bx], ax       ;Store away result.

                inc     J               ;Next loop iteration.
                jmp     ForJLp

ForJDone:       inc     I               ;Next I loop iteration.
                jmp     ForILp

ForIDone:                               ;Done with nested loop.

One problem with the NDX2 macro is that you need to know the row size of an array (since it is a macro parameter). In a short example like this one, that isn't much of a problem. However, if you write a large program you can easily forget the sizes and have to look them up or, worse yet, "remember" them incorrectly and introduce a bug into your program. One reasonable question to ask is if MASM could figure out the row size of the array automatically. The answer is yes.

MASM's length operator is a holdover from the pre-6.0 days. It was supposed to return the number of elements in an array. However, all it really returns is the first value appearing in the array's operand field. For example, (length a) would return 16 given the definition for a above. MASM corrected this problem by introducing the lengthof operator that properly returns the total number of elements in an array. (Lengthof a), for example, properly returns 112 (16 * 7). Although the (length a) operator returns the wrong value for our purposes (it returns the column size rather than the row size), we can use its return value to compute the row size using the expression (lengthof a)/(length a). With this knowledge, consider the following two macros:





; LDAX- This macro loads ax with the word at address Array[Index1][Index2]
;       Assumptions:    You've declared the array using a statement like
;                       Array word Colsize dup (RowSize dup (?))
;                       and the array is stored in row major order.
;
;       If you specify the (optional) fourth parameter, it is an 80x86
;       machine instruction to substitute for the MOV instruction that
;       loads AX from Array[bx].

LDAX            macro   Array, Index1, Index2, Instr
                imul    bx, Index1, (lengthof Array) / (length Array)
                add     bx, Index2
                add     bx, bx

; See if the caller has supplied the fourth operand.

                ifb     <Instr>
                mov     ax, Array[bx]           ;If not, emit a MOV instr.
                else
                instr   ax, Array[bx]           ;If so, emit user instr.
                endif
                endm

; STAX- This macro stores ax into the word at address Array[Index1][Index2]
;       Assumptions: Same as above

STAX            macro   Array, Index1, Index2
                imul    bx, Index1, (lengthof Array) / (length Array)
                add     bx, Index2
                add     bx, bx
                mov     Array[bx], ax
                endm

With the macros above, the original program becomes:






i               textequ <cx>            ;Hold I in CX register.
j               textequ <dx>            ;Hold J in DX register.

                mov     I, 0            ;Initialize I loop index with zero.
ForILp:         cmp     I, 16           ;Is I less than 16?
                jnl     ForIDone        ;If so, fall into body of I loop.

                mov     J, 0            ;Initialize J loop index with zero.
ForJLp:         cmp     J, 7            ;Is J less than 7?
                jnl     ForJDone        ;If so, fall into body of J loop.

                ldax    A, I, J         ;Fetch A[I][J]
                mov     bx, 16          ;Compute 16-I.
                sub     bx, I
                ldax    b, bx, J, imul  ;Multiply in B[16-I][J].
                stax    x, J, I         ;Store to X[J][I]

                inc     J               ;Next loop iteration.
                jmp     ForJLp

ForJDone:       inc     I               ;Next I loop iteration.
                jmp     ForILp

ForIDone:                               ;Done with nested loop.

As you can plainly see, the code for the loops above is getting shorter and shorter by using these macros. Of course, the entire code sequence is actually longer because the macros represent more lines of code that they save in the original program. However, that is an artifact of this particular program. In general, you'd probably have more than three array accesses; furthermore, you can always put the LDAX and STAX macros in a library file and automatically include them anytime you're dealing with two dimensional arrays. Although, technically, your program might actually contain more assembly language statements if you include these macros in your code, you only had to write those macros once. After that, it takes very little effort to include the macros in any new program.

We can shorten this code sequence even more using some additional macros. However, there are a few additional topics to cover before we can do that, so keep reading.


8.14.2 Macros vs. 80x86 Procedures


Beginning assembly language programmers often confuse macros and procedures. A procedure is a single section of code that you call from various points in the program. A macro is a sequence of instructions that MASM replicates in your program each time you use the macro. Consider the following two code fragments:




Proc_1          proc    near
                mov     ax, 0
                mov     bx, ax
                mov     cx, 5
                ret
Proc_1          endp

Macro_1         macro
                mov     ax, 0
                mov     bx, ax
                mov     cx, 5
                endm

                call    Proc_1
                 .
                 .
                call    Proc_1
                 .
                 .
                Macro_1
                 .
                 .
                Macro_1

Although the macro and procedure produce the same result, they do it in different ways. The procedure definition generates code when the assembler encounters the proc directive. A call to this procedure requires only three bytes. At execution time, the 80x86:



The macro, on the other hand, does not emit any code when processing the statements between the macro and endm directives. However, upon encountering Macro_1 in the mnemonic field, MASM will assemble every statement between the macro and endm directives and emit that code to the output file. At run time, the CPU executes these instructions without the call/ret overhead.

The execution of a macro expansion is usually faster than the execution of the same code implemented with a procedure. However, this is another example of the classic speed/space trade-off. Macros execute faster by eliminating the call/return sequence. However, the assembler copies the macro code into your program at each macro invocation. If you have a lot of macro invocations within your program, it will be much larger than the same program that uses procedures.

Macro invocations and procedure invocations are considerably different. To invoke a macro, you simply specify the macro name as though it were an instruction or directive. To invoke a procedure you need to use the call instruction. In many contexts it is unfortunate that you use two separate invocation mechanisms for such similar operations. The real problem occurs if you want to switch a macro to a procedure or vice versa. It might be that you've been using macro expansion for a particular operation, but now you've expanded the macro so many times it makes more sense to use a procedure. Maybe just the opposite is true, you've been using a procedure but you want to expand the code in-line to improve it's performance. The problem with either conversion is that you will have to find every invocation of the macro or procedure call and modify it. Modifying the procedure or macro is easy, but locating and changing all the invocations can be quite a bit of work. Fortunately, there is a very simple technique you can use so procedure calls share the same syntax as macro invocation. The trick is to create a macro or a text equate for each procedure you write that expands into a call to that procedure. For example, suppose you write a procedure ClearArray that zeros out arrays. When writing the code, you could do the following:





ClearArray      textequ <call $$ClearArray>
$$ClearArray    proc    near
                 .
                 .
                 .
$$ClearArray    endm

To call the ClearArray procedure, you'd simply use a statement like the following:





                 .
                 .
                 .
        <Set up parameters for ClearArray>
                ClearArray
                 .
                 .
                 .

If you ever change the $$ClearArray procedure to a macro, all you need to do is name it ClearArray and dispose of the textequ for the procedure. Conversely, if you already have a macro and you want to convert it to a procedure, Simply name the procedure $$procname and create a text equate that emits a call to this procedure. This allows you to use the same invocation syntax for procedures or macros.

This text won't normally use the technique described above, except for the UCR Standard Library routines. This is not because this isn't a good way to invoke procedures. Some people have trouble differentiating macros and procedures, so this text will use explicit calls to help avoid that confusion. Standard Library calls are an exception because using macro invocations is the standard way to call these routines.


8.14.3 The LOCAL Directive


Consider the following macro definition:




LJE             macro   Dest
                jne     SkipIt
                jmp     Dest
SkipIt:
                endm

This macro does a "long jump if equal". However, there is one problem with it. Since MASM copies the macro text verbatim (allowing, of course, for parameter substitution), the symbol SkipIt will be redefined each time the LJE macro appears. When this happens, the assembler will generate a multiple definition error. To overcome this problem, the local directive can be used to define a local symbol within the macro. Consider the following macro definition:





LJE             macro   Dest
                local   SkipIt
                jne     SkipIt
                jmp     Dest
SkipIt:
                endm

In this macro definition, SkipIt is a local symbol. Therefore, the assembler will generate a new copy of SkipIt each time you invoke the macro. This will prevent MASM from generating an error.

The local directive, if it appears within your macro definition, must appear immediately after the macro directive. If you need multiple local symbols, you can specify several of them in the local directive's operand field. Simply separate each symbol with a comma:





IFEQUAL         macro   a, b
                local   ElsePortion, Done
                mov     ax, a
                cmp     ax, b
                jne     ElsePortion
                inc     bx
                jmp     Done
ElsePortion:    dec     bx
Done:
                endm


8.14.4 The EXITM Directive


The exitm directive immediately terminates the expansion of a macro, exactly as though MASM encountered endm. MASM ignores all text from the exitm directive to the endm.

You're probably wondering why anyone would ever use the exitm directive. After all, if MASM ignores all text between exitm and endm, why bother sticking an exitm directive into your macro in the first place? The answer is conditional assembly. Conditional assembly can be used to conditionally execute the exitm directive, thereby allowing further macro expansion under certain conditions, consider the following:




Bytes           macro   Count 
                byte    Count
                if      Count eq 0
                exitm
                endif
                byte    Count dup (?)
                endm

Of course, this simple example could have been coded without using the exitm directive (the conditional assembly directive is all we require), but it does demonstrate how the exitm directive can be used within a conditional assembly sequence to control its influence.


8.14.5 Macro Parameter Expansion and Macro Operators


Since MASM does a textual substitution for macro parameters when you invoke a macro, there are times when a macro invocation might not produce the results you expect. For example, consider the following (admittedly dumb) macro definition:




Index           =       8

; Problem-      This macro attempts to load AX with the element of a word
;               array specified by the macro's parameter. This parameter
;               must be an assembly-time constant.

Problem         macro   Parameter
                mov     ax, Array[Parameter*2]
                endm
                 .
                 .
                 .
                Problem 2
                 .
                 .
                 .
                Problem Index+2

When MASM expands the first invocation of Problem above, it produces the instruction:





                mov     ax, Array[2*2]

Okay, so far so good. This code loads element two of Array into ax. However, consider the expansion of the second invocation to Problem, above:





                mov     ax, Array[Index+2*2]

Because MASM's address expressions support operator precedence (see "Operator Precedence" on page 396), this macro expansion will not produce the correct result. It will access the sixth element of Array (at index 12) rather than the tenth element at index 20.

The problem above occurs because MASM simply replaces a formal parameter by the actual parameter's text, not the actual parameter's value. This pass by name parameter passing mechanism should be familiar to long-time C and C++ programmers who use the #define statement. If you think that macro (pass by name) parameters work just like Pascal and C's pass by value parameters, you are setting yourself up for eventual disaster.

One possible solution, that works well for macros like the above, is to put parentheses around macro parameters that occur within expressions inside the macro. Consider the following code:





Problem         macro   Parameter
                mov     ax, Array[(Parameter)*2]
                endm
                 .
                 .
                 .
                Problem Index+2

This macro invocation expands to





                mov     ax, Array[(Index+2)*2]

This produces the expected result.

Textual parameter substitution is but one problem you'll run into when using macros. Another problem occurs because MASM has two types of assembly time values: numeric and text. Unfortunately, MASM expects numeric values in some contexts and text values in others. They are not fully interchangeable. Fortunately, MASM provides a set of operators that let you convert between one form and the other (if it is possible to do so). To understand the subtle differences between these two types of values, look at the following statements:





Numeric         =       10+2
Textual         textequ <10+2>

MASM evaluates the numeric expression "10+2" and associates the value twelve with the symbol Numeric. For the symbol Textual, MASM simply stores away the string "10+2" and substitutes it for Textual anywhere you use it in an expression.

In many contexts, you could use either symbol. For example, the following two statements both load ax with twelve:





                mov     ax, Numeric     ;Same as mov ax, 12
                mov     ax, Textual     ;Same as mov ax, 10+2

However, consider the following two statements:





                mov     ax, Numeric*2   ;Same as mov ax, 12*2
                mov     ax, Textual*2   ;Same as mov ax, 10+2*2

As you can see, the textual substitution that occurs with text equates can lead to the same problems you encountered with textual substitution of macro parameters.

MASM will automatically convert a text object to a numeric value, if the conversion is necessary. Other than the textual substitution problem described above, you can use a text value (whose string represents a numeric quantity) anywhere MASM requires a numeric value.

Going the other direction, numeric value to text value, is not automatic. Therefore, MASM provides an operator you can use to convert numeric data to textual data: the "%" operator. This expansion operator forces an immediate evaluation of the following expression and then it converts the result of the expression into a string of digits. Look at these invocations of the Problem macro:





                Problem 10+2    ;Parameter is "10+2"
                Problem %10+2   ;Parameter is "12"

In the second example above, the text expansion operator instructs MASM to evaluate the expression "10+2" and convert the resulting numeric value to a text value consisting of the digits that represent the value twelve. Therefore, these two macro expand into the following statements (respectively):





                mov     ax, Array[10+2*2]       ;Problem 10+2 expansion
                mov     ax, Array[12*2]         ;Problem %10+2 expansion

MASM provides a second operator, the substitution operator that lets you expand macro parameter names where MASM does not normally expect a symbol. The substitution operator is the ampersand ("&") character. If you surround a macro parameter name with ampersands inside a macro, MASM will substitute the parameter's text regardless of the location of the symbol. This lets you expand macro parameters whose names appear inside other identifiers or inside literal strings. The following macro demonstrates the use of this operator:





DebugMsg        macro   Point, String
Msg&String&     byte    "At point &Point&: &String&"
                endm
                 .
                 .
                 .
                DebugMsg 5, <Assertion fails>

The macro invocation immediately above produces the statement:





Msg5            byte    "At point 5: Assertion failed"

Note how the substitution operator allowed this macro to concatenate "Msg" and "5" to produce the label on the byte directive. Also note that the expansion operator lets you expand macro identifiers even if they appear in a literal string constant. Without the ampersands in the string, MASM would have emitted the statement:





Msg5            byte    "At point point: String"

Another important operator active within macros is the literal character operator, the exclamation mark ("!"). This symbol instructs MASM to pass the following character through without any modification. You would normally use this symbol if you need to include one of the following symbols as a character within a macro:

! & > %

For example, had you really wanted the string in the DebugMsg macro to display the ampersands, you would use the definition:





DebugMsg        macro   Point, String
Msg&String&     byte    "At point !&Point!&: !&String!&"
                endm

"Debug 5, <Assertion fails>" would produce the following statement:
Msg5            byte    "At point &Point&: &String&"

Use the "<" and ">" symbols to delimit text data inside MASM. The following two invocations of the PutData macro show how you can use these delimiters in a macro:





PutData         macro   TheName, TheData
PD_&TheName&    byte    TheData
                endm
                 .
                 .
                 .
                PutData MyData, 5, 4, 3         ;Emits "PD_MyData byte 5"
                PutData MyData, <5, 4, 3>       ;Emits "PD_MyData byte 5, 4, 3"

You can use the text delimiters to surround objects that you wish to treat as a single parameter rather than as a list of multiple parameters. In the PutData example above, the first invocation passes four parameters to PutData (PutData ignores the last two). In the second invocation, there are two parameters, the second consisting of the text 5, 4, 3.

The last macro operator of interest is the ";;" operator. This operator begins a macro comment. MASM normally copies all text from the macro into the body of the program during assembly, including all comments. However, if you begin a comment with ";;" rather than a single semicolon, MASM will not expand the comment as part of the code during macro expansion. This increases the speed of assembly by a tiny amount and, more importantly, it does not clutter a program listing with copies of the same comment (see "Controlling the Listing" on page 424 to learn about program listings).
Macro Operators
Operator Description
& Text substitution operator
< > Literal text operator
! Literal character operator
% Expression operator
;; Macro comment



8.14.6 A Sample Macro to Implement For Loops


Remember the for loops and matrix operations used in a previous example? At the conclusion of that section there was a brief comment that we could "improve" that code even more using macros, but the example had to wait. With the description of macro operators out of the way, we can now finish that discussion. The macros that implement the for loop are




; First, three macros that let us construct symbols by concatenating others.
; This is necessary because this code needs to expand several components in
; text equates multiple times to arrive at the proper symbol. 
;
; MakeLbl-      Emits a label create by concatenating the two parameters
;               passed to this macro.

MakeLbl         macro   FirstHalf, SecondHalf
&FirstHalf&&SecondHalf&:
                endm

jgDone          macro   FirstHalf, SecondHalf
                jg      &FirstHalf&&SecondHalf&
                endm

jmpLoop         macro   FirstHalf, SecondHalf
                jmp     &FirstHalf&&SecondHalf&
                endm

; ForLp-                This macro appears at the beginning of the for loop. To invoke
;               this macro, use a statement of the form:
;
;               ForLp   LoopCtrlVar, StartVal, StopVal
;
; Note: "FOR" is a MASM reserved word, which is why this macro doesn't
; use that name.

ForLp           macro   LCV, Start, Stop

; We need to generate a unique, global symbol for each for loop we create.
; This symbol needs to be global because we will need to reference it at the
; bottom of the loop. To generate a unique symbol, this macro concatenates
; "FOR" with the name of the loop control variable and a unique numeric value
; that this macro increments each time the user constructs a for loop with the
; same loop control variable.

                ifndef  $$For&LCV&      ;;Symbol = $$FOR concatenated with LCV
$$For&LCV&      =       0               ;;If this is the first loop w/LCV, use
                else                    ;; zero, otherwise increment the value.
$$For&LCV&      =       $$For&LCV& + 1
                endif

; Emit the instructions to initialize the loop control variable:

                mov     ax, Start
                mov     LCV, ax

; Output the label at the top of the for loop. This label takes the form
;               $$FOR LCV x
; where LCV is the name of the loop control variable and X is a unique number
; that this macro increments for each for loop that uses the same loop control
; variable.

                MakeLbl $$For&LCV&, %$$For&LCV&

; Okay, output the code to see if this for loop is complete.
; The jgDone macro generates a jump (if greater) to the label the
; Next macro emits below the bottom of the for loop.

                mov     ax, LCV
                cmp     ax, Stop
                jgDone  $$Next&LCV&, %$$For&LCV&
                endm

; The Next macro terminates the for loop. This macro increments the loop
; control variable and then transfers control back to the label at the top of
; the for loop.

Next            macro   LCV
                inc     LCV
                jmpLoop $$For&LCV&, %$$For&LCV&
                MakeLbl $$Next&LCV&, %$$For&LCV&
                endm

With these macros and the LDAX/STAX macros, the code from the array manipulation example presented earlier becomes very simple. It is






                ForLp   I, 0, 15
                ForLp   J, 0, 6

                ldax    A, I, J         ;Fetch A[I][J]
                mov     bx, 15          ;Compute 16-I.
                sub     bx, I
                ldax    b, bx, J, imul  ;Multiply in B[15-I][J].
                stax    x, J, I         ;Store to X[J][I]

                Next    J
                Next    I

Although this code isn't quite as short as the original C/C++ example, it's getting pretty close!

While the main program became much simpler, there is a question of the macros themselves. The ForLp and Next macros are extremely complex! If you had to go through this effort every time you wanted to create a macro, assembly language programs would be ten times harder to write if you decided to use macros. Fortunately, you only have to write (and debug) a macro like this once. Then you can use it as many times as you like, in many different programs, without having to worry much about it's implementation.

Given the complexity of the For and Next macros, it is probably a good idea to carefully describe what each statement in these macros is doing. However, before discussing the macros themselves, we should discuss exactly how one might implement a for/next loop in assembly language. This text fully explores the for loop a little later, but we can certainly go over the basics here. Consider the following Pascal for loop:





        for variable := StartExpression to EndExpression do
                Some_Statement;

Pascal begins by computing the value of StartExpression. It then assigns this value to the loop control variable (variable). It then evaluates EndExpression and saves this value in a temporary location. Then the Pascal for statement enters the loop's body. The first thing the loop does is compare the value of variable against the value it computed for EndExpression. If the value of variable is greater than this value for EndExpression, Pascal transfers to the first statement after the for loop, otherwise it executes Some_Statement. After the Pascal for loop executes Some_Statement, it adds one to variable and jumps back to the point where it compares the value of variable against the computed value for EndExpression. Converting this code directly into assembly language yields the following code:





;Note: This code assumes StartExpression and EndExpression are simple variables.
;If this is not the case, compute the values for these expression and place
;them in these variables.

                mov     ax, StartExpression
                mov     Variable, ax
ForLoop:        mov     ax, Variable
                cmp     ax, EndExpression
                jg      ForDone

        <Code for Some_Statement>

                inc     Variable
                jmp     ForLoop
ForDone:

To implement this as a set of macros, we need to be able to write a short piece of code that will write the above assembly language statements for us. At first blush, this would seem easy, why not use the following code?





ForLp           macro   Variable, Start, Stop
                mov     ax, Start
                mov     Variable, ax
ForLoop:        mov     ax, Variable
                cmp     ax, Stop
                jg      ForDone
                endm

Next            macro   Variable
                inc     Variable
                jmp     ForLoop
ForDone:
                endm

These two macros would produce correct code - exactly once. However, a problem develops if you try to use these macros a second time. This is particularly evident when using nested loops:





                ForLp   I, 1, 10
                ForLp   J, 1, 10
                 .
                 .
                 .
                Next    J
                Next    I

The macros above emit the following 80x86 code:





                mov     ax, 1           ;The ForLp I, 1, 10
                mov     I, ax           ; macro emits these
ForLoop:        mov     ax, I           ; statements.
                cmp     ax, 10          ;       .
                jg      ForDone         ;       .

                mov     ax, 1           ;The ForLp J, 1, 10
                mov     J, ax           ; macro emits these
ForLoop:        mov     ax, J           ; statements.
                cmp     ax, 10          ;        .
                jg      ForDone         ;        .
                 .
                 .
                 .
                inc     J               ;The Next J macro emits these
                jmp     ForLp           ; statements.
ForDone:
                inc     I               ;The Next I macro emits these
                jmp     ForLp           ; statements.
ForDone:

The problem, evident in the code above, is that each time you use the ForLp macro you emit the label "ForLoop" to the code. Likewise, each time you use the Next macro, you emit the label "ForDone" to the code stream. Therefore, if you use these macros more than once (within the same procedure), you will get a duplicate symbol error. To prevent this error, the macros must generate unique labels each time you use them. Unfortunately, the local directive will not work here. The local directive defines a unique symbol within a single macro invocation. If you look carefully at the code above, you'll see that the ForLp macro emits a symbol that the code in the Next macro references. Likewise, the Next macro emits a label that the ForLp macro references. Therefore, the label names must be global since the two macros can reference each other's labels.

The solution the actual ForLp and Next macros use is to generate globally known labels of the form "$$For" + "variable name" + "some unique number." and "$$Next" + "variable name" + "some unique number". For the example given above, the real ForLp and Next macros would generate the following code:





                mov     ax, 1           ;The ForLp I, 1, 10
                mov     I, ax           ; macro emits these
$$ForI0:        mov     ax, I           ; statements.
                cmp     ax, 10          ;       .
                jg      $$NextI0        ;       .

                mov     ax, 1           ;The ForLp J, 1, 10
                mov     J, ax           ; macro emits these
$$ForJ0:        mov     ax, J           ; statements.
                cmp     ax, 10          ;        .
                jg      $$NextJ0        ;        .
                 .
                 .
                 .
                inc     J               ;The Next J macro emits these
                jmp     $$ForJ0         ; statements.
$$NextJ0:
                inc     I               ;The Next I macro emits these
                jmp     $$ForI0         ; statements.
$$NextI0:

The real question is, "How does one generate such labels?"

Constructing a symbol of the form "$$ForI" or "$$NextJ" is pretty easy. Just create a symbol by concatenating the string "$$For" or "$$Next" with the loop control variable's name. The problem occurs when you try to append a numeric value to the end of that string. The actual ForLp and Next code accomplishes this creating assembly time variable names of the form "$$Forvariable_name" and incrementing this variable for each loop with the given loop control variable name. By calling the macros MakeLbl, jgDone, and jmpLoop, ForLp and Next output the appropriate labels and ancillary instructions.

The ForLp and Next macros are very complex. Far more complex than you would typically find in a program. They do, however, demonstrate the power of MASM's macro facilities. By the way, there are much better ways to create these symbols using macro functions. We'll discuss macro functions next.

8.14 - Macros
8.14.1 - Procedural Macros
8.14.2 - Macros vs. 80x86 Procedures
8.14.3 - The LOCAL Directive
8.14.4 - The EXITM Directive
8.14.5 - Macro Parameter Expansion and Macro Operators
8.14.6 - A Sample Macro to Implement For Loops


Art of Assembly: Chapter Eight - 26 SEP 1996

[Chapter Eight][Previous] [Next] [Art of Assembly][Randall Hyde]



Number of Web Site Hits since Jan 1, 2000: