Chapter 9

Using Macros


A “macro” is a symbolic name you give to a series of characters (a text macro) or to one or more statements (a macro procedure or function). As the assembler evaluates each line of your program, it scans the source code for names of previously defined macros. When it finds one, it substitutes the macro text for the macro name. In this way, you can avoid writing the same code several places in your program.

This chapter describes the following types of macros:

  Text macros, which expand to text within a source statement.

  Macro procedures, which expand to one or more complete statements and can optionally take parameters.

  Repeat blocks, which generate a group of statements a specified number of times or until a specified condition becomes true.

  Macro functions, which look like macro procedures and can be used like text macros but which also return a value.

  Predefined macro functions and string directives, which perform string
operations.

 

This chapter explains how to use macros for simple code substitutions and how to write sophisticated macros with parameter lists and repeat loops. It also describes how to use these features in conjunction with local symbols, macro operators, and predefined macro functions.

 

Text Macros

You can give a sequence of characters a symbolic name and then use the name in place of the text later in the source code. The named text is called a text macro.

The TEXTEQU directive defines a text macro, as these examples show:

name TEXTEQU <text>
name TEXTEQU macroId | textmacro
name TEXTEQU %constExpr

In the previous lines, text is a sequence of characters enclosed in angle brackets, macroId is a previously defined macro function, textmacro is a previously defined text macro, and %constExpr is an expression that evaluates to text.

Here are some examples:

msg     TEXTEQU <Some text>         ; Text assigned to symbol
string  TEXTEQU msg                 ; Text macro assigned to symbol
msg     TEXTEQU <Some other text>   ; New text assigned to symbol
value   TEXTEQU %(3 + num)          ; Text representation of resolved
                                    ;   expression assigned to symbol

The first line assigns text to the symbol msg. The second line equates the text of the msg text macro with a new text macro called string. The third line assigns new text to msg. Although msg has new text, string retains its original text value. The fourth line assigns 7 to value if num equals 4. If a text macro expands to another text macro (or macro function, as discussed on page 248), the resulting text macro will expand recursively.

Text macros are useful for naming strings of text that do not evaluate to integers. For example, you might use a text macro to name a floating-point constant or a bracketed expression. Here are some practical examples:

pi      TEXTEQU <3.1416>            ; Floating point constant
WPT     TEXTEQU <WORD PTR>          ; Sequence of key words
arg1    TEXTEQU <[bp+4]>            ; Bracketed expression

Macro Procedures

If your program must perform the same task many times, you can avoid repeatedly typing the same statements each time by writing a macro procedure. Think of macro procedures (commonly called macros) as text-processing mechanisms that automatically generate repeated text.

This section uses the term “macro procedure” rather than “macro” when necessary to distinguish between a macro procedure and a macro function. Macro functions are described in “Returning Values with Macro Functions.”

Conforming to common usage, this chapter occasionally speaks of “calling” a macro, a term that deserves further scrutiny. It’s natural to think of a program calling a macro procedure in the same way it calls a normal subroutine procedure, because they seem to perform identically. However, a macro is simply a representative for real code. Wherever a macro name appears in your program, so in reality does all the code the macro represents. A macro does not cause the processor to vector off to a new location as does a normal procedure. Thus, the expression “calling a macro” may imply the effect, but does not accurately describe what actually occurs.

Creating Macro Procedures

You can define a macro procedure without parameters by placing the desired statements between the MACRO and ENDM directives:

name  MACRO
statements
ENDM

For example, suppose you want a program to beep when it encounters certain errors. You could define a beep macro as follows:

beep    MACRO
    mov  ah, 2          ;; Select DOS Print Char function
    mov  dl, 7          ;; Select ASCII 7 (bell)
    int  21h            ;; Call DOS
ENDM

The double semicolons mark the beginning of macro comments. Macro comments appear in a listing file only at the macro’s initial definition, not at the point where the macro is referenced and expanded. Listings are usually easier to read if the comments aren’t repeatedly expanded. However, regular comments (those with a single semicolon) are listed in macro expansions. See Appendix C for listing files and examples of how macros are expanded in listings.

Once you define a macro, you can call it anywhere in the program by using the macro’s name as a statement. The following example calls the beep macro two times if an error flag has been set.

        .IF     error   ; If error flag is true
        beep            ;   execute macro two times
        beep
        .ENDIF

 

During assembly, the instructions in the macro replace the macro reference. The listing file shows:

                           .IF                        error
0017  80 3E 0000 R 00   *         cmp    error, 000h
001C  74 0C             *         je     @C0001
                           beep
001E  B4 02               1         mov     ah, 2
0020  B2 07               1         mov     dl, 7
0022  CD 21               1         int     21h
                           beep
0024  B4 02               1         mov     ah, 2
0026  B2 07               1         mov     dl, 7
0028  CD 21               1         int     21h
                           .ENDIF
002A                  *@C0001:

Contrast this with the results of defining beep as a procedure using the PROC directive and then calling it with the CALL instruction.

Many such tasks can be handled as either a macro or a procedure. In deciding which method to use, you must choose between speed and size. For repetitive tasks, a procedure produces smaller code, because the instructions physically appear only once in the assembled program. However, each call to the procedure involves the additional overhead of a CALL and RET instruction. Macros do not require a change in program flow and so execute faster, but generate the same code multiple times rather than just once.

Passing Arguments to Macros

By defining parameters for macros, you can define a general task and then execute variations of it by passing different arguments each time you call the macro. The complete syntax for a macro procedure includes a parameter list:

name  MACRO parameterlist
statements
ENDM

