Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / assembler

How to Turn MacroAssembler into a High Level Language

4.44/5 (6 votes)
9 Sep 2021CPOL5 min read 6.1K  
Macros to help assembler programmers to improve source code
The power of high level languages resides on the capability of writing human readable syntax that the compiler translates to machine code. All these high level languages, such as C, VB, Python, ... include structured programming blocks. A structured programming block, is then, a block of instructions that are executed (or not) depending on conditions. MacroAssembler doesn't allow to program blocks such as IF .. THEN .. ELSE, WHILE .. WEND, REPEAT .. UNTIL or FOR ..NEXT, but with the definition of macros for these keywords, we can mimic a high level language sintax.

Introduction

Programmers that use MacroAssembler, often would like to have the same flexibility that high level languages offer in structured blocks such as "if .. then .. else .. endif", "while .. wend", "repeat .. until" or "for..next". Why has this class of syntax not been included in MacroAssembler? Probably because the power of the MacroAssembler language are the macros itself, so we can mimic the above mentioned blocks and write a more clear code.

I'll use VB naming convention because it is nearer to human readable syntax. (When you, as "C" programmer, reads "}" in your code, you have to go back in your source to know if it closes a "if", "else", "while" or "do" block.)

As said, all these blocks are executed (or not) on conditions. A single condition can be read as the result of Operand1 Operator Operand2 where Operand1 and Operand2 are variables or constant values in your code and operator is an arithmetic one such as =, >, <, >=, <=, ...

So, a high level language can process a syntax such as:

VB.NET
IF age > 50 THEN    'Age is Operand1, > is the operator and 50 is Operand2
    'Actions
ENDIF

-or said in another way-

VB.NET
IF Condition THEN
    'Actions
ENDIF

In this case, if we think about what a VB program does when it finds this block in the code is:

  1. Evaluate the condition.
  2. If result is true, then process all the actions existing from 'THEN' up to 'ENDIF' keywords.

A little more elaborated block could be:

VB.NET
IF Condition THEN
    'Actions_when_true
ELSE
    'Actions_when_false
ENDIF

In this case, the process is:

  1. Evaluate the condition
  2. If result is true, then process all the actions from 'THEN' to 'ELSE' labels, else process all the actions from 'ELSE' to the 'ENDIF' labels

Before going deeper into the creation of structured macros, we have to take into account that MacroAssembler doesn't have the syntax of arithmetic operators. In their place, pnemonics are used. Let's say: E (Equal), L (Less), LE (Less or Equal), B (Below), BE (Below or Equal), G (Greater), GE (Greater or Equal), A (Above), AE (Above or Equal)

or their contraries:

NE (No equal), NL (No less), NLE (No less equal), NB (no below), NBE (no below equal), NG (no greater), NGE (no greater equal), NA (no above), NAE (no Above equal).

What Is a Macro?

For those who use variants of the 'C' language, a macro is the equivalent to #define a result based on parameters. When MacroAssembler finds in your code a macro that has previously been defined, replaces the parameters and expands the macro.

Once arrived at this point, we can write (and study) our first very basic macro with the following syntax: (Text between [] means optional from so on):

ASM
;===================================================
;$iif    op1, oper, op2, label1 [,label2] 
;===================================================
$iif    macro   op1, oper, op2, label1, label2
         cmp     &op1, &op2
         j&oper  &label1
         ifnb    <&label2>  ;if label2 is no blank, 
                 (the 'label2' parameter exists) then MacroAssembler assembles then next line
                 jmp &label2
         endif
         endm 

If we place the following text in our code ...

ASM
$iif    ax,e,5,IsFive

...MacroAssemble will expand the macro to ...

ASM
cmp     ax, 5
je      IsFive

if we place the following text in our code ...

ASM
$iif    ax,e,5,IsFive,NoIsFive

...MacroAssemble will expand the macro to ...

ASM
cmp     ax, 5
je      IsFive
jmp     NoIsFive

So, by the use of this little macro, we can reduce all the source code in comparisons from two or three lines to only one, making our code very much readable.

