User:Doruletz72/ONBA/Assembly Language

INTRODUCTION edit

In the followings we will consider a C simple example to have an idea how the memory is managed while an application is running.

   //
   // globals -> global segment (A5)
   //
   int intVar;
   long longVar;
   char charArray[20];
   //
   // Sum procedure -> code segment (PC)
   //
   int Sum(int op1, int op2)
   {
      int res; // local variable -> frame stack segment (A6)
      res = op1 + op2;
      return res;
   }
   //
   // Pilot Main function -> code segment (PC)
   //
   unsigned long PilotMain
     (unsigned int cmd, void *cmdPBP, unsigned int launchFlags)
   {
      int operand1, operand2, result; // local variables-> frame stack segment (A6)
      char string[8];                 // local variables-> frame stack segment (A6)
      StrCopy("abcdefgh", string);    // "abcdefgh", constant -> code segment (PC)
      operand1 = 1;
      operand2 = 10;
      result = Sum(operand1, operand2);
      return result;
   }

It is a good idea to read the section Running an application first, if don't read it yet. Also, if you are eager to test your skills you can start OnbA. Follow the instructions from STARTING OnbA.

Global segment edit

First will refer the global variables. As I said before, all variables outside of the procedures forms the globals. They are stored inside of the DATA record in the PRC file. When application is started, it creates a portion of memory called GLOBAL SEGMENT.

 
Onba01

The base address of this segment is saved into register A5. So if you want to use this register for other purpose save its value (push it on the stack), then restore it after use. As you can see in the picture the variables are stored in backward order as they are declared in code. To use a global variable you must specify a value of offset relative to A5, where is stored the base address. These values are always negative numbers. Address modes used to do this are indirect address using register with index or with displacement.
Unfortunately, the disassembler programs usually not disassembles the DATA segment. Under OnbA to declare global variables you must "instruct" Onba to switch in "data mode" its output, i.e. OnbA will write inside the DATA record of the PRC file.

   data ; instruct OnbA to switch to data mode
   intVar dc.w  0 
   longVar dc.l 0
   charArray dc.20 0

To access the global variables OnbA allow you to use two modes:

   ; easy way to access variables
   move.w intVar(a5), d0
   move.l longVar(a5), d0
   ; hard a bit, this is what you will see into disassembled source
   move.w -26(a5), d0
   move.l -24(a5), d0

The two modes are equals, because in fact OnbA will replace "numeric labels" with proper values into binary code. But when you disassemble the binary code there are no more "numeric labels", so you must understand this mode of global variables organization in memory.

Stack segment edit

The STACK SEGMENT it is a little bit complicated than GLOBAL SEGMENT. This segment is created based on size stored in CODE 0 record of the PRC file. It holds the local variables - the variables inside of the procedures - and the intermediate values such parameters to be passed to a procedure or a system function at the moment of the call, values of the register pushed to the stack when situation demands.

 
Onba01

SP (A7) register keeps the current top pointer of the program stack. When a procedure/system function is called, the parameters are push it on the program stack (stack grows), along with PC register value (instructions pointer) and the current A6 register value (frame stack pointer). After that, the execution of the program will be continued with effective call of the procedure: creating a local stack frame (A6 hold the base address of this stack) and running of the instructions inside of the procedures.
We can see in the picture the moment of PilotMain() initialization, when is created a local stack frame with the local variables. To access local variables of the PilotMain() we will proceed in the same way how we handled the GLOBAL SEGMENT. But now will use A6 register as reference:

   move.w #1, operand1(a6)  ; operand1 = 1
   move.w #10, operand2(a6) ; operand2 = 10
   ; or
   move.w #1, -2(a6)
   move.w #10, -4(a6)

Let see the next, the call of the OS system function StrCopy:

 
Onba01

The stack is increased, the parameters - string and "abcdefgh" addresses - , PC register value (the current instruction pointer), status registers SR and the values of the A6 register (containing the PilotMain frame stack value) are pushed on the stack. System function StrCopy is a black box for us, but mainly the things are going in the same way like to the procedures call (see bellow).
Part of this push process is not visible, but is done it by the TRAP instruction: it pushes PC, SR and A6, then it restores PC, SR and A6. And of course additionally stack management, too. SP contains now a new top stack value.

   pea string(a6)      ; push local variable <ea>, increases stack += 4
   pea stringParam(pc) ; push constant value <ea>, increases stack += 4
   trap #15             ; call OS API
   dc.w $A0C5           ; invoke StrCopy
   addq.l #8, sp        ; decreases stack -= 8
   ; or
   pea -14(a6)
   pea 48(pc)
   ...

