Classic Computer Magazine Archive A.N.A.L.O.G. ISSUE 79 / DECEMBER 1989 / PAGE 12

BY MATTHEW J. W. RATCLIFF

ASSEMBLER

/EDITOR REFERENCE, PART II


Debugging
    Your assembly language programs are bound to have some bugs. Luckily, Asm/Ed provides a method for testing assembled object code. When at the Asm/Ed EDIT prompt, type BUG and press Return. You'll be presented with the DEBUG prompt.
    The debugger uses short one-or two-letter commands, some followed by an optional hexadecimal address. Below is a complete list of the commands.
    X-Exit from the debugger.
    DR-Display the contents of the 6502 registers:

DR
    A=00 X=10 Y=20 P=B0 S=DF

    A is the accumulator, X and Y are the index registers, P is the processor status register (which includes the carry flag, zero flag, etc.) and S is the stack pointer.
    CR-Change the contents of any of the 6502 registers.

        CR<,1,2,,DE

    The specified values are stored in the registers in the same order the registers are displayed by the DR command. In the above example, the accumulator is unchanged, the X register receives a 1, the Y register a 2, the status register remains unchanged and the stack pointer is adjusted to DE.
    D-Display memory.
    "D3000,0" displays memory location $3000. When the second parameter is less than or equal to the first, only one location is shown. "D3000,3010" displays memory from locations $3000 through $3010. Enter D by itself, and the next eight locations (in this case, $3011 through $3018) will be displayed. If only the second parameter is omitted, a default of eight memory locations is displayed:

D3000
3000  10 40 28 22 14 11 12 FE

    Note that the output of the debugger is always in hexadecimal. All input addresses and register values must be specified in hex as well.
    C-Change memory.

      C3034<21,23,,2E

    The command itself is immediately followed by the starting hexadecimal address to change. The values to be placed in memory, starting at the first location, are separated by commas. Two consecutive commas tell the debugger to skip over that memory location, leaving it unchanged. In the above example, memory location $3034 receives $21, $3035 receives $23 and $3037 gets $2E.
    M-Move memory.

       M0600<0700,0800

    The above tells the debugger to move memory from locations $0700 through $0800 to memory beginning at location $0600. The destination address ($0600 in this case) must be less than the first source address ($0700) or greater than the ending source address ($0800). If the source and destination areas of memory overlap, you may get unexpected results.
    V-Compare two blocks of memory (verify).
    You might, for example, use this to compare two slightly different versions of the same program to see where something has changed.

        V7000<7100,7123

    The above command tells the debugger to compare memory from $7100 through $7123 to memory at $7000. Any memory locations that do not match are shown side by side:

        V7000<7100,7123
        7101 00 7001 21

    In the above comparison, all memory from $7100 through $7123 matched memory from $7000 through $7023, except at one location. Memory location $7101 contained a $0, while $7001 contained a $21.

IF YOU DO NOT KNOW THE
ABSOLUTE ADDRESS OF A
NEEDED LABEL, THEN YOU
SHOULD GO BACK TO THE
SOURCE CODE, MAKE THE
CHANGES THERE AND
REASSEMBLE.


    L-List memory with disassembly.
    The "L" command is one of Asm/Ed's most powerful. It can be used to disassemble your operating system ROM (beginning at $0000) to see what some of the routines look like. It can be used to disassemble object files loaded into memory to see how they work. Here are some examples:
    L7000-List memory with disassembly (as many lines that will fit on the screen) beginning at $7000.
    L-List memory with disassembly starting at the next location (picking up where the previous L command left off).
    L7000,0 or L7000,7000-List and disassemble $7000 only.
    L2300,2400-List and disassemble memory from $2300 through $2400.
    When the debugger comes across data that cannot be disassembled (such as data tables or strings, for example), it will print a series of question marks. Otherwise, the data is shown in hexadecimal, as well as in its equivalent assembly mnemonic form:

   L50000,00 A9 8A LDA #$8A

    A-Assemble a single instruction.
    This comes in handy when you want to test a small patch to a program. Simply type A and press return to get into the singleline assembly mode. You must first specify an address, followed by a less-than character (<) and the assembly instruction. To assemble to successive memory locations, subsequent entries require only the less-than character followed by the assembly instructions. For example:

SOONER OR LATER, YOU'LL
GET TIRED OF USR ROUTINES
(MOSTLY BECAUSE THEY
ARE SO DIFFICULT TO DEBUG).

      A
      5001<LDY $1234
      5001 AC3412
      <INY
      5004 CB

    In the above example, we have assembled LDY $1234 and INY into consecutive memory, starting at $5001. Note that here your assembly instructions must use the dollar sign to indicate hexadecimal. Press Return on an empty line to exit the mini-assembler. You cannot refer to labels in the program, since the debugger doesn't keep track of them. If you do not know the absolute address of a needed label, then you should go back to the source code, make the changes there and reassemble.
    G-Execute instructions beginning at a particular address.
    Type the letter "G" followed by the first execution address. The program will continue to run until the system crashes, you press the break key, or a BRK (break) instruction is executed.
    T-TRACE.
    Type the letter "T" followed by the address at which to begin execution. The instruction will be executed, immediately followed by a dump of the instruction (list a single line with disassembly) and the CPU registers. This continues until a BRK instruction is executed or you press the break key.
    S-Step singly through instructions.
    Sometimes you need to test a single instruction at a time. The debugger's step command is used for this task. Enter "S" followed by the address to begin execution. The effects are the same as the TRACE command, except the debugger stops execution after each assembly instruction. Type "S" and Return repeatedly to continue single-stepping through the program.
    X-Exit the debugger and return to Asm/Ed's editor.