The next thing we can study are the high level language Iterating blocks:

  1. VB.NET
    Repeat
        'Actions
    Until Condition (is true)
    

    In this case, what a compiler does when it finds the 'Repeat' tag is to save the address of the first action to do. When it finds the 'Until' tag, the program has to evaluate the condition. If condition evaluates to false, then the program has to jump to the address of the first action in the block saved before (else the work has done).

  2. VB.NET
    While Condition (is true) 
            'Actions 
        Wend

    In this case, when the compiler finds the 'While' tag, it saves the starting position of the actions. The runtime evaluates 'condition'. If it's true, the execution will continue in the first action if the condition evaluates to false, then execution will continue after 'Wend' tag.

  3. VB.NET
    For variable,Initial_Value,Final_Value,Step 
            'Actions 
        Next

    This is a bit complex. The function is to execute 'Actions' with 'variable' values from 'Initial_Value' to 'Final_Value', increasing or decreasing 'variable' in each iteration by 'Step'. When 'variable' exceeds 'final_value', the iteration is finished.

First, the runtime assigns 'Initial_Value' to 'variable'. Second evaluates if 'variable' exceeds 'Final_Value'. If Final_Value is not exceeded, then all the actions are executed, variable is increased or decreased and the process continues at point 2. If final_value is exceeded, then the iteration is finished.

Putting All Together for MacroAssembler

MacroAssembler assembles top-down in your code, as all high level languages. That means that when assembler tries to assembly an instruction (or macro) it knows where is this instruction in your code (its address). First of all, we have a counter of symbolic places in the code. Each place name for macroassemble will be $sim_ plus the counter value.

As iterating blocks require to know where they start and/or end to allow jumps to this places, some internal macros are provided:

  • $pushaddr: When found, it increases the counter of symbolic names and generates a $sim_counter name to identify the start of a block equal to the place where the macro has been found.
  • $popaddr: When found, it recovers the $sim_counter value as $jmp variable and decreases the counter of symbolic names.
  • $makenops: Allocates a 3 byte code for a jmp 'xxxx' that $filljmp will set later
  • $filljmp: Fills the preallocated 3 bytes space with a jmp 'xxxx'.

NOTE: These macros were created for MacroAssembler 5.1. In assembling code for flat memory, perhaps you should add more nops in $makenops to allow offsets in code to be greater than a signed word. To do so, you have to write a little program such as:

ASM
 jmp    veryfar 
 db    100000 dup(0) 
veryfar:

... when assembled, you can see how many bytes "jmp veryfar" uses to store the instruction. This number of bytes are the nops you have to reserve in $makenops. Remember to revise the keyword 'near' in the code provided if needed.

Explanations of every macro are detailed in the included text. This macros can be nested, that means that from now on, you can write MacroAssembler code as:

ASM
$IF value,le,100
$THEN
    ;actions
$ELSE
    ;actions
$ENDIF

$FOR    x,0,1366-1,1
    $FOR y,0,768-1,1
        ;GetPixel(x,y)
        ;some actions more
    $NEXT
$NEXT

$REPEAT
    ;Get Keyboard key
$UNTIL key,e,Escape

$WHILE value,l,100
    ;some actions more
    ;value = calculated_value
$WEND

Using the Code

Just include the text provided at the beginning of your code or #include it.

Code

ASM
        .XLIST
        .XCREF

; STRUCTURED PROGRAMMING MACROS ======================================================
;-------------------------------------------------------------------------------------
; internals
;-------------------------------------------------------------------------------------
        $simcount = 0   ;Counter used to generate symbolic names along the program as $sim_1, &sim_2 ...     

$pushaddr  macro
        $simcount = $simcount + 1    ; increase symbol count
        $newsim    %$simcount        ; creates new sim name
        endm

$newsim macro $n
        $sim_&$n = this near ;generates symbolic address. I.E. $sim_100 = place into code where $newsim was expanded, I.E.= 506
        endm

$popaddr  macro
        $getsim %$simcount
        $simcount = $simcount - 1   ; decrease symbol count
        endm

$getsim macro $n
        ifndef $sim_&$n
               %out     fail in structure !!!
        endif
        $jmp = $sim_&$n            
        endm

$makenops macro
        nop    ;allocate space to place a jmp when other macros will be processed
        nop    ;a jmp instructions use 1 byte for the coding of the instruction itself plus 2 bytes more
        nop    ;that are treated as a signed word (offset potitive or negative from position of the instruction)
        endm

$makejump macro towhere
        here = this near ;we save or position in the source code
        $popaddr         ;recover address of last 3 nops
        org $jmp         ;we go there ...
        jmp &towhere    ;... and replace the three nops with "jmp towhere"
        org here         ;we come back to our position in the source code
        endm

