80x86 Assembler, Part 6

Atrevida Game Programming Tutorial #17
Copyright 1998, Kevin Matz, All Rights Reserved.

Prerequisites:

  • Chapter 16, 80x86 Assembler, Part 5

       
In this short chapter, we'll examine the use of equates and macros to simplify our code. Then we'll take a look at assembler graphics programming using Mode 13h.

Equates

We can declare constants in our assembler programs using the "EQU" (equate) directive. Equates are just like the use of "#define" directives in C or C++ programs. For example, in C or C++, we can do this:

#define MaxSize 10

main ()
{
    int Array[MaxSize];
    .
    .
    .
}

When you compile a C/C++ program, the first compilation stage is the pre-processor stage. The compiler's pre-processor scans the source code listing, and when it sees a #define'd name, such as MaxSize, it removes that name and plugs in the value ("10", in this example) which was specified in the #define directive. So the above C/C++ listing would be converted to:

main ()
{
    int Array[10];
    .
    .
    .
}

An equate directive in assembler is very similar. Here's an example:

MaxSize                           EQU  10

    DATASEG

Array                             DW   MaxSize DUP (0)

    CODESEG
.
.
.
    MOV CX, MaxSize
    ADD AL, MaxSize
.
.
.

"MaxSize" is equated to "10", and so every time the assembler sees the text "MaxSize" in a location where a value, expression or string is expected, it replaces it with the sequence of characters "10". The above assembler listing would become:

    DATASEG

Array                             DW   10 DUP (0)

    CODESEG
.
.
.
    MOV CX, 10
    ADD AL, 10
.
.
.

The equate makes it easy to change the constant. If we wanted a 20-element array, we could change the "MaxSize EQU 10" to "MaxSize EQU 20".

Here are a few notes about equates:

  1. Equates can go anywhere in your program listings, although it's best to place them at the top of the program. It's common to see equates near the top of the program, right above the DATASEG section. You'll often see equates in the DATASEG section as well. (Note that equates and their names or values won't actually get stored in the data segment, because they're not actually variables.)
  2. It's tempting to think of equate names as variable names, but doing so can have unexpected consequences. Can you find the mistake in the MOV instruction in the following listing?

    NumberOfPlayers                   EQU  5
    .
    .
    .
        MOV AX, [NumberOfPlayers]    ; Let AX equal the number of players
    

    Well, let's figure out what the MOV instruction gets translated to:

        MOV AX, [5]
    

    The square brackets say "get the value at address 5", so the word at address DS:0005 will be copied to AX. That's probably not what we meant. We probably meant this:

        MOV AX, NumberOfPlayers
    

    That instruction gets translated to...

        MOV AX, 5
    

    ...which is correct, because AX then equals the number of players, as the original comment indicates.

  3. Note that equated numbers have no specific size; they are not bytes or words or doublewords. As shown in the first assembler example above, you can do this:

       MOV CX, MaxSize
       ADD AL, MaxSize
    

  4. You can do arithmetic with numeric equates and the operators +, -, *, and /, like this:

    Rows                              EQU  20
    Columns                           EQU  30
    TotalElements                     EQU  Rows * Columns
    .
    .
    .
        MOV CX, TotalElements
    

    What does TotalElements equate to? Note that the occurrences of "Rows" are changed to "20" and the occurrences of "Columns" are changed to "30". Then TotalElements equates to "20 * 30", and that is what is substituted for any occurrences of "TotalElements", so "MOV CX, TotalElements" becomes "MOV CX, 20 * 30". The assembler can figure out arithmetic expressions that evaluate to constant values, so that instruction is assembled as "MOV CX, 600".

  5. You can also equate strings, like this:

    PhraseA                           EQU  "Hello "
    PhraseB                           EQU  'World'
    
    NewString                         DB   PhraseA, PhraseB, 13, 10, "$"
    

  6. Turbo Assembler also lets you use "=", which works somewhat like EQU, but with a few differences. It can only be used for numeric values, and if an expression is specified on the right side of the "=", the expression is evaluated right there. If we modify our previous example like this:

    Row                               EQU  20
    Column                            EQU  30
    TotalElements                     =    Rows * Columns
    

    Then "TotalElements" will be equated to "600", not "20 * 30".

    One more useless fact: "=" equates can be "re-used" many times in a program, giving new values to the same name:

    SomeNumber = 5
        MOV AX, SomeNumber                ; Now AX = 5
    SomeNumber = 8
        MOV AX, SomeNumber                ; Now AX = 8
    