Error Codes
    The error codes between 128 and 255 are the same as those in your Atari BASIC reference manual. These are generally input/output errors associated with CIO (central input/output) utility operations, the heart of your Atari's operating system. There are 19 other error codes that you may encounter while assembling or debugging your programs:
    1-The memory available is insufficient for the program to be assembled.
    2-For the command "DEL xx,yy," the line number xx cannot be found.
    3-There is an error in specifying an address (mini-assembler).
    4-The file named cannot be loaded (wrong file format).
    5-Undefined label reference (you probably misspelled a label in your program).
    6-Error in syntax of a statement (missing operand or misspelled assembly mnemonic).
    7-Label defined more than once.
    8-Buffer overflow. (I'm not certain what this means.)
    9-There is no label or"*" before "=". (An equals sign was found in the first field of a line of code. All equals signs must be preceded by either a valid label or an asterisk.)
    10-The value of an expression is greater than 255 where only one byte was required. (e.g., LDA #LABEL, where label is an address of some memory location greater than 255).
    11-A null string has been used where invalid.
    12 The address or address type specified is incorrect (e.g., LDA (PGZRO),Y would result in this assembly error if the label PGZRO was not an address of a memory location less than 256).
    13-Phase error. An inconsistent result has been found from pass 1 to pass 2 (e.g., two bytes were reserved for some label on the first pass, but on the second pass only one byte was needed. This is avoided by minimizing forward references and by defining all known labels at the top of the file before any assembly code. You will get this error a lot as you learn the language).
    14-Undefined forward reference (e.g., misspelled label or reference to a label not defined).
    15-Line is too large.
    16-Assembler does not recognize the source statement.
    17-The line number is too large (32767 is maximum).
    18-LOMEM command was attempted after other command(s) or instruction(s). LOMEM, if used, must be the first command after entering the Asm/Ed editor.
    19-There is no starting address (e.g., you forgot the directive at the top of your program).

IF YOU DIDN'T PAY MUCH ATTENTION
TO ANALOG'S "MASTER MEMORY
MAP" SERIES, I STRONGLY RECOMMEND
THAT YOU GO BACK AND READ IT.


Expressions
    The assembler can perform many useful computations for you. The operators recognized and the operations they perform are as follows:

    + Addition
    - Subtraction
    * Multiplication
    / Division
    & Logical AND

    Expressions may not contain parentheses, and they are always evaluated left to right. (There is no precedence placed on operators.) Some examples follow:

  100 STORAGE = $4000
  110 *= STORAGE + $10
  200 JMP START+20
  300 LDA t#STORAGE&$0FF
  310 LDX #STORAGE/$100
  320 LDA #3*15

USR Routines
    The USR command of Atari BASIC allows you to call assembly language routines. These routines can perform special functions to vastly improve the performance of BASIC. For example, assembly USR routines may be implemented for Player/Missile graphics movement, sort algorithms or high-speed disk I/O functions.
    Assembly code won't normally be loaded as part of your BASIC program. It must be loaded using a routine in BASIC, placing the data values into strings or POKEing it into safe RAM, for example. You may place up to 256 bytes of assembly code into Page 6 (beginning at memory location 1536). If you do not use the cassette (C:), up to 128 bytes of code can go into Page 4 (beginning at memory location 1024), the cassette buffer. If your code is "position independent" (relocatable), it may be loaded into a BASIC string.
    What is position-independent assembly code? Such a program may have no JMP or JSR instructions (with the exception of JSR's to ROM addresses that are guaranteed not to move). So how do you implement loops? Use branch instructions. If your code gets much larger than 256 bytes, writing position-independent code can be difficult. The largest routine I've ever written of this type was 410 bytes long. You may also "relocate" your code. This requires a foreknowledge of all the JMP and JSR instructions in your code. You may then load the object code into a string, determine its starting address, and then POKE adjusted address values in for all the JMP and JSR instructions. This is no small task and is seldom used. Generally, your USR routines will be fairly small and can be written in a position-independent manner.
    The format of a BASIC USR command is:

A=USR(ADR,PARAM1,PARAM2,PARAM3)

    The first parameter, ADR, is the starting address of the assembly code you wish to execute. The values following are parameters that are passed to the assembly code on the system stack after being converted to integers. The variable A takes on an integer from memory locations $D4 and $D5 (low byte, high byte). This is how you return a value to BASIC.
    Let's write a USR routine to add two integers and return the result. Our BASIC program might look like this:

10 TRAP 1000
20 OPEN #1,4,0,"D:MYUSR.OBJ":
REM Our USR Code in a file
30 TRAP 70
40 FOR I=1 TO 6:GET #1,A:NEXT
 I:REM Ignore 6-byte load hea
 der of file
50 I=1536:REM USR routine was
 assembled for Page 6
60 GET #1,A:POKE I,A:I=I+1:GO
TO 60:REM End of file error w
ill terminate our entry of th
e program
70 CLOSE #1
80 PRINT"INPUT NUMBER 1 ";:IN
PUT N1
90 IF N1<0 OR N1>65535 THEN ?
 OUT OFRANGE":GOTO 80
100 PRINT "INPUT NUMBER 2 ";:
INPUT N2
11O IF N2<0OR N2>65535-N1 THE
N ? "OUT OF RANGE":GOTO 100
120 SUM = USR(15361, N1, N2 )
130 PRINT "NUMBER ";N1;" PLUS
 ";N2;" EQUALS ";SUM
140 END
1000 PRINT "COULD NOT FIND US
R ROUTINE FILE"
1010 PRINT "MYUSR.OBJ"
1020 END

    Now we need to write an assembly language program with Asm/Ed that implements this USR routine. It will accept parameters Nl and N2 off the stack (two two-byte integers), add them, and return the result to SUM through memory locations $D4 and $D5. Our code might appear as shown in Listing 1.
    Enter this program with Asm/Ed and execute the instructions in the first two comment lines. When you get an assembly with no errors, your file D:MYUSR.OBJ should be ready to test with the BASIC program.
    Work at this until it performs as expected. As you become more adept at writing USR routines, you may wish to develop utilities for converting OBJ files into a series of BASIC DATA statements, so you can simply READ and POKE them without using messy file I/O to initialize the USR routine. It takes a relatively long time to install USR routines by poking them into memory or strings, but once in place, they execute amazingly fast.
    You will find that USR routines are difficult to debug since you need to initialize and call them from BASIC. If you mess up the stack or some other operation, the computer usually crashes inexplicably. It isn't easy to debug USR routines from DEBUG, because you will have to write sophisticated test routines to stuff all sorts of test values on the stack.

Stand-Alone Assembly
    Sooner or later, you'll get tired of USR routines (mostly because they are so difficult to debug). When you do, it is time to take the plunge into writing a stand-alone assembly language program. Then you will get into the complexities of keyboard input, screen output, disk I/O and printer output from the Asm/Ed environment. Complete libraries of routines, such as a "graphics package" that performs the equivalent of BASIC's GRAPHICS, COLOR, PLOT and DRAWTO, will become a necessity. This is where ANALOG'S Boot Camp series will help the most. In the months to come you will learn everything from keyboard input to floating-point processing, all from the assembly language level.
    As an example of a stand-alone assembly language program, and an illustration of its raw speed, we present the following demonstration. First, type this BASIC program and run it. While it executes (it will take about 12 minutes), read the remainder of this article to see how the same functions can be performed in assembly language:

10 DINDEX=88:REM Screen RAM p
ointer
20 SCREEN=PEEK(DINDEX)+256*PE
EK(DINDEX+1)
30 FOR X=0 TO 255
40 A=X
50 FOR Y=0 TO 255
60 POKE SCREEN+Y,A
70 NEXT Y
80 NEXT X

    At location DINDEX is a two-byte "pointer." Memory locations 88 and 89 hold the address of the beginning of screen RAM. The equation in line 20 calculates the variable SCREEN, which we use as a direct pointer for the POKE in line 60. In our assembly language equivalent of the above program, this problem is even easier to solve. (This is seldom the case, however; most things are harder to do in assembly language. This demonstration is designed specifically to show the strengths and speed of assembly language.)
    Next, two loops are set up. The inner Y loop is used to POKE the current value of X into the first 256 screen RAM locations. You will see these characters fill the top portion of your display. All ATASCII values from zero through 255 are POKEd, with the help of the X loop. The variable A was used simply for a more symmetrical comparison with the assembly code to follow.
    Let this BASIC program run to completion. Time it carefully. When you finally get the READY prompt, reboot your computer with Asm/Ed and enter this equivalent assembly language program:

0 ;LIST#D:SCREEN.ASM
1 ;ASM,,#D:SCREEN.OBJ
2 *=$3400
3 RUNAD=$2E0
10 DINDEX = 88 ; Screen RAM
pointer
20 ; We don't have to compute
 SCREEN; we use post indexed
addressing
30 START LDX #0 ; Initialize
variables for loops
40 LDY #0
50 STORE TXA     ; Place scre
en character into A register
60 PUTIT STA (DINDEX),Y ; Pla
ce character on screen
70 INY           ; Next scree
n location
80 BNE PUTIT     ; Y register
 "wraps around" to zero after
 255
90 INX
100 BNE STORE    ; NEXT X
110 RTS          ; Return con
trol to DOS
120 *= RUNAD
130 .WORDSTART  ; So we can 1
oad and run from DOS

    Now execute the two commands in the first two comment lines at the top of the listing. If you get no assembly errors, you will have a file SCREEN.OBJ that is ready to load and run. Go to DOS and execute a binary load of the file SCREEN.OBJ. It will run immediately after loading and return control back to DOS after performing all 65,536 "POKES" of characters to screen memory. Did you catch it? You probably didn't if you blinked. This version of the program takes barely a second to run! If you want to watch the show for a while and exit to DOS when a key is pressed, for example, modify your program as follows:

15 CH = 764   ; Keyboard buf
fer
101 LDA #255
102 CMP CH    ; keypressed?
103 BEQ START ; Nope, loop
104 STA CH    ; Yes, clear o
ut key buffer and exit to DOS

    List this version to disk and reassemble it. When loaded from DOS, it will POKE all those ATASCII patterns to the screen continuously until you press a key on the keyboard. To randomize the show, make these changes:

16 RANDOM = 53770    ; Always
a random number here
50 STORE
60 PUTIT LDA RANDOM  ; Get a r
andom fill character
61 STA (DINDEX),Y    ; Place c
haracter on screen/PX

    Notice how I always added a meaningful label for each important memory location. Avoid the use of code, such as LDA 53770. The proper use of labels makes it much easier to see exactly what your program does and how it does it.
    If you didn't pay much attention to ANALOG's "Master Memory Map" series, I strongly recommend that you go back and read it. Even if you do not understand it all, you will learn a lot. A good memory map is the key to unleashing all the power of your computer. As a 6502 assembly language reference manual, I use 6502Assembly Language Programming by Leventhal. This is a general reference for the 6502 microprocessor and does not have any specifics on the Atari computer. It does detail all the 6502 assembly mnemonics and provides examples of multiply, divide and other useful routines.
    When you find that Asm/Ed is too slow to suit your tastes as you build larger and more sophisticated programs, consider upgrading to MAC/65 (sold by ICD). This macro assembler supports the use of INCLUDE files, allowing you to easily import "canned" routines that have already been debugged. Its MACRO capabilities allow you to define high-level constructs that vastly simplify the development of assembly programs. With a good MACRO library (such as the MAC/65 Toolkit from ICD or QuickCode from Stardust Software), your assembly source code will resemble BASIC or some other high-level language while retaining all the power and speed of pure assembly language. MAC/65 is the fastest native 6502 assembler I have ever used, bar none. (Mad Mac for the Atari ST will assemble 6502 code at a speed that blows the doors off MAC/65; but that's a whole new ball game.)
    Welcome to the fast and complicated world of assembly language programming. I hope this guide will inspire you to put that inexpensive Asm/Ed cartridge to work on all those fantastic ideas that the old faithful Atari BASIC could never handle.

Mathew


    Matthew J.W. Ratcliff   is an electrical engineer at McDonnell Aircraft in St. Louis, Missouri. An experienced assembly language, C and Ada programmer on IBM and main frame computers, he still enjoys developing new programs and articles for the 8 bit Atari home computer. He has been an Atari enthusiast since 1982.