After the call of StrCopy, the variable string contains value "abcdefgh". The value of PC, SR and A6 are restored and the stack is decreased. The SP contains now the old top stack value before calling of StrCopy. A6 register contains now the PilotMain frame stack reference so we can access the PilotMain local variable using A6 (see first picture).
The next C code lines initialize the values of the operand1 and operand2.

   move.w #1, operand1(a6)  ; operand1 = 1
   move.w #10, operand2(a6) ; operand2 = 10
   ; or
   move.w #1, -2(a6)
   move.w #10, -4(a6)

The values of Sum() parameters, operand1 and operand2 is pushed on the stack. The procedure Sum() is called.

   move.w operand2(a6), -(sp) ; push operand2 value on the stack, increases stack += 2
   move.w operand1(a6), -(sp) ; push operand1 value on the stack, increases stack += 2
   jsr Sum(pc)                ; jump to the procedure Sum()
   addq.l #4, sp              ; decreases stack -= 4 
   ; or
   move.w -4(a6), -(sp)
   move.w -2(a6), -(sp)
   jsr $ffc0(pc)
   ...
 
Onba01

At this moment a new frame stack is created for Sum() procedure using A6 as reference register. The local variable res can be access through A6. As you can see by now the register SP is used only for stack management. If you want use SP register for other purpose, create a variable or use another register to save it, because SP cannot be pushed by itself on stack and then used. If you do this you will lose any reference to the top of the stack. So save it before.
The assembly code of the Sum() procedures:

   ; OnbA source
   proc Sum(op1.w, op2.w)
   local res.w
   beginproc
      move.w op1(a6), d0 ; load op1 to D0 register
      add.w op2(a6), d0  ; add op2 to D0
      move.w d0, res(a6) ; res = op1 + op2
      move.w res(a6), d0 ; return res
   endproc
   ; or disassembled source
   ; this code start at address $ffc0 referred by PC
   link a6, #-2      ; save A6 on stack, allocate 2 bytes for local
   move.w 8(a6), d0  ; load op1 to D0 register
   add.w 10(a6), d0  ; add op2 to D0
   move.w d0, -2(a6) ; res = op1 + op2
   move.w -2(a6), d0 ; return res
   unlk a6           ; free local stack, restore A6
   rts               ; restore PC (jump back)

Inside the Sum() procedure, the parameters op1, op2 which are pushed on the stack, are referred using positive values, starting with offset 8. This "inversion" is normally because stack segment is backward addressed, from higher addresses to lower addresses. So when when "look in front", in fact you increase the negative absolute value, and when you try to "look back" you decrease the negative value. Because A6 is the reference, "inside" procedure means negative values, and before procedure means positive values. The offset of 8, between start of the procedure frame stack and its parameters, represents 4 bytes value of PC pushed by JSR instruction and 4 bytes value of old A6 pushed by LINK instruction when the procedure frame stack is created. The UNLK instruction restore the old A6 register value, and the RTS restore the PC old value. We will insist no more here. Just I hope you now understand a little bit more the STACK SEGMENT internals.

Code segment edit

By now you saw some references to PC register, which hold the pointer of the current instruction. Only one thing to discuss: the reference to "abcdefgh" constant. The local constant are the only written inside the CODE SEGMENT (in fact record). You can declare constant anywhere inside your program using <constant_name> dc.b "value" directive. OnbA will translate all string (array of chars) constants passed to functions/procedures after the end of current procedure body. You can do the same:

   procedure Example()
   beginproc
     ... ExampleStr(pc) ; reference of the constant
   endproc
   ExampleStr dc.b "aadsdaddsd"
   ;
   ;disassembled code
   ;
   link a6, #0
   ....
   unlk a6
   rts
   dc.b "aadsdaddsd"
   dc.w 0