The parameterlist can contain any number of parameters. Use commas to separate each parameter in the list. You cannot use reserved words as parameter names unless you disable the keyword with OPTION NOKEYWORD. You must also set the compatibility mode with OPTION M510 or the /Zm command-line option.

To pass arguments to a macro, place the arguments after the macro name when you call the macro:

macroname  arglist

The assembler treats as one item all text between matching quotation marks in an arglist.

The beep macro introduced in the previous section used the MS-DOS interrupt to write only the bell character (ASCII 7). We can rewrite the macro with a parameter that accepts any character:

writechar MACRO char
    mov  ah, 2                  ;; Select DOS Print Char function
    mov  dl, char               ;; Select ASCII char
    int  21h                    ;; Call DOS
ENDM

Whenever it expands the macro, the assembler replaces each instance of char with the given argument value. The rewritten macro now writes any character to the screen, not just ASCII 7:

         writechar 7             ; Causes computer to beep
         writechar ‘A’           ; Writes A to screen

If you pass more arguments than there are parameters, the additional arguments generate a warning (unless you use the VARARG keyword; see page 242). If you pass fewer arguments than the macro procedure expects, the assembler assigns empty strings to the remaining parameters (unless you have specified default values). This may cause errors. For example, a reference to the writechar macro with no argument results in the following line:

         mov     dl,

The assembler generates an error for the expanded statement but not for the macro definition or the macro call.

You can make macros more flexible by leaving off arguments or adding additional arguments. The next section tells some of the ways your macros can handle missing or extra arguments.

Specifying Required and Default Parameters

Macro parameters can have special attributes to make them more flexible and improve error handling. You can make parameters required, give them default values, or vary their number. Variable parameters are used almost exclusively with the FOR directive, so are covered in “FOR Loops and Variable-Length Parameters,” later in this chapter.

 

The syntax for a required parameter is:

parameter:REQ

For example, you can rewrite the writechar macro to require the char
parameter:

writechar MACRO char:REQ
    mov  ah, 2                  ;; Select DOS Print Char function
    mov  dl, char               ;; Select ASCII char
    int  21h                    ;; Call DOS
ENDM

If the call does not include a matching argument, the assembler reports the error in the line that contains the macro reference. REQ can thus improve error reporting.

You can also accommodate missing parameters by specifying a default value, like this:

parameter:=textvalue

Suppose that you often use writechar to beep by printing ASCII 7. The following macro definition uses an equal sign to tell the assembler to assume the parameter char is 7 unless you specify otherwise:

writechar  MACRO char:=<7>
    mov  ah, 2                  ;; Select DOS Print Char function
    mov  dl, char               ;; Select ASCII char
    int  21h                    ;; Call DOS
ENDM

If a reference to this macro does not include the argument char, the assembler fills in the blank with the default value of 7 and the macro beeps when called.

Enclose the default parameter value in angle brackets so the assembler recognizes the supplied value as a text value. This is explained in detail in “Text Delimiters and the Literal-Character Operator,” later in this chapter.

Missing arguments can also be handled with the IFB, IFNB, .ERRB, and .ERRNB directives. They are described in the section “Conditional Directives” in chapter 1 and in Help. Here is a slightly more complex macro that uses some of these techniques:

Scroll MACRO distance:REQ, attrib:=<7>, tcol, trow, bcol, brow
    IFNB <tcol>             ;; Ignore arguments if blank
        mov   cl, tcol
    ENDIF
    IFNB <trow>
        mov   ch, trow
    ENDIF
    IFNB <bcol>
        mov   dl, bcol
    ENDIF
    IFNB <brow>
        mov   dh, brow
    ENDIF
    IFDIFI <attrib>, <bh>   ;; Don’t move BH onto itself
        mov   bh, attrib
    ENDIF
    IF distance LE 0        ;; Negative scrolls up, positive down
        mov   ax, 0600h + (-(distance) AND 0FFh)
    ELSE
        mov   ax, 0700h + (distance AND 0FFh)
    ENDIF
    int   10h
ENDM

In this macro, the distance parameter is required. The attrib parameter has a default value of 7 (white on black), but the macro also tests to make sure the corresponding argument isn’t BH, since it would be inefficient (though legal) to load a register onto itself. The IFNB directive is used to test for blank arguments. These are ignored to allow the user to manipulate rows and columns directly in registers CX and DX at run time.  

The following shows two valid ways to call the macro:

        ; Assume DL and CL already loaded
        dec     dh                   ; Decrement top row
        inc     ch                   ; Increment bottom row
        Scroll -3                    ; Scroll white on black dynamic
                                     ;   window up three lines
        Scroll 5, 17h, 2, 2, 14, 12  ; Scroll white on blue constant
                                     ;   window down five lines

This macro can generate completely different code, depending on its arguments. In this sense, it is not comparable to a procedure, which always has the same code regardless of arguments.

 

Defining Local Symbols in Macros

You can make a symbol local to a macro by identifying it at the start of the macro with the LOCAL directive. Any identifier may be declared local.

You can choose whether you want numeric equates and text macros to be local or global. If a symbol will be used only inside a particular macro, you can declare it local so that the name will be available for other declarations outside the macro.

You must declare as local any labels within a macro, since a label can occur only once in the source. The LOCAL directive makes a special instance of the label each time the macro appears. This prevents redefinition of the label when expanding the macro. It also allows you to reuse the label elsewhere in your code.

You must declare all local symbols immediately following the MACRO statement (although blank lines and comments may precede the local symbol). Separate each symbol with a comma. You can attach comments to the LOCAL statement and list multiple LOCAL statements in the macro. Here is an example macro that declares local labels:

