Archive

Assembly Language - Part 3



Chris Johnson

Feedback

I have had some feedback on the first two articles, mainly by email, but some letters as well. All have been encouraging, and so far are about 10 to 1 in favour of a slow and gradual treatment of the subject. I have also received some code snippets, which I shall try to use, either as illustration of specific points, or simply as routines that may prove useful for readers' own development purposes. Please keep the feedback coming, since there have already been some very useful comments that I shall endeavour to keep in mind in future articles. See below for my contact details.

More on CALL

In the last part, we used the Basic command CALL to run our machine code program from within a Basic program. At the time, I suggested that CALL was a little like a PROC in Basic. One of the features of PROC is that it is possible to pass parameters. It would certainly be very restrictive if we were unable to pass parameters to a section of machine code. In fact, CALL is very flexible in the way it allows parameters to be passed from the calling Basic program.

The simplest and most direct way to pass parameters is to make use of resident integer variables. Every time the CALL statement is executed, before the code is entered, the ARM's registers R0 - R7 are set up as follows.

R0 = A%
R1 = B%
R2 = C%
R3 = D%
R4 = E%
R5 = F%
R6 = G%
R7 = H%

This allows us to pass up to eight integer values to the code we are calling. These integers may simply be values, or may be, for example, pointers to blocks of memory, or strings.

As a simple example, let us modify the code we used last time, to allow us to pass the start and end characters as parameters. We will use simple GET statements to set A% and B% to the start and end characters before calling the assembled code. I have also included a loop so you can try various combinations of characters to convince yourself that the routine is using the variables A% and B%. Press <escape> to finish.


length%=100
DIM ourcode% length%
FOR pass% = 0 TO 3 STEP 3
  P% = ourcode%
  [
                      ; set value of OPT as appropriate
  OPT pass%
                      ; On entry R0 holds start character (from A%)
                      ; On entry R1 holds end character (from B%)
  .repeat             ; label - loop back here for next char
  SWI "OS_WriteC"     ; print the character
  ADD R0, R0, #1      ; R0 = R0 + 1
  CMP R0, R1          ; compare R0 with R1 (end character)
  BLS repeat          ; if R0 is lower or the same go round again
  SWI "OS_NewLine"    ; print a new line character
  MOV R15, R14        ; transfer return address to program
                      ; counter and return to Basic
  ]
NEXT pass%
PRINT "length of code = ";P%-ourcode%
PRINT "block allocated = ";length%
REPEAT
  PRINT"Enter start character"
  A%=GET
  PRINT"Enter finish character"
  B%=GET
  PRINT "Now calling code"
  CALL ourcode%
UNTIL FALSE
REM Press escape to finish

Returning a value

The command USR operates in a very similar manner to CALL, except that it returns the integer value that is held in the register R0 on exit from the machine code routine. This can be demonstrated by modifying the REPEAT...UNTIL loop used above as follows.

REPEAT
R%=0
PRINT"Enter start character"
A%=GET
PRINT"Enter finish character"
B%=GET
PRINT "R% is now = ";R%
PRINT "Now calling code"
CALL ourcode%
PRINT "R% is now = ";R%
PRINT "Now calling code using USR"
R%=USR ourcode%
PRINT "R% is now = ";R%
UNTIL FALSE

You should find that the value of R%, printed after using USR, corresponds to one more than the ASCII code of the last character printed, since this is the test used in the CMP instruction to terminate the loop.

More on passing variables

CALL (but not USR) allows the passing of additional variables by adding them to the calling line, as in

CALL somecode,parameter1%,b,c%(),d$

The parameters are passed as a comma separated list, and must exist, of course. When the machine code is entered, R9 points to a list giving details of the parameters. For each variable, two word-aligned words are used, the first giving the address where the variable is actually stored or of a block which contains the address and further information.

The second word has a value indicating the type of variable, e.g. integer, string, array. R10 gives the number of variables passed, or zero if no variables are passed in this way. The user code must then provide its own routines for extracting the values of these passed variables. For the moment, I shall say no more about this method of passing variables to a user routine. There are full details in the BBC Basic Guide under the CALL keyword.

Assembler instructions

The time has now come to consider in more detail the assembler instructions, and it is going to take us a few issues to cover them all. In general we might initially group the instructions into classes: data operations, loading and saving registers, and SWI calls. There are also a small number of instructions that do not fit comfortably into any of these classes, such as the branching instructions.

Data operations

These are the operations that we shall make use of the most, since they are the ones that involve manipulating data. We have already met some, such as MOV, ADD and CMP, and it is this group I shall look at in detail first.

Load and save

These instructions allow registers to be loaded from memory, or their contents saved to memory, either singly or as groups. Other variations include whether a full word (32 bit) or a single byte (8 bit) is used in the operation, and how the memory location is addressed. This is the only group of operations that involve memory locations.

