Archive

Assembly Language - Part 2



Chris Johnson

In the first part of this series, we had an overview of features of the CPU hardware that any assembly language programmer should be aware of when writing code. Let us now take the first steps in learning how to write in assembler. You may find it useful to have the first part to hand, in case you wish to refer back to it.

The Basic assembler

All RISC OS machines are provided with Basic as part of the operating system. Many would argue that Acorn's flavour of Basic, which had its origins in the humble BBC model B (or should that be A?), is the best in the world. (You may say that - I could not possibly comment!) One of the features that makes it unique is the inbuilt assembler, which not only allows you to save assembled code to a file for subsequent use, but also allows a running program to assemble machine code on the fly and then use it without needing to save a file first. Since Basic under RISC OS is an interpreted language, this makes for a simple and speedy way to produce and test machine code routines. It also means we can make use of variables known to the global program for addresses or data.

Acorn has provided, within Basic, additional keywords to invoke the assembler, and the assembler itself knows about the ARM registers and the mnemonics corresponding to the various cpu instructions. One thing the Basic assembler does not include is the ability to assemble floating point instructions. We shall try to deal with this in a much later article.

Health warning and disclaimer

Over enthusiastic playing with assembler and machine code can be detrimental to the smooth running of your computer. When you get it wrong, you can trample over very sensitive parts of the anatomy (memory) of your beloved machine, either necessitating a hard reset to regain control or, more frustratingly, screwing up other applications innocently sitting on the iconbar, and causing them to crash at some undefined time in the future. I would therefore strongly recommend that you save all documents, and probably quit as many applications as you can before experimenting. It is also wise to reboot your machine before you return to using applications.

Down to business

Let us try to produce a very simple program that will assemble a small bit of code to do nothing more than write to screen some alphanumeric characters. In the process, I hope to explain some of the basics of invoking the assembler, and then actually running the code. There are two special 'keywords' used to invoke the Basic assembler, and tell it what are assembler instructions. The '[' square open bracket symbol signals the start of assembler, and the corresponding ']' square close bracket symbol is placed after the assembler instructions. The use of these keywords will become clear later.

Reserving memory

When we assemble some code, we have to put it somewhere in memory, which must be reserved solely for the code. We can do this easily enough using the standard DIM statement, e.g.

DIM ourcode% length%

This will reserve a block of memory of length length% (actually length%+1), and place the start address of this memory in the variable ourcode%. We must ensure that we reserve enough memory for our purpose, otherwise the assembler will overrun the end of the reserved memory, and may overwrite other variables used by our program, causing a crash. Thus always be generous in allocation of memory, and check the length of the code produced.

Two pass assembly

The Basic assembler, in common with essentially all other assemblers, can be made to process the assembler code twice, which is known as two pass assembly. This is necessary to allow it to deal with what are known as forward references. All but the simplest programs will involve, for example, the use of subroutines that are called from somewhere else in the code, or perhaps conditional tests (equivalent to IF...THEN...ELSE). This is implemented by abandoning the sequential execution of instructions, and jumping to a new section of the code. If this is somewhere later in the program, the assembler does not yet know the address where that code will be. However, once the assembler has been through the whole code, it then knows about all these referenced addresses. On the second pass, the address can be correctly inserted into the appropriate instruction. Since we are using Basic, we can implement this two pass assembly using a simple FOR...NEXT loop.

The OPT directive

OPT is an assembler directive which controls certain aspects of the assembly process. (This is not the same as the *opt used by the operating system to control the boot behaviour of a hard disc.) Each (digital) bit of OPT has a particular meaning.

Bit Meaning
0 Produce assembly listing if set
1 Enable assembler errors if set
2 Allow offset assembly if set
3 Check that assembled code does not exceed memory limit if set

We are allowed to use different values of OPT for each pass through the code. This is helpful because we would not want to produce the listing twice over, and we would expect errors to occur during the first pass, due to forward references as described above. It would make sense then to have bits 0 and 1 unset during the first pass, but set them during the second pass to obtain a listing, and report any real errors.

Offset assembly we shall meet later. Briefly, it allows you to assemble code in one part of memory, when it is actually for use in another part of memory, e.g. it is a relocatable module. Bit 3 implements one way of checking that we have reserved sufficient memory for the code.

The program pointer