power   MACRO   factor:REQ, exponent:REQ
    LOCAL   again, gotzero      ;; Local symbols
    sub     dx, dx              ;; Clear top
    mov     ax, 1               ;; Multiply by one on first loop
    mov     cx, exponent        ;; Load count
    jcxz    gotzero             ;; Done if zero exponent
    mov     bx, factor          ;; Load factor
again:
    mul     bx                  ;; Multiply factor times exponent
    loop    again               ;; Result in AX
gotzero:
ENDM

If the labels again and gotzero were not declared local, the macro would work the first time it is called, but it would generate redefinition errors on subsequent calls. MASM implements local labels by generating different names for them each time the macro is called. You can see this in listing files. The labels in the power macro might be expanded to ??0000 and ??0001 on the first call and to ??0002 and ??0003 on the second.

 

You should avoid using anonymous labels in macros (see “Anonymous Labels” in Chapter 7). Although legal, they can produce unwanted results if you expand a macro near another anonymous label. For example, consider what happens in the following:

Update  MACRO arg1
@@: .
    .
    .
    loop @B
ENDM
        .
        .
        .
        jcxz    @F
        Update  ax
@@:

Expanding Update places another anonymous label between the jump and its target. The line

        jcxz    @F

consequently jumps to the start of the loop rather than over the loop exactly the opposite of what the programmer intended.

Assembly-Time Variables and Macro Operators

In writing macros, you will often assign and modify values assigned to symbols. Think of these symbols as assembly-time variables. Like memory variables, they are symbols that represent values. But since macros are processed at assembly time, any symbol modified in a macro must be resolved as a constant by the end of assembly.

The three kinds of assembly-time variables are:

  Macro parameters

  Text macros

  Macro functions

 

When the assembler expands a macro, it processes the symbols in the order shown here. MASM first replaces macro parameters with the text of their actual arguments, then expands text macros.

 

Macro parameters are similar to procedure parameters in some ways, but they also have important differences. In a procedure, a parameter has a type and a memory location. Its value can be modified within the procedure. In a macro, a parameter is a placeholder for the argument text. The value can only be assigned to another symbol or used directly; it cannot be modified. The macro may interpret the argument text it receives either as a numeric value or as a text value.

It is important to understand the difference between text values and numeric values. Numeric values can be processed with arithmetic operators and assigned to numeric equates. Text values can be processed with macro functions and assigned to text macros. 

Macro operators are often helpful when processing assembly-time variables. Table 9.1 shows the macro operators that MASM provides.

Table 9.1    MASM Macro Operators

Symbol

Name

Description

< >

Text Delimiters

Opens and closes a literal string.

!

Literal-Character Operator

Treats the next character as a literal character, even if it would normally have another meaning.

%

Expansion Operator

Causes the assembler to expand a constant expression or text macro.

&

Substitution Operator

Tells the assembler to replace a macro parameter or text macro name with its
actual value.

 

The next sections explain these operators in detail.

Text Delimiters and the Literal-Character Operator

The angle brackets (< >) are text delimiters. A text value is usually delimited when assigning a text macro. You can do this with TEXTEQU, as previously shown, or with the SUBSTR and CATSTR directives discussed in “String Directives and Predefined Functions,” later in this chapter.

By delimiting the text of macro arguments, you can pass text that includes spaces, commas, semicolons, and other special characters. The following example expands a macro called work in two different ways:

        work    <1, 2, 3, 4, 5> ; Passes one argument with 13 chars,
                                ;   including commas and spaces
        work    1, 2, 3, 4, 5   ; Passes five arguments, each
                                ;   with 1 character

The literal-character operator (!) lets you include angle brackets as part of a delimited text value, so the assembler does not interpret them as delimiters. The assembler treats the character following ! literally rather than as a special character, like this:

errstr  TEXTEQU <Expression !> 255>  ; errstr = “Expression > 255”

Text delimiters also have a special use with the FOR directive, as explained in “FOR Loops and Variable-Length Parameters,” later in this chapter.

Expansion Operator

The expansion operator (%) expands text macros or converts constant expressions into their text representations. It performs these tasks differently in different contexts, as discussed in the following.

Converting Numeric Expressions to Text

The expansion operator can convert numbers to text. The operator forces immediate evaluation of a constant expression and replaces it with a text value consisting of the digits of the result. The digits are generated in the current radix (default decimal).

This application of the expansion operator is useful when defining a text macro, as the following lines show. Notice how you can enclose expressions with parentheses to make them more readable:

a       TEXTEQU <3 + 4>         ; a = “3 + 4”
b       TEXTEQU %3 + 4          ; b = “7”
c       TEXTEQU %(3 + 4)        ; c = “7”

When assigning text macros, you can use numeric equates in the constant expressions, but not text macros:

num     EQU     4               ; num = 4
numstr  TEXTEQU <4>             ; numstr = <4>
a       TEXTEQU %3 + num        ; a = <7>
b       TEXTEQU %3 + numstr     ; b = <7>

The expansion operator gives you flexibility when passing arguments to macros. It lets you pass a computed value rather than the literal text of an expression. The following example illustrates by defining a macro

work    MACRO   arg
    mov ax, arg * 4
ENDM

 

which accepts different arguments:

        work    2 + 3           ; Passes “2 + 3”
                                ; Code: mov ax, 2 + (3 * 4)
        work    %2 + 3          ; Passes 5
                                ; Code: mov ax, 5 * 4
        work    2 + num         ; Passes “2 + num”
        work    %2 + num        ; Passes “6”
        work    2 + numstr      ; Passes “2 + numstr”
        work    %2 + numstr     ; Passes “6”

You must consider operator precedence when using the expansion operator. Parentheses inside the macro can force evaluation in a desired order:

work    MACRO   arg
    mov ax, (arg) * 4