"dc.w 0" means that OnbA automatically adds '\0' terminator string and an addtional '\0', if situation demands, to keep word alignment of code.
The entire equivalent OnbA source using all directives (easy way):

   prc "WikiExample"
   creator "Doru"
   ; put OnbA on data mode
   data
   intVar dc.w  0 
   longVar dc.l 0
   charArray dc.20 0
   ; put OnbA on code mode
   code
   ; some equ
   StrCopy equ $A0C5
   ;
   ; procedure Sum
   ;
   proc Sum(op1.w, op2.w)
   local res.w
   beginproc
      move.w op1(a6), d0
      add.w op2(a6), d0
      move.w d0, res(a6)
      move.w res(a6), d0
   endproc
   ;
   ; procedure PilotMain
   ;
   proc PilotMain_00(cmd.w, cmdPBP.l, launchFlags.w)
   local operand1.w
   local operand2.w
   local result.w
   local string.8
   beginproc
      systrap StrCopy(&stringValue(pc).l, &string(a6).l)
      move.w #1, operand1(a6)
      move.w #10, operand2(a6)
      call Sum(operand1(a6).w, operand2(a6).w)
      move.w d0, result(a6)
      move.w result(a6), d0
   endproc
   stringValue dc.b "abcdefgh"

The entire equivalent OnbA source without using of systrap and call directives (harder way):

   prc "WikiExample"
   creator "Doru"
   ; put OnbA on data mode
   data
   intVar dc.w  0 
   longVar dc.l 0
   charArray dc.20 0
   ; put OnbA on code mode
   code
   ;
   ; procedure Sum
   ;
   Sum
      link a6, #-2
      move.w 8(a6), d0
      move.w 10(a6), d0
      move.w d0, -2(a6)
      move.w -2(a6), d0
      unlk a6
      rts
   ;
   ; procedure PilotMain
   ;
   proc PilotMain_00(cmd.w, cmdPBP.l, launchFlags.w)
   local operand1.w
   local operand2.w
   local result .w
   local string.8
   beginproc
      pea string(a6)
      pea stringValue(pc)
      trap #15
      dc.w $A0C5 ; StrCopy
      addq.l #8, sp
      move.w #1, -2(a6)
      move.w #10, -4(a6)
      move.w -4(a6), -(sp)
      move.w -2(a6), -(sp)
      jsr Sum(pc)
      addq.l #4, sp 
      move.w d0, result(a6)
      move.w result(a6), d0
   endproc
   stringValue dc.b "abcdefgh"

VARIABLES edit

  • The variables types supported by OnbA are the byte, word (2 bytes) and long (4 bytes), both signed and unsigned. The only difference between a signed and unsigned types is how is interpreted the most signifiant bit (MSBi) as a sign or as a value. If is a sign the range of value is symmetrical disposed around value 0, from - max_value / 2 to + max_value / 2 - 1. If bit is a value, range is between 0 and max_value - 1. It is up to you, but check the supported type when you invoke an instruction or the correct type when call an OS function.
  • The values stored to registers or variables, can be pure values or pointers. Again it is up to you to decide this, and to use the properly instructions to manipulate them. Take care when you move a value to an address register, it will be automatically signed.

Now we iterate the knowledge enumerated in above examples:

  • use registers as often as you can instead of global or local variables. The instructions are designed to work with them.
  • global variables are referred by A5 register; to declare put OnbA on data mode; use directive dc.SIZE, SIZE = .b, .w, .l or .<VALUE>; to access them you must be in code mode.
   ; put OnbA on data mode, is mandatory
   ; do this at the begining of your file
   ; for a single time
   data
   ; declaration name dc.SIZE init_value
   varByte dc.b 'a'        ; a byte, initialized with 'a'
   varWord dc.w 120        ; a word value, initialized with 120
   varWord1 dc.2 120       ; equivalent with previous variable
   varLong dc.l 0          ; a long value, initialized with 0
   varArray dc.24 0        ; an array variable of 24 bytes, initialized with 0
   ...
   ; access the global variables, using A5 register
   ; you must be in code mode!!!!
   ; so put OnbA on code mode 
   code 
   ...
   move.b varByte(a5), d0 ; copy byte value stored in varByte to D0
   pea varWord(a5)        ; push to the stack the address of the varWord variable
   move.l d0, varLong(a5) ; copy the entire value D0 (all 4 bytes) to varLong variable
  • local variables are referred by register A6 (you can use a different address register if you want, but OnbA directives use only A6 register); to declare use directive local name.SIZE; the "local" directive is valid only between directives proc and the next beginproc.
   proc ExampleProc()
   ; declaring local variables
   local var1.w
   local var2.l
   local var3.20
   beginproc
      ...
      ; accessing local variables
      move.w var1(a6), d1    ; copy word value stored in var1 to register D1
      move.l #1000, var2(a6) ; var2 = 1000
      pea var3(a6)           ; push on the stack the address of var3
      ...
   endproc
  • Note: - at the assembly time, OnbA will replace the labels of the global or local variables with the proper numeric value.

Arrays edit