Macros

Macros in assembler are somewhat like the "macros" that you can define in C/C++ using "#define". Assembler macros look and act a lot like procedures, but they aren't actually called as procedures are. Macros are expanded inline, much like the way equated names in a program are replaced with their corresponding values.

Macro definitions start with "MACRO" and end with "ENDM". Let's examine a sample macro:

MACRO DisplayCRLF
    PUSH AX
    PUSH DX

    ; Use INT 21h, Service 2 to display a Carriage Return character:
    MOV AH, 2
    MOV DL, 13                         ; CR character is ASCII 13 dec
    INT 21h

    ; Use INT 21h, Service 2 to display a Line Feed character:
    MOV DL, 10                         ; LF character is ASCII 10 dec
    INT 21h

    POP DX
    POP AX
ENDM

Now, in our code segment, whenever we want to print a carriage return and line feed (to make the cursor go to the beginning of the next line), we can simply use the following line:

    DisplayCRLF

When the assembler sees the macro name DisplayCRLF, it replaces it with the text of the macro definition. So the following code listing...

    DATASEG

MyName                            DB   "Elvis Presley", $

    CODESEG
.
.
.
    ; Display a string:
    MOV AH, 9
    MOV DX, OFFSET MyName
    INT 21h

    DisplayCRLF

    ; More code...
    NOP
    NOP
.
.
.

...gets turned into:

    DATASEG

MyName                            DB   "Elvis Presley", $

    CODESEG
.
.
.
    ; Display a string:
    MOV AH, 9
    MOV DX, OFFSET MyName
    INT 21h

    PUSH AX
    PUSH DX

    ; Use INT 21h, Service 2 to display a Carriage Return character:
    MOV AH, 2
    MOV DL, 13                         ; CR character is ASCII 13 dec
    INT 21h

    ; Use INT 21h, Service 2 to display a Line Feed character:
    MOV DL, 10                         ; LF character is ASCII 10 dec
    INT 21h

    POP DX
    POP AX

    ; More code...
    NOP
    NOP
.
.
.

The line "DisplayCRLF" is removed, and the macro definition is plugged into its place. Again, this is usually called inline expansion, and it's very much like a giant #define in C/C++.

Macros are useful: they save a lot of typing, and just like with a procedure, if we want to change how we do a task, we only have to change it in one place.

Macros offer slightly more speed than procedures, because there is no overhead (other than saving and restoring registers as necessary). When we call a procedure using CALL, the computer must put the return address on the stack, jump to the start of the procedure by changing the IP register (and CS if necessary), and then at the end of the procedure, the return address must be popped off so that execution can continue at the original place. If a procedure uses parameters or local variables, then there are more stack operations. When we use a macro, there is none of this overhead; no jumps are required because the code is right there in place. But this means the code is getting repeated every time we use the macro. (With a procedure, we only have one copy of the code, which we can jump to repeatedly.) So it's a trade-off between time and space -- we can occupy more space but go faster (using macros), or we can go slightly slower but take up less space (using procedures).

Can we use parameters with macros? Yes, and here is an example of a macro that takes one parameter:

MACRO DisplayCharacter  CharToDisplay
    PUSH AX
    PUSH DX

    ; Use INT 21h, Service 2 to display a single character:
    MOV DL, CharToDisplay
    MOV AH, 2
    INT 21h

    POP DX
    POP AX
ENDM

Then, to use this macro, we can use a line such as:

    DisplayCharacter 'W'

or

    DisplayCharacter [MyChar]

The assembler expands the DisplayCharacter macro, and every time the assembler sees the text "CharToDisplay" in a place where a value is expected, it replaces it with the text of the parameter that was specified.

So, if we used "DisplayCharacter 'Q'", it would get replaced with:

    PUSH AX
    PUSH DX

    ; Use INT 21h, Service 2 to display a single character:
    MOV DL, 'Q'
    MOV AH, 2
    INT 21h

    POP DX
    POP AX

Here's an somewhat-useless example showing how you can use more than one parameter:

MACRO DisplayThreeChars  Char1, Char2, Char3
    PUSH AX
    PUSH DX
    PUSH DX
    PUSH DX

    ; Use INT 21h, Service 2 to display each character:
    MOV DL, Char1
    MOV AH, 2
    INT 21h

    POP DX
    MOV DL, Char2
    INT 21h

    POP DX
    MOV DL, Char3
    INT 21h

    POP DX
    POP AX
ENDM

(We went through some contortions there which might seem unnecessary. We'll look at this a little later.)

There are two ways to use multiple-parameter macros: with commas or without. Most people do this:

    DisplayThreeChars 'A', 'B', 'C'

But you can also omit the commas, like this:

    DisplayThreeChars 'A' 'B' 'C'

Unfortunately, this leads to a problem. Look at this example:

    DisplayThreeChars [BYTE DS:SI + 3], 'B', 'C'

The "[BYTE DS:SI + 3]" is legal; it refers to the byte at the address 3 bytes past the current DS:SI pointer. But when Turbo Assembler sees this line, it thinks "[BYTE" is the first parameter, "DS:SI" is the second parameter, and "+" is the third parameter. It thinks the spaces are separators. How do we solve this problem? You can use angle brackets (less-than and greater-than signs) to enclose a parameter that contains spaces, like this:

    DisplayThreeChars <[BYTE DS:SI + 3]>, 'B', 'C'

This is ugly, but we'll have to live with it. If you forget the angle brackets, the assembler will give an error message.

Here's another point to watch out for when writing macros. Look at the following "DoSomething" macro:

    DATASEG

SomeNumber                        DW   0
AnotherNumber                     DW   123

MACRO DoSomething  MyParameter
    MOV [SomeNumber], MyParameter
ENDM

This macro is perfectly legal; we just have to be careful about our parameters. For example, it works great when used like this...

    DoSomething AX

...because it gets translated into...

    MOV [SomeNumber], AX

...which is okay because SomeNumber is word-sized, and a word-sized register can be copied to a word-sized variable.

But if we did this...

    DoSomething [AnotherNumber]

...it would get translated to...

     MOV [SomeNumber], [AnotherNumber]

...which is not okay -- you can't copy one memory location to another. You have to use a register as an intermediate. Similarly, you must be careful about the types (sizes) of parameters. For instance...

    DoSomething CL

...would cause a problem because it would get translated to...

    MOV [SomeNumber], CL

...which is a problem because CL is byte-sized and SomeNumber is word-sized.

Basically, be careful with your parameters. If the assembler finds a mismatch, it will produce an error message with a line number, so you can determine where the problem is. Of course, you can always try to make your macros as foolproof as possible. For example, the DoSomething macro could be changed to:

MACRO DoSomething  MyParameter
    PUSH AX

    MOV AX, MyParameter
    MOV [SomeNumber], AX

    POP AX
ENDM

That solves the "MOV mem, mem" problem, but it is less efficient if a register is the parameter. And it still doesn't prevent the problem caused by using a byte-sized parameter, because it will still "clash" with the word-sized AX.

Remember how DisplayThreeChars looked like it had a lot of unnecessary pushing and popping? Let's look at a simpler case. The following example macro has a rather hard-to-find bug:

MACRO DisplayCharacter_NoGood  CharToDisplay
    PUSH AX
    PUSH DX

    MOV AH, 2
    MOV DL, CharToDisplay
    INT 21h

    POP DX
    POP AX
ENDM

It works fine, except when the parameter is AH. Why? Let's take a look. If we do this...

    DisplayCharacter_NoGood AH

...then it gets replaced with:

    PUSH AX
    PUSH DX

    MOV AH, 2
    MOV DL, AH
    INT 21h

    POP DX
    POP AX

Notice that AH is getting overwritten! AH gets set to 2, overwriting the data that we wished to display!

This particular problem is relatively easy to fix. Do "MOV DL, AH" first, and then do "MOV AH, 2" next. This is how the original DisplayCharacter macro was presented. Here it is again:

MACRO DisplayCharacter  CharToDisplay
    PUSH AX
    PUSH DX

    ; Use INT 21h, Service 2 to display a single character:
    MOV DL, CharToDisplay
    MOV AH, 2
    INT 21h

    POP DX
    POP AX
ENDM

If you look back to DisplayThreeChars, you'll see how the three "PUSH DX"'s are used. They save backup copies of DX in case DL is used as one or more of the parameters. Of course, if DL doesn't happen to be one of the parameters, then it's not as efficient as it could be.

Well, now you know all about macros. But don't forget about procedures! Macros are very convenient, if you're very careful about parameters, but macros are best for short sequences of code. Remember that every time a macro is used, the text of that macro gets expanded inline each time. Procedures are better for longer or more complex sections of code.

Mode 13h graphics, using assembler

In all the chapters so far, all the example programs have been very, very boring. Let's start doing some basic graphics programming! We'll use the Mode 13h graphics mode. (If you want a refresher on this topic, take a quick look back at Chapter 7.)

We'll need a way to set the video mode to Mode 13h. Should we use a procedure or a macro? I would choose the procedure option, because the mode won't be changed many times in a program, so the tiny, tiny amount of speed gained by using a macro isn't needed. For practice, however, let's try both:

PROC SetMode13h
    PUSH AX

    ; Use INT 10h, Service 0 to set the screen mode to Mode 13h:
    MOV AH, 0
    MOV AL, 13h
    INT 10h

    POP AX
    RET
ENDP

Or...

MACRO SetMode13h_Macro
    PUSH AX

    ; Use INT 10h, Service 0 to set the screen mode to Mode 13h:
    MOV AH, 0
    MOV AL, 13h
    INT 10h

    POP AX
ENDM

And to switch back to text mode, we'll do almost the same thing. I'll just show the procedure; it's easy to create a macro version.

PROC SetTextMode
    PUSH AX

    ; Use INT 10h, Service 0 to set the screen mode to text mode (Mode 3):
    MOV AH, 0
    MOV AL, 3
    INT 10h

    POP AX
    RET
ENDP

Now, let's use some equates for some important constants...

VideoSegment                      EQU  0A000h
Mode13h_ScreenWidth               EQU  320
Mode13h_ScreenHeight              EQU  200

You can rename these however you like. I put "Mode13h_" in front of the width and height constants because we might use other video modes in the future which have different screen widths and heights.

Now, the most important Mode 13h operation is setting a single pixel to a certain color. For our PutPixel routine, should we use a procedure or a macro? Actually, this is the perfect use for a macro. In a game, we might be drawing tens of thousands or even hundreds of thousands of pixels every second, so we can save a lot of time by using a macro. It's only a tiny amount of time that is wasted when a procedure is called, but when that time is multiplied by, say, a million or so, it really starts to add up. Of course, it wouldn't hurt to have a procedure version too.

Remember the formula for calculating the address of a pixel. The segment address for video memory is A000, and the offset for pixel (column, row) is calculated using "offset = (row * Mode13h_ScreenWidth) + column". Then, looking back to Chapter 8, we saw how this can be optimized to "offset = (row << 8) + (row << 6) + column". Of course, this assumes that Mode13h_ScreenWidth equals 320, but I'm willing to make this assumption for the extra speed gain.

So, our plan is to construct a pointer to the address of the pixel we want to modify. We'll use ES:DI for the pointer. ES will be set to VideoSegment (0A000h), and DI will be set to the offset that we calculate. Then we can write a byte, representing the color number, to the byte pointed to by ES:DI. Here's a macro version:

MACRO PutPixel  Column, Row, Color
    PUSH AX
    PUSH CX
    PUSH ES
    PUSH DI

    PUSH AX                            ; "Immediate backups"
    PUSH DI

    ; Let DI equal the offset of the pixel.  The formula is:
    ;  Offset = (Row << 8) + (Row << 6) + Column
    MOV AX, Row                        ; Let AX = Row parameter
    MOV DI, AX                         ; Also let DI = Row parameter
    MOV CL, 8
    SHL AX, CL                         ; Shift AX left by 8

    DEC CL
    DEC CL                             ; Now CL = 6
    SHL DI, CL                         ; Shift DI left by 6
    ADD AX, DI                         ; AX += DI

    POP DI                             ; Retrieve the backup of DI, in
                                       ;  case Column parameter is DI,
                                       ;  b/c DI has been overwritten above
    MOV DI, Column                     ; Let DI = Column parameter

    ADD DI, AX                         ; DI += AX

    ; Let ES equal the video segment:
    MOV AX, VideoSegment               ; (intermediate)
    MOV ES, AX                         ; Let ES = VideoSegment constant

    ; Now ES:DI points to the address of the pixel.  Place the byte-sized
    ;  color value at that address:
    POP AX                             ; Retrieve the backup of AX, in case
                                       ;  parameter Color is AL or AH.
    MOV AL, Color                      ; Let AL = Color parameter
    MOV [ES:DI], AL                    ; Store AL at ES:DI

    POP DI
    POP ES
    POP CX
    POP AX
ENDM

Admittedly, that's a lot of code just to draw one pixel!

A ReadPixel routine would be very similar, except that it would need to return a byte-sized value, for the pixel color. If you wish, you can try writing a ReadPixel macro or procedure yourself. I'll write a version in a later tutorial if a ReadPixel routine becomes necessary.

So, let's tie our routines together in a short example program. This will help demonstrate equates and macros. Type in, assemble and link, and run this program:

------- TEST13.ASM begins -------

%TITLE "Assembler Test Program 13 -- Pattern-Drawing Mode 13h Graphics Demo"

    IDEAL

    MODEL small
    STACK 256
    LOCALS
    
    DATASEG

VideoSegment                      EQU  0A000h
Mode13h_ScreenWidth               EQU  320
Mode13h_ScreenHeight              EQU  200

StartColor                        EQU  15

CurrentColor                      DB   StartColor

x_Increment                       DW   1
y_Increment                       DW   1

LeftBoundary                      EQU  0
RightBoundary                     EQU  Mode13h_ScreenWidth - 1
TopBoundary                       EQU  0
BottomBoundary                    EQU  Mode13h_ScreenHeight - 1

    CODESEG

; -------------------------------------------------------------------------
; MACRO PutPixel
; -------------------------------------------------------------------------
; Desc: Plots a pixel at (Column, Row) on the Mode 13h screen, using the
;       color specified by Color.
;  Pre: Column and Row must be word-sized; Color must be byte-sized.
; Post: Assuming Row and Column are within range, the pixel is plotted.
;       No range checking is performed.
; -------------------------------------------------------------------------
MACRO PutPixel  Column, Row, Color
    PUSH AX
    PUSH CX
    PUSH ES
    PUSH DI

    PUSH AX                            ; "Immediate backups"
    PUSH DI

    ; Let DI equal the offset of the pixel.  The formula is:
    ;  Offset = (Row << 8) + (Row << 6) + Column
    MOV AX, Row                        ; Let AX = Row parameter
    MOV DI, AX                         ; Also let DI = Row parameter
    MOV CL, 8
    SHL AX, CL                         ; Shift AX left by 8

    DEC CL
    DEC CL                             ; Now CL = 6
    SHL DI, CL                         ; Shift DI left by 6
    ADD AX, DI                         ; AX += DI

    POP DI                             ; Retrieve the backup of DI, in
                                       ;  case Column parameter is DI,
                                       ;  b/c DI has been overwritten above
    MOV DI, Column                     ; Let DI = Column parameter

    ADD DI, AX                         ; DI += AX

    ; Let ES equal the video segment:
    MOV AX, VideoSegment               ; (intermediate)
    MOV ES, AX                         ; Let ES = VideoSegment constant

    ; Now ES:DI points to the address of the pixel.  Place the byte-sized
    ;  color value at that address:
    POP AX                             ; Retrieve the backup of AX, in case
                                       ;  parameter Color is AL or AH.
    MOV AL, Color                      ; Let AL = Color parameter
    MOV [ES:DI], AL                    ; Store AL at ES:DI

    POP DI
    POP ES
    POP CX
    POP AX
ENDM
; --------------------------------------------------------------------------


Start:
    ; Make data segment variables addressable:
    MOV AX, @data
    MOV DS, AX

    CALL SetMode13h

    XOR BX, BX                          ; X coordinate; set it to 0
    XOR DX, DX                          ; Y coordinate; set it to 0

PatternDrawLoop:
    ; Draw a pixel at the current position:
    PutPixel BX, DX, [CurrentColor]

    ; Update coordinates and color:
    MOV AX, [x_Increment]
    ADD BX, AX                         ; Update X coordinate (BX)
    MOV AX, [y_Increment]
    ADD DX, AX                         ; Update Y coordinate (DX)
    INC [CurrentColor]                 ; Update color...
    AND [CurrentColor], 0Fh            ; ...but restrict to 0..15 dec range

    ; Have we touched one of the edges of the screen?
    CMP BX, LeftBoundary
    JNE @@Bypass1                      ; Reversed condition for long jump

    ; Left boundary was hit.  Reverse the horizontal direction:
    MOV [x_Increment], 1
    
@@Bypass1:
    CMP BX, RightBoundary
    JNE @@Bypass2

    ; Right boundary was hit.  Reverse horizontal direction:
    MOV [x_Increment], -1

@@Bypass2:
    CMP DX, TopBoundary
    JNE @@Bypass3

    ; Top boundary was hit.  Reverse vertical direction:
    MOV [y_Increment], 1

@@Bypass3:
    CMP DX, BottomBoundary
    JNE @@Bypass4

    ; Bottom boundary was hit.  Reverse vertical direction:
    MOV [y_Increment], -1

@@Bypass4:
    ; Check: was a key pressed?  Use INT 21h, Service 0Bh:
    MOV AH, 0Bh
    INT 21h                            ; If key is in keyboard buffer, AL
                                       ;  will equal 0FFh; else AL == 0
    CMP AL, 0FFh
    JE @@Finished

    JMP PatternDrawLoop                ; Go back and draw the next pixel


@@Finished:
    ; Read in the key that was pressed.  Use INT 21h, Service 7:
    MOV AH, 7
    INT 21h                            ; Ignore character that was placed
                                       ;  in AL

    CALL SetTextMode

    ; Terminate program:
    MOV AX, 04C00h
    INT 21h

; --------------------------------------------------------------------------

PROC SetMode13h
    PUSH AX

    ; Use INT 10h, Service 0 to set the screen mode to Mode 13h:
    MOV AH, 0
    MOV AL, 13h
    INT 10h

    POP AX
    RET
ENDP


PROC SetTextMode
    PUSH AX

    ; Use INT 10h, Service 0 to set the screen mode to text mode (Mode 3):
    MOV AH, 0
    MOV AL, 3
    INT 10h

    POP AX
    RET
ENDP

END

------- TEST13.ASM ends -------

This program does the job, although it was a little slower than I expected. Let's take a second to think of optimizations. Note that AX, CX, ES, and DI are all saved and restored unnecessarily. In this program, it would be okay to comment out the four pushes and four pops (but not the "immediate backups"), because the main program doesn't rely on those registers. But I wouldn't then use this modified PutPixel macro in another program, because the other program might rely on those registers.

Likewise, ES is getting set to the VideoSegment constant for each and every pixel drawn. In this program, ES isn't used anywhere else, so we could set ES once and leave it. More complex programs would probably need ES though.

And similarly, DI, the offset address of the pixel, is re-calculated from scratch for each pixel. In this program, we could set scrap the entire PutPixel routine: we could just set ES:DI once, and then to move one pixel to the right, we could increment DI; to move one row down, we could add Mode13h_ScreenWidth to DI, and so on.

I don't immediately see an optimization for all the comparisons in the main loop. I'd hope there is a clever way to speed this up. Also in the main loop, we might try using unused registers instead of variables for CurrentColor, x_Increment, and y_Increment. And so on.

If you want some practice, try writing some macros or procedures to draw graphics primitives. Start with a macro or procedure to draw a horizontal line. Then try a vertical line. Then try a box. When you're feeling confident, you might try a circle routine or a slanted-line routine. Flip back to Chapters 8 and 9 for information on graphics primitives.

Summary

In this chapter, we've learned how to use equates and macros. We also began programming graphics in Mode 13h using assembler.

We're getting closer to the end of the assembler chapters! In the remaining few assembler chapters, we'll learn about the string instructions (which can be very helpful for graphics programming in Mode 13h), and we'll find out how to interface our assembler code with C or C++ code.

  

Copyright 1998, Kevin Matz, All Rights Reserved. Last revision date: Sat, Apr. 25, 1998.

[Go back]

A project