The assembler uses the Basic variable P% to store the current value of the program pointer. Thus we must always ensure we initialise P% to the correct value (and this means for both passes, since P% is continuously incremented as the assembly proceeds), and it is wise to treat P% as a reserved variable, so we do not inadvertently change it at the wrong time.

Labels

We have seen that we may need to influence the flow of the program by branching to new parts of the code. We refer to these particular points in the code by assigning a name or label to it, and prefix the name with the fullstop as in

.labelname

The full stop identifies this name labelname to the assembler as a label, and the assembler will create a variable labelname and place in it the current value of P%. From now on, whenever we refer to this particular label, the assembler will know the correct address to use.

How to execute the code from memory

Assuming that we have successfully assembled our code, we will be keen to test it out. How can we do that? Basic provides two keywords that allow a piece of machine code to be executed. These are CALL and USR, and they are somewhat similar in use. CALL behaves very much as a PROC or GOSUB in Basic, while USR returns a result and so is more like a FN. Both methods must be passed the address of the code to be executed so, in its very simplest implementation, the Basic instruction

CALL ourcode%

will execute the code we have assembled in the memory block pointed at by the variable ourcode%, and then return to the next Basic statement. The most important thing to note at the moment is that when Basic transfers execution to our bit of code, it places the return address in register R14. We use this return address when we exit our code, so we must not corrupt it.

Enough of this - gimme some code!

What we shall try to do first is to implement the following Basic program in assembler.

FOR char% = 33 TO 126
  PRINT CHR$(char%)
NEXT

Exciting eh? All this does is to print out the standard keyboard characters, excluding the space character. Well, we have to keep it pretty simple, since we have not looked in any detail at assembler instructions, so we will have to take the code on trust.

We should be able to write down a shell program from what has gone before, and then fill in the assembler bit shortly. In the following code the semi­colon ';' character is used within the assembler to signify a comment. (The assembler itself, although running under Basic, does not understand the REM statement, but instead uses ; which is the standard symbol for a comment for assemblers on many platforms.)

length%=100
DIM ourcode% length%
REM implement two passes and set up 
REM values for OPT at the same time
REM first pass - enable nothing,
REM i.e. OPT 0
REM second pass - enable error report
REM and listing, i.e. OPT 3
FOR pass% = 0 TO 3 STEP 3
  REM set P% to start of our code
  REM on each pass
  P% = ourcode%
  REM now invoke the assembler
  [
  ; set value of OPT as appropriate
  OPT pass%
  ; assembler code will go here
  ]
REM we have now left the assembler
NEXT pass%
PRINT "Length of code = ";P%-ourcode%
PRINT "Block allocated = ";length%
PRINT "Now calling code"
CALL ourcode%

It might be useful to type in and save this shell separately for use in the future. I have included some REM's to remind you what we are doing, and some print statements to show the length of code to check we have allocated enough memory. Remember that at the end of each pass, P% will be pointing to the next location after the end of our code, and so must be reset for the second pass.

Some simple mnemonics

Although we are once again anticipating what is to come, we cannot avoid using some mnemonics. To load a register with a value, we use

MOV dest_register, #number

The hash tells the assembler it is a number constant and not a register. To copy the contents of register R1 into register R2 we would use

MOV R2, R1

To implement some sort of FOR..NEXT loop, we need a way of comparing two values. We shall use the CMP instruction, which can compare either two registers, or a register and a number. This instruction compares the left hand operand with the right hand operand and automatically sets the flags. (Remember from part 1 that the program counter register has four flags that can be set according to the result of any arithmetic operation.)

CMP R0, #126

will compare the contents of register R0 with number 126 (note the hash).

Since we are already getting adventurous, and implementing a loop, we need the branch instruction. No prizes for guessing this is B. However, we shall need to qualify this instruction by taking account of the flags that we set using the CMP instruction, so we can decide when to exit the loop. Luckily, we do not have to laboriously work out in which state the carry, negative or overflow flags must be, since the mnemonics are written to be more user-friendly. We can add conditions such as EQ (equal to) or HI (higher than), and the assembler does the hard work.

We also need to implement a counter for the loop. In this case, it is a simple matter of adding 1 each time we pass around the loop. The instruction

ADD dest_register, operand_1, operand_2

will add the operands together, and store the result in the specified destination register. Operand_1 must be a register, while operand_2 can be a register or a number.

