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.
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:
call
instruction,
Proc_1
,
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.
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
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
.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.
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).
Operator | Description |
---|---|
& | Text substitution operator |
< > | Literal text operator |
! | Literal character operator |
% | Expression operator |
;; | Macro comment |
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.