ENDM

        work    2 + 3           ; Code: mov ax, (2 + 3) * 4
        work    %2 + 3          ; Code: mov ax, (5) * 4

Several other uses for the expansion operator are reviewed in “Returning Values with Macro Functions,” later in this chapter.

Expansion Operator as First Character on a Line

The expansion operator has a different meaning when used as the first character on a line. In this case, it instructs the assembler to expand any text macros and macro functions it finds on the rest of the line.

This feature makes it possible to use text macros with directives such as ECHO, TITLE, and SUBTITLE, which take an argument consisting of a single text value. For instance, ECHO displays its argument to the standard output device during assembly. Such expansion can be useful for debugging macros and expressions, but the requirement that its argument be a single text value may have unexpected results. Consider this example:

    ECHO    Bytes per element: %(SIZEOF array / LENGTHOF array)

Instead of evaluating the expression, this line echoes it:

Bytes per element: %(SIZEOF array / LENGTHOF array)

However, you can achieve the desired result by assigning the text of the expression to a text macro and then using the expansion operator at the beginning of the line to force expansion of the text macro.

temp    TEXTEQU %(SIZEOF array / LENGTHOF array)
%       ECHO    Bytes per element: temp

Note that you cannot get the same results simply by putting the % at the beginning of the first echo line, because % expands only text macros, not numeric equates or constant expressions.

Here are more examples of the expansion operator at the start of a line:

; Assume memmod, lang, and os specified with /D option
%   SUBTITLE  Model: memmod  Language: lang  Operating System: os

; Assume num defined earlier
tnum    TEXTEQU %num
%       .ERRE   num LE 255, <Failed because tnum !> 255>

Substitution Operator

References to a parameter within a macro can sometimes be ambiguous. In such cases, the assembler may not expand the argument as you intend. The substitution operator (&) lets you identify unambiguously any parameter within a macro.

As an example, consider the following macro:

errgen  MACRO   num, msg
    PUBLIC  errnum
    errnum BYTE    “Error num: msg”
ENDM

This macro is open to several interpretations:

  Is errnum a distinct word or the word err next to the parameter num?

  Should num and msg within the string be treated literally as part of the string or as arguments?

 

In each case, the assembler chooses the most literal interpretation. That is, it treats errnum as a distinct word, and num and msg as literal parts of the string.

The substitution operator can force different interpretations. If we rewrite the macro with the & operator, it looks like this:

errgen  MACRO   num, msg
    PUBLIC  err&num
    err&num BYTE    “Error &num: &msg”
ENDM

 

When called with the following arguments,

errgen  5, <Unreadable disk>

the macro now generates this code:

        PUBLIC  err5
err5    BYTE    “Error 5: Unreadable disk”

When it encounters the & operator, the assembler interprets subsequent text as a parameter name until the next & or until the next separator character (such as a space, tab, or comma). Thus, the assembler correctly parses the expression err&num because num is delimited by & and a space. The expression could also be written as err&num&, which again unambiguously identifies num as a parameter.

The rule also works in reverse. You can delimit a parameter reference with & at the end rather than at the beginning. For example, if num is 5, the expression num&12 resolves to “512.”

The assembler processes substitution operators from left to right. This can have unexpected results when you are pasting together two macro parameters. For example, if arg1 has the value var and arg2 has the value 3, you could paste them together with this statement:

&arg1&&arg2&    BYTE    “Text”

Eliminating extra substitution operators, you might expect the following to be equivalent:

&arg1&arg2      BYTE    “Text”

However, this actually produces the symbol vararg2, because in processing from left to right, the assembler associates both the first and the second & symbols with the first parameter. The assembler replaces &arg1& by var, producing vararg2. The arg2 is never evaluated. The correct abbreviation is:

arg1&&arg2      BYTE    “Text”

which produces the desired symbol var3. The symbol arg1&&arg2 is replaced by var&arg2, which is replaced by var3.

The substitution operator is also necessary if you want to substitute a text macro inside quotes. For example,

arg     TEXTEQU <hello>
%echo   This is a string “&arg” ; Produces: This is a string “hello”
%echo   This is a string “arg”  ; Produces: This is a string “arg”

You can also use the substitution operator in lines beginning with the expansion operator (%) symbol, even outside macros (see page 236). It may be necessary to use the substitution operator to paste text macro names to adjacent characters or symbol names, as shown here:

text    TEXTEQU <var>
value   TEXTEQU %5
%       ECHO    textvalue is text&&value

This echoes the message

textvalue is var5

Macro substitution always occurs before evaluation of the high-level control structures. The assembler may therefore mistake a bit-test operator (&) in your macro for a substitution operator. You can guarantee the assembler correctly recognizes a bit-test operator by enclosing its operands in parentheses, as shown here:

test    MACRO   x
    .IF ax==&x      ; &x substituted with parameter value
    mov     ax, 10
    .ELSEIF ax&(x)  ; & is bitwise AND
    mov ax, 20
    .ENDIF
ENDM

The rules for using the substitution operator have changed significantly since MASM 5.1, making macro behavior more consistent and flexible. If you have macros written for MASM 5.1 or earlier, you can specify the old behavior by using OLDMACROS or M510 with the OPTION directive (see page 24).

Defining Repeat Blocks with Loop Directives

A “repeat block” is an unnamed macro defined with a loop directive. The loop directive generates the statements inside the repeat block a specified number of times or until a given condition becomes true.

MASM provides several loop directives, which let you specify the number of loop iterations in different ways. Some loop directives can also accept arguments for each iteration. Although the number of iterations is usually specified in the directive, you can use the EXITM directive to exit the loop early.