We will want to print out the results of our little epic. In Basic itself, we can liberally sprinkle PRINT statements all over the place to keep track of what is going on. There is no corresponding PRINT statement in assembler. However, Acorn has allowed easy access to all of the operating system by means of numerous SWI calls. These are the assembler equivalent of the SYS call in Basic. The call of most interest to us is 'OS_WriteC' which sends the byte in R0 to all of the active output streams. This will be the screen in our case.

Finally, we must go back to Basic when our little routine has finished. Remember that when we CALL our code, the address we must return to is placed in register R14. Assuming the contents of R14 have not been changed in the meantime, we simply have to copy its contents into the program counter. This will cause execution to continue from where we left Basic. The program counter is register R15. The instruction

MOV R15, R14

will therefore accomplish this.

The code

This is the code we would put in the assembler section left blank above.

[
; set value of OPT as appropriate
OPT pass%
MOV R0, #33     ; set the starting
                ; character value
.repeat         ; label - loop back
                ; here for next char
SWI "OS_WriteC" ; print the character
ADD R0, R0, #1  ; R0 = R0 + 1
CMP R0, #126    ; compare R0 with 126
BLS repeat      ; if R0 is lower or
                ; the same go again
MOV R15, R14    ; transfer return
                ; address to program
                ; counter and so
                ; return to Basic
]

There is not much to it is there? Save the full Basic program, once you are sure there are no errors.

Testing the program

I would suggest you open a task window, and enter Basic using *basic. Once Basic is running, LOAD"yourprog", (you will have to take account of the full path name, or take the easy way out as I do and temporarily copy it into the root directory) and then RUN it. If all is well, you should see the assembler listing, followed by the results of the PRINT statements, followed by a long string of the keyboard characters. Since you are using a task window, you can scroll back and inspect the assembler listing.

The assembler listing

You will see it has three fields of the form

000092F8 E3A00021  MOV R0, #33   ; set the
starting character value

The first set of eight hexadecimal digits is the current value of the program pointer, i.e. the address of the instruction. You can check this by using the Basic command (in the task window)

PRINT ~ourcode%

The result should be the same as the address of the first assembled instruction. Whenever an instruction is actually assembled, the value should increment by four, since each machine instruction is one 32 bit word (i.e. four bytes).

The second eight digit hexadecimal number is the actual machine code instruction the assembler has produced. Note in the above example that the last two hex digits, 21, correspond to the value 33 in decimal. The third field is simply the original assembler instruction.

Can I experiment?

Remember the disclaimer at the start of this article. However, there are some things you can do. For example you could change the threes to twos in the following code.

FOR pass% = 0 TO 3 STEP 3

This should suppress the assembler listing. In this particular example, the two pass assembly is not strictly necessary, since there are no forward references, but it makes sense to start as we intend to continue.

You could change the start number from #33, and the end number from #126, to change the range of characters actually printed out. You are recommended not to go below 32, since you are then in the area of control characters, and these could have unpredictable effects. You could also change the ADD instruction to implement stepping 2 or 3 characters at a time (similar to the STEP part of the FOR..NEXT loop in Basic).

You could add the following line within the loop.

SWI "OS_NewLine"

You should be able to predict the effect on the print out.

What next?

In the next part, we shall first extend our little program above, to illustrate further subtleties of the CALL and USR commands, and then begin to consider in much more detail the ARM instruction set.

Feedback

I reproduce below what I wrote at the end of the first article. I should state that some feedback, and some routines, have begun to arrive, but I should like to see much more.

In order for this series to be a success, I believe it is essential for there to be some real feedback. I should like readers to comment on areas such as whether I am going too slowly/too quickly, whether I am assuming too much/too little in prior knowledge, and so on. It would also be helpful to know where readers see a use for assembler, and what areas they would like expanded.

As part of this series, I should like to set up a hints and tips section, or maybe a 'code snippets' section, where useful subroutines or algorithms could be made available. This can only work successfully with active cooperation from readers. I am therefore asking the more experienced amongst you to have a look in all those libraries of code on your hard disc, and dig out anything that might be useful. If such a routine could also form the basis of a section on the use of particular types of instruction, so much the better. Due acknowledgement would always be made, of course.


Contents - The Archives - Archive Articles