SWI calls

The SWI (SoftWare Interrupt) instruction allows us to access the facilities provided by the RISC OS operating system, so we can make use of much of the software residing in the ROMs in our machine. One problem is obtaining information on the many hundreds of possible calls, and in particular how the registers must be set up on entry, and what values are returned. The Programmer's Reference Manual, which is available also on CD-ROM, is almost indispensable, but of course is a serious outlay.

There are few alternatives other than what appears in the various Acorn magazines from time to time. There is also a StrongHelp manual covering many of the more common calls. As far as I am aware this is freeware, as is StrongHelp itself, so I am sending copies to Paul, in case he can find room on the monthly disc. For the purposes of this series, I will try to explain about the SWI calls I use when I use them.

Data storage

Before considering assembler instructions in detail, which I shall leave to next month, this may be an opportune moment to look at data storage. Many programs require their own associated data storage area, perhaps as data for the program, or to use as workspace. There are some additional assembler directives to allow us to set aside data areas. Note that these directives are a function of the Basic assembler, and are not ARM mnemonics. They are:

EQUB - reserve a byte and assign a value to it

EQUW - reserve a 16-bit word (two bytes) and assign a value to it

EQUD - reserve a double word (four bytes) and assign a value to it

EQUS - reserve and assign a string (of length 0 - 255 bytes)

When including such data storage areas within code areas, a problem would arise if the data storage area were not a multiple of four bytes, since all ARM instructions must reside on a four byte word boundary. Thus the Basic assembler includes the directive ALIGN, which should be used after setting up data storage areas to ensure that the next instruction will be assembled on the correct word boundary. To demonstrate in particular the use of EQUS and ALIGN, let us use a modified form of our simple program to do the "PRINT" and "GET" from within the assembler.

We shall make use of two more SWI calls.

SWI"OS_WriteS"

This is a very useful call that uses no registers on entry, but simply sends the string immediately following the instruction to all of the output streams. The string must be terminated by a zero byte (in assembler the zero byte, equivalent to CHR$(0), generally is used as a terminator, rather than the CHR$(13) used by Basic). The SWI also alters its return address so that execution continues at the first word aligned instruction after the string. Thus the string is simply included in-line with the code.

SWI"OS_ReadC"

This call, which uses no registers at entry, reads a character from the current input stream, and returns its ASCII code in R0. If the carry flag is set (=1), then an error occurred, and R0 will contain the error type. In particular, if the carry flag is set, and R0=&1B (27 in decimal), then escape was pressed. However, in this simple example, we are not specifically trapping the escape key, but are assuming the operating system will sort it out for us (not good practice, but OK for now).

Here then is the modified code. Note that, with the in-lined data, the length of the code has increased, and so the length of the DIM'd block should be increased to say 200 bytes.

length%=200
DIM ourcode% length%
FOR pass% = 0 TO 3 STEP 3
  P% = ourcode%
  [
                               ; set value of OPT as appropriate
  OPT pass%
  SWI "OS_WriteS"              ; print the following string
  EQUS "Enter start character" ; string to print
  EQUB 0                       ; terminate string with zero
  ALIGN                        ; make sure next instruction
                               ; is on a word boundary
  SWI"OS_NewLine"
  SWI "OS_ReadC"               ; get a character in R0
  MOV R4, R0                   ; copy it to R4 as temporary store
  SWI "OS_WriteS"
  EQUS "Enter finish character"; string to print
  EQUB 0                       ; terminate string with zero
  ALIGN                        ; make sure next instruction
                               ; is on a word boundary
  SWI"OS_NewLine"
  SWI "OS_ReadC"               ; get a character in R0
  MOV R1, R0                   ; copy it to R1
  MOV R0, R4                   ; load start character back into R0
                               ; On entry R0 holds start character
                               ; On entry R1 holds end character
  .repeat                      ; label - loop back here for next char
  SWI "OS_WriteC"              ; print the character stored in R0
  ADD R0, R0, #1               ; R0 = R0 + 1
  CMP R0, R1                   ; compare R0 with R1 (end character)
  BLS repeat                   ; if R0 is lower or the same go round again
  SWI"OS_NewLine"
  MOV R15, R14                 ; transfer return address to program
                               ; counter and return to Basic
  ]
NEXT pass%
PRINT "length of code = ";P%-ourcode%
PRINT "block allocated = ";length%
REPEAT
  PRINT "Now calling code"
  CALL ourcode%
UNTIL FALSE
END

The above code demonstrates how easy it is to make use of standard operating system routines via SWI calls to carry out complex tasks, and saves us having to write an awful lot of code from scratch. All these code examples are on the monthly disc to save you a bit of typing. Next month, we shall deal in earnest with a range of assembler instructions.


Contents - The Archives - Archive Articles