Repeat blocks can be used outside macros, but they frequently appear inside macro definitions to perform some repeated operation in the macro. Since repeat blocks are macros themselves, they end with the ENDM directive.

 

This section explains the following four loop directives: REPEAT, WHILE, FOR, and FORC. In versions of MASM prior to 6.0, REPEAT was called REPT, FOR was called IRP, and FORC was called IRPC. MASM 6.1 recognizes the old names.

The assembler evaluates repeat blocks on the first pass only. You should therefore avoid using address spans as loop counters, as in this example:

REPEAT  (OFFSET label1 - OFFSET label2) ; Don't do this!

Since the distance between two labels may change on subsequent assembly passes as the assembler optimizes code, you should not assume that address spans remain constant between passes.

 

Note

The REPEAT and WHILE directives should not be confused with the REPEAT and WHILE directives (see “Loop-Generating Directives” in Chapter 7), which generate loop and jump instructions for run-time program control.

 

REPEAT Loops

REPEAT is the simplest loop directive. It specifies the number of times to generate the statements inside the macro. The syntax is:

REPEAT constexpr
statements
ENDM

The constexpr can be a constant or a constant expression, and must contain no forward references. Since the repeat block expands at assembly time, the number of iterations must be known then.

Here is an example of a repeat block used to generate data. It initializes an array containing sequential ASCII values for all uppercase letters.

alpha   LABEL   BYTE            ;  Name the data generated
letter  =       ‘A’             ;  Initialize counter
REPEAT  26                      ;; Repeat for each letter
    BYTE    letter              ;; Allocate ASCII code for letter
    letter  = letter + 1        ;; Increment counter
ENDM

 

Here is another use of REPEAT, this time inside a macro:

beep    MACRO   iter:=<3>
    mov ah, 2                   ;; Character output function
    mov dl, 7                   ;; Bell character
    REPEAT iter                 ;; Repeat number specified by macro
        int 21h                 ;; Call DOS
    ENDM
ENDM

WHILE Loops

The WHILE directive is similar to REPEAT, but the loop continues as long as a given condition is true. The syntax is:

WHILE expression
statements
ENDM

The expression must be a value that can be calculated at assembly time. Normally, the expression uses relational operators, but it can be any expression that evaluates to zero (false) or nonzero (true). Usually, the condition changes during the evaluation of the macro so that the loop won’t attempt to generate an infinite amount of code. However, you can use the EXITM directive to break out of the loop.

The following repeat block uses the WHILE directive to allocate variables initialized to calculated values. This is a common technique for generating lookup tables. (A lookup table is any list of precalculated results, such as a table of interest payments or trigonometric values or logarithms. Programs optimized for speed often use lookup tables, since calculating a value often takes more time than looking it up in a table.)

cubes   LABEL   BYTE            ;; Name the data generated
root    =   1                   ;; Initialize root
cube    =   root * root * root  ;; Calculate first cube
WHILE   cube LE 32767           ;; Repeat until result too large
    WORD   cube                 ;; Allocate cube
    root   =    root + 1        ;; Calculate next root and cube
    cube   =    root * root * root
ENDM

 

FOR Loops and Variable-Length Parameters

With the FOR directive you can iterate through a list of arguments, working on each of them in turn. It has the following syntax:

FOR parameter, <argumentlist>
statements

ENDM

The parameter is a placeholder that represents the name of each argument inside the FOR block. The argument list must contain comma-separated arguments and must always be enclosed in angle brackets. Here’s an example of a FOR block:

series  LABEL   BYTE
FOR     arg, <1,2,3,4,5,6,7,8,9,10>
    BYTE  arg DUP (arg)
ENDM

On the first iteration, the arg parameter is replaced with the first argument, the value 1. On the second iteration, arg is replaced with 2. The result is an array with the first byte initialized to 1, the next 2 bytes initialized to 2, the next 3 bytes initialized to 3, and so on.

The argument list is given specifically in this example, but in some cases the list must be generated as a text macro. The value of the text macro must include the angle brackets.

arglist TEXTEQU <!<3,6,9!>>     ; Generate list as text macro
%FOR  arg, arglist
    .                           ; Do something to arg
    .
    .
ENDM

Note the use of the literal character operator (!) to identify angle brackets as characters, not delimiters. See “Text Delimiters (< >) and the Literal-Character Operator,” earlier in this chapter.

The FOR directive also provides a convenient way to process macros with a variable number of arguments. To do this, add VARARG to the last parameter to indicate that a single named parameter will have the actual value of all additional arguments. For example, the following macro definition includes the three possible parameter attributes required, default, and variable.

work    MACRO   rarg:REQ, darg:=<5>, varg:VARARG

 

The variable argument must always be last. If this macro is called with the statement

        work 4, , 6, 7, a, b

the first argument is received as the value 4, the second is replaced by the default value 5, and the last four are received as the single argument <6, 7, a, b>. This is the same format expected by the FOR directive. The FOR directive discards leading spaces but recognizes trailing spaces.

The following macro illustrates variable arguments:

show    MACRO chr:VARARG
    mov     ah, 02h
    FOR arg, <chr>
        mov     dl, arg
        int     21h
    ENDM
ENDM

When called with

         show  ‘O’, ‘K’, 13, 10

the macro displays each of the specified characters one at a time.

The parameter in a FOR loop can have the required or default attribute. You can modify the show macro to make blank arguments generate errors: 

show    MACRO chr:VARARG
    mov     ah, 02h
    FOR arg:REQ, <chr>
        mov     dl, arg
        int     21h
    ENDM
ENDM

The macro now generates an error if called with

        show  ‘O’,, ‘K’, 13, 10

 

Another approach would be to use a default argument:

show    MACRO chr:VARARG
    mov     ah, 02h
    FOR arg:=<‘ ’>, <chr>
        mov     dl, arg
        int     21h
    ENDM
ENDM

Now calling the macro with

        show  ‘O’,, ‘K’, 13, 10

inserts the default character, a space, for the blank argument.

FORC Loops

The FORC directive is similar to FOR, but takes a string of text rather than a list of arguments. The statements are assembled once for each character (including spaces) in the string, substituting a different character for the parameter each time through.  

The syntax looks like this:

FORC parameter, < text>
statements
ENDM

The text must be enclosed in angle brackets. The following example illustrates FORC:

FORC arg, <ABCDEFGHIJKLMNOPQRSTUVWXYZ>
    BYTE  ‘&arg’             ;; Allocate uppercase letter
    BYTE  ‘&arg’ + 20h       ;; Allocate lowercase letter
    BYTE  ‘&arg’ - 40h       ;; Allocate ordinal of letter
ENDM

Notice that the substitution operator must be used inside the quotation marks to make sure that arg is expanded to a character rather than treated as a literal string.

With versions of MASM earlier than 6.0, FORC is often used for complex parsing tasks. A long sentence can be examined character by character. Each character is then either thrown away or pasted onto a token string, depending on whether it is a separator character. The new predefined macro functions and string processing directives discussed in the following section are usually more efficient for these tasks.

String Directives and Predefined Functions

The assembler provides four directives for manipulating text:

Directive

Description

SUBSTR

Assigns part of string to a new symbol.

INSTR

Searches for one string within another.

SIZESTR

Determines the size of a string.

CATSTR

Concatenates one or more strings to a single string.

 

These directives assign a processed value to a text macro or numeric equate. For example, the following lines

num     =       7
newstr  CATSTR  <3 + >, %num, < = > , %3 + num ; "3 + 7 = 10"

assign the string "3 + 7 = 10" to newstr. CATSTR and SUBSTR assign text in the same way as the TEXTEQU directive. SIZESTR and INSTR assign a number in the same way as the = operator. The four string directives take only text values as arguments. Use the expansion operator (%) when you need to make sure that constants and numeric equates expand to text, as shown in the preceding lines.  

Each of the string directives has a corresponding predefined macro function version: @SubStr, @InStr, @SizeStr, and @CatStr. Macro functions are similar to the string directives, but you must enclose their arguments in parentheses. Macro functions return text values and can appear in any context where text is expected. The following section, “Returning Values with Macro Functions,” tells how to write your own macro functions. The following example is equivalent to the previous CATSTR example:

num     =       7
newstr  TEXTEQU @CatStr( <3 + >, %num, < = > , %3 + num )

Macro functions are often more convenient than their directive counterparts because you can use a macro function as an argument to a string directive or to another macro function. Unlike string directives, predefined macro function names are case sensitive when you use the /Cp command-line option.

Each string directive and predefined function acts on a string, which can be any textItem. The textItem can be text enclosed in angle brackets (< >), the name of a text macro, or a constant expression preceded by % (as in %constExpr). Refer to Appendix B, “BNF Grammar,” for a list of types that textItem can represent.

The following sections summarize the syntax for each of the string directives and functions. The explanations focus on the directives, but the functions work the same except where noted.

SUBSTR

name SUBSTR string, start[[, length]]
@SubStr( string, start[[, length]] )

The SUBSTR directive assigns a substring from a given string to the symbol name. The start parameter specifies the position in string, beginning with 1, to start the substring. The length gives the length of the substring. If you do not specify length, SUBSTR returns the remainder of the string, including the start character.  

INSTR

name INSTR [[start,]] string, substring
text@InStr( [[start]], string, substring )

The INSTR directive searches a specified string for an occurrence of substring and assigns its position number to name. The search is case sensitive. The start parameter is the position in string to start the search for substring. If you do not specify start, it is assumed to be position 1, the start of the string. If INSTR does not find substring, it assigns position 0 to name.

The INSTR directive assigns the position value name as if it were a numeric equate. In contrast, the @InStr returns the value as a string of digits in the current radix.

The @InStr function has a slightly different syntax than the INSTR directive. You can omit the first argument and its associated comma from the directive. You can leave the first argument blank with the function, but a blank function argument must still have a comma. For example,

pos     INSTR   <person>, <son>

is the same as

pos     = @InStr( , <person>, <son> )

You can also assign the return value to a text macro, like this:

strpos  TEXTEQU @InStr( , <person>, <son> )

SIZESTR

name SIZESTR string
text@SizeStr( string )

The SIZESTR directive assigns the number of characters in string to name. An empty string returns a length of zero. The SIZESTR directive assigns the size value to a name as if it were a numeric equate. The @SizeStr function returns the value as a string of digits in the current radix. 

CATSTR

name CATSTR string[, string]...
text@CatStr( string[,  string]... )

The CATSTR directive concatenates a list of text values into a single text value and assigns it to name. TEXTEQU is technically a synonym for CATSTR. TEXTEQU is normally used for single-string assignments, while CATSTR is used for multistring concatenations.    

The following example pushes and pops one set of registers, illustrating several uses of string directives and functions:

; SaveRegs - Macro to generate a push instruction for each
; register in argument list. Saves each register name in the
; regpushed text macro.
regpushed TEXTEQU <>                    ;; Initialize empty string

SaveRegs MACRO regs:VARARG
    LOCAL reg
    FOR reg, <regs>                     ;; Push each register
        push reg                        ;;   and add it to the list
        regpushed CATSTR   <reg>, <,>, regpushed
    ENDM                                ;; Strip off last comma
    regpushed CATSTR <!<>, regpushed    ;; Mark start of list with <
    regpushed SUBSTR regpushed, 1, @SizeStr( regpushed )
    regpushed CATSTR regpushed, <!>>    ;; Mark end with >