Here start the ugly part of the OnbA. It don't have proper support for arrays. There are no simple "ways" or directives to access array's elements. But let's take the good part, in this hard way you will understand the assembly language better.

void ExampleProc(void)
{
  long array[20];
  array[15] = 1000;
}

Simple in C, isn't it?

   proc ExampleProc()
   local array.80 ; 20 * 4 bytes
   beginproc
      lea array(a6), a0         ; a0 = &array
      moveq #15, d0             ; d0 = 15
      lsl.w #2, d0              ; d0 = d0 << 2, d0 = 15 * 4
      move.l #1000, 0(a0, d0.w) ; &(a0 + d0) = 1000 
   endproc

In fact it doesn't matter how simple it looks in C or C++ or C#, finally it is only a matter how simple it looks in assembly code. Because the processor don't run the C code, but the assembly code. Let's parse the example. First of all to access an array's element we need to access the address of it, address of the first element, and calculate then the offset of the desired element, using element's size and index. First instruction, LEA, loads effective address of the arrays in register address A0. You can choose other address register if you wish. Then, we must compute the offset relative to this address. In this case, size of the elements is size of long (4 bytes), so the offset is 15*4. Both instructions used to compute this value are the fastest possible. MOVEQ is faster then classically MOVE, and LSL << 2 (logical shift left) is faster than MUL * 4. These aspects are very important, because accessing and using array's elements are very common inside of program source. The last instruction is a special type of address mode. Value 1000 is copied to the location pointed by base address stored in A0 shifted with the value of index stored in D0, and with value of the offset, 0 in this case. Perhaps is time to read more about address modes.

void ExampleProc(void)
{
  int *array;              // a pointer to an array of 20 int elements 
  int i                  // index variable
  array = MemPtrNew(40);   // dynamic memory allocation
  for(i = 0; i < 20 ; i++) // initialization loop
   array[i] = i;
  MemPtrFree(array);       // free the memory
}