;-------------------------------------------------------------------------------------
; $iif    op1, oper, op2, label1 [,label2]
;-------------------------------------------------------------------------------------
$iif    macro op1, oper, op2, label1, label2
        cmp     &op1, &op2
        j&cond  &label1
        ifnb <&label2>        ;if label2 was passed as argument, then is expanded
                jmp &label2
        endif
        endm

;=====================================================================================
; Sintax:
; $IF op1, oper ,op2
; [$THEN] 
;    block1
; [$ELSE]
;    block2
; $ENDIF
;=====================================================================================
$IF     macro   op1, oper, op2
        local   block1
        $iif    <op1>, oper, <op2>, block1
        $pushaddr
        $makenops   ;nops will be filled by $ELSE or by $ENDIF with a jmp tosomewhere when condition evaluates false
block1:             ;this is the start of block of instructions executed when condition evaluates to true
        endm

;-------------------------------------------------------------------------------------
$THEN   macro
        endm        ;macro formal. $THEN is optional and used to produce only a better readability of the source code

;-------------------------------------------------------------------------------------
$ELSE   macro
        local   block2
        $makejump block2 ;as we use $ELSE macro, the $IF nops has to be replaced by a jmp to the start of the $ELSE block
        $pushaddr   ;also, we have to reserve space for the jmp out the $IF
        $makenops
block2:             ;this is the start of block of instructions executed when the $IF condition evaluates to false
        endm

;-------------------------------------------------------------------------------------
$ENDIF  macro
        local   exitif
    $makejump exitif ;if $ELSE is not used then the $IF nops are replaced with "jmp exitif"
exitif:              ;if $ELSE has been used then the $ELSE nops are replaced with "jmp exitif"
        endm

;=====================================================================================
; Sintax:
; $WHILE op1, oper, op2
;        ...         ;Actions executed if condition evaluates true
; $WEND
;=====================================================================================
$WHILE  macro  op1, oper, op2
    local istrue
        $pushaddr           ;address of start of loop
        $iif   <op1>, oper, <op2>, istrue
        $pushaddr           ;this is the address reached when condition is false
        $makenops           ;reserve nops to be filled with 'jmp outofloop'
istrue:
        endm

$WEND   macro
    local quitloop
        $makejump quitloop
        $popaddr        ;recover address of the start of loop
        jmp     $jmp        ;goto there
quitloop:
        endm
  
;=====================================================================================
; Sintax:
; $REPEAT
;     ...            ;Actions executed until condition evaluates true
; $UNTIL op1, oper, op2
;=====================================================================================
$REPEAT macro
        $pushaddr           ;address of start of loop
        endm

$UNTIL  macro   op1, oper, op2
    local    quitloop
        $iif   <op1>, oper, <op2>, quitloop
        $popaddr        ;recover address of the start of loop
        jmp     $jmp        ;goto there
quitloop:
        endm

;=====================================================================================
; Syntax:
; $FOR index,initial_value,final_value,step_value,[register]
;     ...
; $NEXT | $LOOP
; register has to be used if both index and initial_value are variables
;=====================================================================================
$FOR    macro   index,initial_value,final_value,step,register
    local compare, inrange
        ;set the initial value to index
        ifnb    <&register>
                mov     &register, &initial_value    
                mov     &index, &register
        else
                mov     &index, &initial_value
        endif
        jmp short compare

        $pushaddr           ;address of start of loop
        if step eq 1
              inc     index
        else
              if step eq -1
                   dec index
              else
                   add index, step
              endif
        endif
compare:
        ifnb <&register>
                mov     &register, &final_value
                cmp     &index, &register
        else
                cmp     &index, &final_value
        endif
        if      step gt 0
                jle  inrange    ;goto block if less or equal
        else
                jge  inrange    ;goto block if greater or equal
        endif
        $pushaddr            ;here = address of out of range
        $makenops        ;we will fill nops with "jmp quitloop"
inrange:
        endm

$NEXT   macro
        local    quitloop
        $makejump quitloop
        $popaddr        ;recover address of the start of loop
        jmp     $jmp        ;goto there
quitloop:
        endm

$LOOP   equ <$NEXT>
;----------------------------------------------------------------------

        .CREF
        .LIST

Points of Interest

After decompiling "C" code snippet, I was able to determine what a compiler does, so the macros mimic the same process.

History

These macros haven't ever been modified.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)