ENDM

; RestoreRegs - Macro to generate a pop instruction for registers
; saved by the SaveRegs macro. Restores one group of registers.

RestoreRegs MACRO
    LOCAL reg
    %FOR reg, regpushed                  ;; Pop each register

    pop reg
    ENDM
ENDM

Notice how the SaveRegs macro saves its result in the regpushed text macro for later use by the RestoreRegs macro. In this case, a text macro is used as a global variable. By contrast, the reg text macro is used only in RestoreRegs. It is declared LOCAL so it won’t take the name reg from the global name space. The MACROS.INC file provided with MASM 6.1 includes expanded versions of these same two macros.

Returning Values with Macro Functions

A macro function is a named group of statements that returns a value. When calling a macro function, you must enclose its argument list in parentheses, even if the list is empty. The function always returns text.

MASM 6.1 provides several predefined macro functions for common tasks. The predefined macros include @Environ (see page 10) and the string functions @SizeStr, @CatStr, @SubStr, and @InStr (discussed in the preceding section).

You define macro functions in exactly the same way as macro procedures, except that a macro function always returns a value through the EXITM directive. Here is an example:

DEFINED MACRO   symbol:REQ
    IFDEF symbol
        EXITM <-1>              ;; True
    ELSE
        EXITM <0>               ;; False
    ENDIF
ENDM

This macro works like the defined operator in the C language. You can use it to test the defined state of several different symbols with a single statement, as shown here:

IF DEFINED( DOS ) AND NOT DEFINED( XENIX )
    ;; Do something
ENDIF

Notice that the macro returns integer values as strings of digits, but the IF statement evaluates numeric values or expressions. There is no conflict because the assembler sees the value returned by the macro function exactly as if the user had typed the values directly into the program:

IF -1 AND NOT 0

Returning Values with EXITM

The return value must be text, a text equate name, or the result of another macro function. A macro function must first convert a numeric value such as a constant, a numeric equate, or the result of a numeric expression before returning it. The macro function can use angle brackets or the expansion operator (%) to convert numbers to text. The DEFINED macro, for instance, could have returned its value as 

         EXITM   %-1

Here is another example of a macro function that uses the WHILE directive to calculate factorials:

factorial   MACRO   num:REQ
    LOCAL   i, factor
    factor  =   num
    i       =   1
    WHILE   factor GT 1
        i       =   i * factor
        factor  =   factor - 1
    ENDM
    EXITM   %i
ENDM

The integer result of the calculation is changed to a text string with the expansion operator (%). The factorial macro can define data, as shown here:

var     WORD    factorial( 4 )

This statement initializes var with the number 24 (the factorial of 4).

Using Macro Functions with Variable-Length Parameter Lists

You can use the FOR directive to handle macro parameters with the VARARG attribute. “FOR Loops and Variable-Length Parameters,” page 242, explains how to do this in simple cases where the variable parameters are handled sequentially, from first to last. However, you may sometimes need to process the parameters in reverse order or nonsequentially. Macro functions make these techniques possible.

For example, the following macro function determines the number of arguments in a VARARG parameter:  

hidden@ArgCount MACRO arglist:VARARG
    LOCAL count
    count = 0
    FOR arg, <arglist>
        count = count + 1       ;; Count the arguments
    ENDM
    EXITM %count
ENDM

 

You can use @ArgCount inside a macro that has a VARARG parameter, as shown here:

work    MACRO args:VARARG
%   ECHO Number of arguments is: @ArgCount( args )
ENDM

Another useful task might be to select an item from an argument list using an index to indicate the item. The following macro simplifies this.

hidden@ArgI MACRO index:REQ, arglist:VARARG
    LOCAL count, retstr
    retstr TEXTEQU <>            ;; Initialize count
    count  = 0                   ;; Initialize return string
    FOR arg, <arglist>
        count = count + 1
        IF count EQ index        ;; Item is found
            retstr TEXTEQU <arg> ;; Set return string
            EXITM                ;;   and exit IF
        ENDIF
    ENDM
    EXITM retstr                 ;; Exit function
ENDM

You can use @ArgI like this:

work    MACRO args:VARARG
%   ECHO Third argument is: @ArgI( 3, args )
ENDM

Finally, you might need to process arguments in reverse order. The following macro returns a new argument list in reverse order.

hidden@ArgRev MACRO arglist:REQ
    LOCAL txt, arg
    txt TEXTEQU <>
%   FOR arg, <arglist>
        txt CATSTR <arg>, <,>, txt      ;; Paste each onto list
    ENDM
                                        ;; Remove terminating comma
    txt SUBSTR  txt, 1, @SizeStr( %txt ) - 1
    txt CATSTR  <!<>, txt, <!>>         ;; Add angle brackets   
    EXITM txt
ENDM

 

Here is an example showing @ArgRev in use:

work    MACRO   args:VARARG
%   FOR  arg, @ArgRev( <args> )   ;; Process in reverse order
        ECHO    arg
    ENDM
ENDM

These three macro functions appear in the MACROS.INC include file, located on one of the MASM distribution disks.

Expansion Operator in Macro Functions

This list summarizes the behavior of the expansion operator (%) with macro
functions.

  If a macro function is preceded by a %, it will be expanded. However, if it expands to a text macro or a macro function call, it will not expand further.

  If you use a macro function call as an argument for another macro function call, a % is not needed.

  If a macro function is called inside angle brackets and is preceded by %, it will be expanded.

 

Advanced Macro Techniques