In this example we use a pointer to an array instead of a static array, i.e. the allocated memory is in the DYNAMICALLY SEGMENT, so we keep a low size for the program stack. For simplicity there is no safe code to check if the allocation was succeeded.

   proc ExampleProc()
   local array.l  ; pointer (4 bytes)
   local i.w      ; don't use byte, use word at least
   beginproc
      systrap MemPtrNew(#40.l)  ; alloc memory, 20*2 bytes
      move.l a0, array(a6)      ; get the address of the memory location
      clr.w i(a6)               ; i = 0
      FOR_LOOP                     ; start loop label
         movea array(a6), a0       ; load base array address to A0
         move.w i(a6), d0          ; get crt index
         add.w d0, d0              ; calculate offset, i*2
         move.w i(a6), 0(a0, d0.w) ; array[a0+d0] = i
         addq.w #1, i(a6)          ; i ++
         cmpi.w #20, i(a6)         ; compare i with 20
         blt FOR_LOOP              ; if i < 20, branch to FOR_LOOP 
      systrap MemPtrFree(array(a6).l) ; free memory
   endproc

Now our local array variable is a pointer, an address of memory. So instead of LEA, we need only the MOVEA (move address) to load the base array address to the base register A0. In case of addresses we can use MOVEA instead of simple MOVE (don't ever use MOVEA with data registers). To calculate the offset, "add <register>, <register>" is faster than "lsl 1, <register>". Last time we used LSL << 2 instead of slower MUL * 4. Now we could do better. Results are the same in all cases, but you will save hundred of microseconds at the execution time. So your code is faster. The code is a simple disassembly of the C source compiled under Onboard C. Now we can understand why is good to "know" assembly language. The example can be improved, because the algorithm created by Onboard C is not the best, is only the safest.
Inside the for loop, first instruction "movea array(a6), a0" is looped on each iteration. And is not need for this. The value in register A0 is not altered, and there is no system functions calls inside the loop (system calls alters the contain of the registers), so it no need to refresh data in A0. We can move it outer. Why use only 2 neurons/registers of the processor, A0 and D0? Onboard C not allows register variables, I know, but here we can do this. So instead of local variable "i", we will use another available data register, D1 by example. Don't forget registers are faster.

   proc ExampleProc()
   local array.l  ; pointer (4 bytes)
   beginproc
      systrap MemPtrNew(#40.l)  ; alloc memory, 20*2 bytes
      move.l a0, array(a6)      ; get the address of the memory location
      clr.w d1                  ; d1 = 0, our new index
      movea array(a6), a0       ; load base array address
      FOR_LOOP                     ; start loop label
         move.w d1, d0             ; get crt index
         add.w d0, d0              ; calculate offset, d1*2
         move.w d1, 0(a0, d0.w)    ; array[a0+d0] = d1 
         addq.w  #1, d1            ; d1 ++
         cmpi.w #20, d1            ; compare d1 with 20
         blt FOR_LOOP              ; if d1 < 20, branch to FOR_LOOP 
      systrap MemPtrFree(array(a6).l) ; free memory
   endproc

So faster algorithm because less instructions and fasters. Now another example:

void ExampleProc(void)
{
  int array0[10], array1[10]; // 2 arrays of 10 int elements 
  int i;                      // index variable
  for(i = 0; i < 10 ; i++)    // initialization loop
     {
        array0[i] = i;
        array1[i] = array0[i]*2; // array1[i] = 2*i in fact
     }
}

And an optimized version of assembly code:

   proc ExampleProc()
   local array1.20 ; 10 * 2 bytes
   local array2.20 ; 
   beginproc
      lea array(a6), a0         ; a0 = &array0
      lea array(a6), a1         ; a1 = &array1
      clr.w d1                  ; index d1 = 0,
      FOR_LOOP                     ; start loop label
         move.w d1, d0             ; get crt index
         add.w d0, d0              ; calculate offset, d1*2
         move.w d1, 0(a0, d0.w)    ; array0[a0+d0] = d1 
         move.w d0, 0(a1, d0.w)    ; array1[a1+d0] = d0, d0 = 2*d1
         addq.w  #1, d1            ; d1 ++
         cmpi.w #10, d1            ; compare d1 with 10
         blt FOR_LOOP              ; if d1 < 10, branch to FOR_LOOP 
   endproc

You can't do this under Onboard C, but here you will do. For arrays of chars or strings I will show another type of address mode:

   void ExampleProc(void)
   {
      char String[40];
      int i;
      for(i = 0; i< 40; i++)
       String[i] = i + 50;
   }

This source is built by me, because Onboard C uses same type of address mode (d8,An,Xi). Here is the (An)+ address mode, indirect address register with post-increment. First will use the address in A0 for our interest, then the address is incremented with size of the operand, in this case size of byte, 1.

   proc ExampleProc()
   local String.40 ; 40 bytes
   beginproc
      lea String(a6), a0        ; a0 = &String
      clr.w d0                  ; index d0 = 0,
      FOR_LOOP                     ; start loop label
         move.w d0, d1             ; d1 = d0
         addq.w #50, d1            ; d1 = d1 + 50
         move.b d1, (a0)+          ; String[a0] = d1, 
                                   ; then a0 = a0 + 1 (size of byte operand)
         addq.w  #1, d0            ; d0 ++
         cmpi.w #40, d0            ; compare d0 with 40
         blt FOR_LOOP              ; if d0 < 40, branch to FOR_LOOP 
   endproc

In conclusion, use faster instruction when you calculate the offset of an element:

  • moveq and lsl for arrays of long
  • add <Dn>, <Dn> instead of lsl for arrays of word
  • try (An)+ address mode for arrays of byte

Structures edit

Here start the very, very ugly part of the OnbA. It don't have proper support for structures. No directives to access structure's elements. Even the "equ" directive is wrong implemented, and is working only in few cases. So you can't use some suggestive way to represent members of a structure. The only way is to use a lot of comments, or to avoid to use structures. Let's consider the following example:

   typedef struct{
      int member1;
      long member2;
      char member3[10];
      }MyStruct; // size of the structure 2 + 4 + 10
   //
   void ExampleProc(void)
   {
      MyStruct mys;
      mys.member1 = 10;
      mys.member2 = 1000;
      mys.member3[5] = 'a';
   }

Assembly code built by OnbA:

   proc ExampleProc()
   local mys.16 
   beginproc
      lea mys(a6), a0 ; a0 = &mys = &first member
      move.w #10, a0  ; mys[a0] = 10
      lea mys(a6), a0     ; a0 = &mys
      move.l #1000, 2(a0) ; mys[a0+2] = 1000
      lea mys(a6), a0         ; a0 = &mys
      lea 6(a0), a0           ; a0 = a0 + 6 = &mys + 6 = &mys.member3
      moveq.l #5, d0          ; index of member3
      move.b #97, 0(a0, d0.w) ; mys.member3[d0] = 97, 'a'
   endproc

Ugly, very ugly. An idea is to insert comment to have a clear view of your structure. Anyway, when use Onboard C you instruct first the compiler how the structure it looks. So you can proceed in the same manner, but to instruct yourself how the structure looks. A optimized source code for previous example:

; MyStruct map offset
; 0 member1, int
; 2 member2, long
; 6 + i, member3[10], char
; 16, size
   proc ExampleProc()
   local mys.16
   beginproc
      lea mys(a6), a0     ; base address of mys
      move.w #10, (a0)    ; copy word #10 at address in A0 + 0, member1
      move.l #1000, 2(a0) ; copy long #1000 at address in A0 + 2, member2
      move.b #97, 11(a0)  ; copy byte #97 at address in A0 + 6 + 5, member3[5]
   endproc

You can structure your comment as you consider is better, to easy see the offsets of the members. Normally you could use the "equ" directive to replace numerical offset with suggestive labels, but is not works here. Other assemblers support this, like Pila. But Pila is a desktop software.
Now is the time to dig a lot inside the address modes to easy handle variables, cause we will continue with other assembly language elements.

STRING CONSTANTS edit

We refer here the implied string passed as arguments to the procedures or system functions.

   void ExampleProc(void)
   {
      char s[20];
      StrCopy("This a string constant", s);
   }

Where is stored the value "This a string constant"? The string constants are the only data stored inside CODE SEGMENT. So if we refer a string constant we must use PC register as reference.

   proc ExampleProc()
   local s.20
   beginproc
      systrap StrCopy(&sValue(pc).l, &s(a6).l)
   endproc
   sValue dc.b "This a string constant"

Don't bother for '\0' null terminator or for padding. OnbA automatically adds a null terminator, and if the situation demands, adds a '\0' to preserve the word alignment boundary. Because string constants are stored inside CODE SEGMENT, which is read-only, don't attempt to write at these locations. You can't and your application probably will crash at this point. There is a minus: writing string constants inside will increase the size of the code segment. And as we know a code segment is limited to 32kb in size. Perhaps is better to use resources to store the strings or pdb files. It is up to you. If your code is too large you can use additional code segments.

OPERATORS edit

Assembly language is not use operators in the way of the high level languages they do. It treats the operators as operations and it uses instructions to cover this.

Assignment edit

As you can see in the above examples, the most used operator, = assignment, is covered mainly by memory movement instructions like MOVE, MOVEA, MOVEQ, LEA etc.

Integer arithmetic edit

Also there are available instructions for integer arithmetic operators:

  • +, addition - ADD, ADDA (add address), ADDI (immediate), ADDQ (quick), ADDX (extend with sign)
  • -, subtraction - SUB, SUBA, SUBI, SUBQ, SUBX
  • *, multiplication - MULS (signed), MULU (unsigned)
  • /, division - DIVS, DIVU
  • %, modulo/remainder is not available through an instruction. Here is an example for remainder operation:
   proc Modulo(op1.w, op2.w) ; return op1%op2
   local
   beginproc
      move.w op2(a6), d0 ; load op2 -> D0
      ext.l d0           ; extends D0 from word to long
      divs.w op1(a6), d0 ; op1/op2
                         ; after execution of the instruction, D0 contains two values
                         ; the higher word -> the remainder
                         ; the lower word -> the quotient
                         ; R:Q
      swap d0            ; swaps the higher word with the lower word
                         ; Q:R
                         ; now, if you use content of the D0 as a word you will have the remainder
   endproc

BCD edit

The 68k processor supports the integer numbers as BCD (binary-coded decimal). The BCD is a encoding of decimal integers where each digit of the number is represented by its binary codification. Read the wiki article http://en.wikipedia.org/wiki/Binary-coded_decimal for more information about BCD. There are available the following instructions: ABCD, SBCD, NBCD.

Bits operations edit

Also there is available a set of bits instructions: BTST, BSET, BCLR, TAS.

Floating Point edit

68k processor isn't supports the floating point numbers. You can use long and juggle with the decimal point. What is difference between 0,00000078 and 78/100000000? Or you can use the Palm OS the two sets of emulation libraries (FPLxxx and FLPxxx) to deal with the floating point numbers.

Logical edit

For logical operations are available: AND, ANDI, OR, ORI, EOR, EORI, NOT

Shift and rotation edit

  • shift: arithmetic ASL/ASR, logical LSL/LSR
  • rotations: ROL/ROR, ROXL/ROXR, SWAP

More information you can find in Instructions Set chapter.

STATEMENTS edit

Both conditional and loop statements are consist of a test portion, followed by a jump instruction to a label. Jump labels are visible in the whole code segment, so is not a bad idea to tag name prefixed with the procedures initials to make the code more readable and to avoid confusion and errors.

Conditionals edit

Let's consider an example:

   int ExampleProc(int Param)
   {
    if(Param == 0)
     return 0;
    else
     return 1;
   }

Three alternatives for the C source:

   ; standard but not used in case of "0" comparison
   proc ExampleProc(Param.w)
   beginproc
     cmpi.w #0, Param(a6) ; CMPI sets the CCR register bits
                          ; according with (Param - 0) result 
     beq EP_Return0       ; if CCR, Z == 1, branch to EP_Return0 
     moveq #1, D0         ; D0 = 1
     bra EP_End           ; branch always to EP_End (return 1)
     EP_Return0
        moveq #0, D0      ; D0 = 0, return 0
     EP_End
   endproc
   ...
   ; alternative because is a "0" comparison
   ; so in fact it is no need for a comparison
   ; we can use a faster instruction, MOVE is faster than CMPI
   proc AnotherExample(Param.w)
   beginproc
     move.w Param(a6), D0 ; when MOVE load the Param to D0
                          ; it sets the CCR register bits
     beq AE_Return0       ; if CCR, Z == 1, branch to AE_Return0 
     moveq #1, D0         ; D0 = 1
     bra AE_End           ; branch always to AE_End (return 1)
     AE_Return0
        moveq #0, D0      ; D0 = 0, return 0
     AE_End
   endproc
   ...
   ; alternative, inverted logic
   proc AnotherAnotherExample(Param.w)
   beginproc
     move.w Param(a6), D0  ; when MOVE load the Param to D0
                           ; it sets the CCR register bits
     bne AAE_Return1       ; if CCR, Z == 0, branch to AAE_Return1
     moveq #0, D0          ; D0 = 0
     bra AAE_End           ; branch always to AAE_End (return 0)
     AAE_Return1
        moveq #1, D0      ; D0 = 1, return 1
     AAE_End
   endproc

After the execution of an instruction, the register CCR flags are set according to the result value from the perspective of value 0. Considering this, if a comparison with 0 is sufficient to analyze the CCR register value immediately after the execution of instruction. For a comparison with a nonzero value, an additional CMP instruction must be executed. In fact the CMP is a SUB instruction without effective execution, but the flags (C, X, N, Z, V) are set in accordance.
The Bcc instructions set (Branch if Condition Code) can be used to create different conditional scenarios. Practically these instructions will cause a branch in the program if certain flags are set.

BHI Branch if Higher Than if Z == 0 && C == 0 BLS Branch if Lower or Same if Z == 1 && C == 1
BCC Branch if Carry Clear if C == 0 BCS Branch if Carry Set if C == 1
BNE Branch if Not Equal if Z == 0 BEQ Branch if EQual if Z == 1
BVC Branch if oVerflow is Clear if V == 0 BVS Branch if oVerflow is Set if V == 1
BPL Branch if PLus if N == 0 BMI Branch if MInus if N == 1
BGE Branch if Greater or Equal if N == V BLT Branch if Less Than if N != V
BGT Branch if Greater Than if N == V && Z == 0 BLE Branch if Less or Equal if N != V && Z == 1
BRA BRanch Always

A switch ... case example:

   int SwitchProc(int Param)
   {
    switch(Param) {
       case 1: return 1;
       case 2: return 2;
       case 3: return 3;
       default: return 0;
      }
   }

And the asm source:

   proc SwitchProc(Param.w)
   beginproc
      cmpi.w #1, Param(a6)  ; Param - 1
         beq SP_ret1        ; if Z == 1 ( Param == 1) branch SP_ret1
      cmpi.w #2, Param(a6)  ; Param - 2
         beq SP_ret2        ; if Z == 1 ( Param == 2) branch SP_ret2
      cmpi.w #3, Param(a6)  ; Param - 3
         beq SP_ret3        ; if Z == 1 ( Param == 3) branch SP_ret3
      clr.w d0              ; else D0 = 0
         bra SP_End
      SP_ret1
         move.w #1, d0      ; D0 = 1
         bra SP_End
      SP_ret2
         move.w #2, d0      ; D0 = 2
         bra SP_End
      SP_ret3
         move.w #3, d0      ; D0 = 3
      SP_End                ; return
   endproc

It's not so difficult as it seems to be.

Loops edit

For loop statements you need a starting label to identify the start of the loop. Then you must put a branch instruction to jump to the start of the loop or out of it. Between start label and the branch instruction you must alter the reference value.

   ...
   int var;
   ...
   for(int i = 0 ; i < 10; i++)
     var = i;
   ...

We can use one of the Bcc instructions to do this:

   ...
   local var.w
   ...
   ; we will use D1 as index
   move.w 0, d1          ; i = 0
   START_LOOP
      move.w d1, var(a6) ; var = i
      addi.w #1, d1      ; i++
      cmpi.w #10, d1     ; i ? 10
      blt START_LOOP     ; Branch if Less Than to START_LOOP
   ...

Alternatively we could use DBcc instructions set (Decrement and Branch if a condition code):

   ...
   int var;
   ...
   for(i = 10; i > 0 ; i --)
      var = i;
   ...

But is not supported by OnbA:

   ...
   local var.w
   ...
   ; we will use D1 as index
   move.w #10, d1          ; i = 10
   START_LOOP
      move.w d1, var(a6) ; var = i
      dbhi d1, START_LOOP     ; branch if higher than 0 to START_LOOP and decrement D1
   ...

FUNCTIONS edit

As we have seen, most examples have been structured as procedures. The goal was gaining habit of using them. There are two types of functions: system routines and the user procedures. In fact, the concept of function is very different from the C language. Here, there is no return values. In case of the system routines (Palm OS APIs):

  • register D0 will be used as a placeholder for return values
  • register A0 will be used as a placeholder for return addresses (pointers)

So if you intend to use these values you must do this before to alter in some ways the content of the specified registers. In case of user procedures there is no obligation to use D0 or A0 (it is up to you where/from write/read), but it is a good habit to use same standards.

System Routines edit

The system routines (Palm OS APIs) are blocks of binary code already placed at fixed memory locations by OS at the moment of device initialization. They are interfaces with memory, files area, interrupts, display registers and memory and so on. We can invoke them using TRAP #15 instruction followed by a word value containing the code of the routine. First of all we must do a PUSH operation of their arguments: increase the size of stack and put arguments on the stack into this space. The parameters must be pushed backwardly. After the call of the routine, we must decrease the size of the stack. And of course if we intend to use a "return" value, we must read D0 or A0, immediately after these. Let's consider a mixed example, a system routine, inside of a user procedure:

   void Example(void)
   {
      MemHandle h;
      h = MemHandleNew(50); create a memory handle, 50 size
      ...
   }

The asm "real" source:

 Example            ; label of Example proc
  link a6, #-4      ; creates local frame for proc, only a long value for handle
  move.l #50, -(sp) ; increase the stack, then put the "50" parameter to the stack
  trap #15          ; call system api
  dc.w $A01E ;      ; invoke MemHandleNew
  addq.l #4, sp     ; decrease size of the stack  
  move.l a0, -4(a6) ; copy the returned address into -4(a6) handle 
  ...
  unlk a6           ; free the local frame 
  rts               ; return

The asm OnbA source (easy way):

   proc Example()
   local h.l
   beginproc
      systrap MemHandleNew(#50.l)
      move.l a0, h(a6)
      ...
   endproc

Using of systrap directive:

systrap TrapName / #TrapNumber (parameter.size, ...)

where:

  • TrapName equ $<NumberOfSystemTrap>
  • size - specifies the size of the operand, for correct stack management; if you forgot to do this OnbA will use .w, so take care!

In case of pointers, if you need to pass a "location", use ampersand & prefix and there is no need to specify a size:

systrap TrapName / #TrapNumber (&parameter, ...)
  • here size will be always long, or 4, because it is an address.
  • in the PUSH sequence "move.l ..." will be replaced with "pea ..."

User Procedures edit

The user procedures are blocks of code referred using PC register. To access them you can use BSR/JSR instructions. See example above in this chapter Code Segment. To define a procedure you must use the following OnbA directives:

  • proc ProcedureName(param.size, ...)
  • local var.size ; optional, valid only here
  • local ...
  • beginproc
  • ...
  • endproc

When you call a procedure defined with OnbA directives you must use call directive:

call ProcedureName (parameter.size, ...)

where:

  • size - specifies the size of the operand, for correct stack management; if you forgot to do this OnbA will use .w, so take care!

In case of pointers, if you need to pass a "location", use ampersand & prefix and there is no need to specify a size:

call ProcedureName (&parameter, ...)
  • here size will be always long, or 4, because it is an address.
  • in the PUSH sequence "move.l ..." will be replaced with "pea ..."

To get the "returned" values, it is up to you where/from where write/read this values, but is better to use D0 for values and A0 for addresses. Is easy to read and everybody will talk same "language".