The concept of replacing macro names with predefined macro text is simple in theory, but it has many implications and complications. Here is a brief summary of some advanced techniques you can use in macros.

Defining Macros within Macros

Macros can define other macros, a technique called “nesting macros.” MASM expands macros as it encounters them, so nested macros are always processed in nesting order. You cannot reference a nested macro directly in your program, since the assembler begins expansion from the outer macro. In effect, a nested macro is local to the macro that defines it. Only the amount of available memory limits the number of macros a program can nest.

 

The following example demonstrates how one macro can define another. The macro takes as an argument the name of a shift or rotate instruction, then creates another macro that simplifies the instruction for 8088/86 processors.

shifts  MACRO   opname                  ;; Macro generates macros
    opname&s    MACRO operand:REQ, rotates:=<1>
        IF rotates LE 2                 ;; One at a time is faster
            REPEAT rotate               ;;   for 2 or less
                opname  operand, 1
            ENDM
        ELSE                            ;; Using CL is faster for
            mov     cl, rotates         ;;   more than 2
            opname  operand, cl
        ENDIF
    ENDM
ENDM

Recall that the 8086 processor allows only 1 or CL as an operand for shift and rotate instructions. Expanding shifts generates a macro for the shift instruction that uses whichever operand is more efficient. You create the entire series of macros, one for each shift instruction, like this:

         ; Call macro repeatedly to make new macros
        shifts  ror                     ; Generates rors
        shifts  rol                     ; Generates rols
        shifts  shr                     ; Generates shrs
        shifts  shl                     ; Generates shls
        shifts  rcl                     ; Generates rcls
        shifts  rcr                     ; Generates rcrs
        shifts  sal                     ; Generates sals
        shifts  sar                     ; Generates sars

Then use the new macros as replacements for shift instructions, like this:

        shrs    ax, 5
        rols    bx, 3

Testing for Argument Type and Environment

Macros can expand conditional blocks of code by testing for argument type with the OPATTR operator. OPATTR returns a single word constant that indicates the type and scope of an expression, like this:

OPATTR expression

If expression is not valid or is forward-referenced, OPATTR returns a 0. Otherwise, the return value incorporates the bit flags shown in the table below.

 

OPATTR serves as an enhanced version of the .TYPE operator, which returns only the low byte (bits 0 7) shown in the table. Bits 11 15 of the return value are undefined.

Bit

Set If expression

0

References a code label

1

Is a memory variable or has a relocatable data label

2

Is an immediate value

3

Uses direct memory addressing

4

Is a register value

5

References no undefined symbols and is without error

6

Is relative to SS

7

References an external label

8 10

Has the following language type:

   000 No language type

   001 C

   010 SYSCALL

   011 STDCALL

   100 Pascal

   101 FORTRAN

   110 Basic

 

A macro can use OPATTR to determine if an argument is a constant, a register, or a memory operand. With this information, the macro can conditionally generate the most efficient code depending on argument type.

For example, given a constant argument, a macro can test it for 0. Depending on the argument’s value, the code can select the most effective method to load the value into a register:

        IF CONST
        mov     bx, CONST    ; If CONST > 0, move into BX
        ELSE
        sub     bx, bx       ; More efficient if CONST = 0
        ENDIF

 

The second method is faster than the first, yet has the same result (with the byproduct of changing the processor flags).

The following macro illustrates some techniques using OPATTR by loading an address into a specified offset register:

load    MACRO reg:REQ, adr:REQ
    IF (OPATTR (adr)) AND 00010000y    ;; Register
        IFDIFI reg, adr                ;; Don’t load register
            mov     reg, adr           ;;   onto itself
        ENDIF
    ELSEIF (OPATTR (adr)) AND 00000100y
        mov     reg, adr               ;; Constant
    ELSEIF (TYPE (adr) EQ BYTE) OR (TYPE (adr) EQ SBYTE)
        mov    reg, OFFSET adr         ;; Bytes
    ELSEIF (SIZE (TYPE (adr)) EQ 2
        mov    reg, adr                ;; Near pointer
    ELSEIF (SIZE (TYPE (adr)) EQ 4
        mov    reg, WORD PTR adr[0]    ;; Far pointer
        mov    ds,  WORD PTR adr[2]
    ELSE
        .ERR <Illegal argument>
    ENDIF
ENDM

A macro also can generate different code depending on the assembly environment. The predefined text macro @Cpu returns a flag for processor type. The following example uses the more efficient constant variation of the PUSH instruction if the processor is an 80186 or higher.

IF  @Cpu AND 00000010y
    pushc  MACRO op             ;; 80186 or higher
        push op
    ENDM
ELSE
    pushc  MACRO op             ;; 8088/8086
        mov  ax, op
        push ax
    ENDM
ENDIF

Another macro can now use pushc rather than conditionally testing for processor type itself. Although either case produces the same code, using pushc assembles faster because the environment is checked only once.

 

You can test the language and operating system using the @Interface text macro. The memory model can be tested with the @Model, @DataSize, or @CodeSize text macros.

You can save the contexts inside macros with PUSHCONTEXT and
POPCONTEXT. The options for these keywords are: 

Option

Description

ASSUMES

Saves segment register information

RADIX

Saves current default radix

LISTING

Saves listing and CREF information

CPU

Saves current CPU and processor

ALL

All of the above

 

Using Recursive Macros

Macros can call themselves. In MASM 5.1 and earlier, recursion is an important technique for handling variable arguments. MASM 6.1 handles variable arguments much more cleanly with the FOR directive and the VARARG attribute, as described in “FOR Loops and Variable-Length Parameters,” earlier in this chapter. However, recursion is still available and may be useful for some macros.