You could ask why I don't speak about structuring the programs. The answer is that there is no structuring in assembly! The language is entirely made up of instructions, and it's up to you to create structuring. Such abstract structural elements as loops or subroutines are not provided by the language; you have to code them manually. This might sound scary, but it's quite easy to get used to. Moreover, this is a very powerful feature, since it gives you the complete freedom of creation.
Arrays provide a good opportunity to demonstrate this process in an example. Let's make a routine that creates an array of ten elements containing the even integers from 2 to 20. There will be lots of new concepts to explain.
MakeArray: ; this is the label indicating the beginning of the routine ld a,2 ; A holds the value of the first element ld b,10 ; B is the number of times we want our loop to execute ld hl,Numbers ; HL now holds the address of the first element ArrayLoop: ; the label marking the beginning of the loop ld (hl),a ; storing the current element at its proper place inc hl ; increasing HL by 1, so that it points to the next element add a,2 ; increasing A by 2, giving the value of the next element djnz ArrayLoop ; the end of the loop; this instruction decreases B, checks ; if it is zero, and jumps to the label given if it isn't; ; when B is zero, execution continues after DJNZ ret ; returning from the subroutine; explained a bit later Numbers: ; this is the label that identifies our array .byte 0,0,0,0,0,0,0,0,0,0 ; initially the array will be full of zeroes OtherStuff: ; just some kind of other data not used now .byte 100 ; this is the byte that immediately follows our array
Let's go through the code step by step. The three
ld
's at the beginning are the initial values of the
loop. Keep in mind that Numbers is just a memory address for the
computer, i. e. an ordinary 16-bit integer. The same goes with
ArrayLoop, but it is of course a different address. It is very
important to note: assembly does NOT distinguish between variable names and
labels that mark certain parts of the code. They are all the same kind of
stuff. You could use ArrayLoop as a variable as well, but then
you would overwrite the instructions of the loop. (This is actually an advanced
programming technique called Self Modifying Code or SMC, but it is too early
for you to talk about at this point.) Be aware that the computer cannot
distinguish between data and code, because both are just bunches of bits.
It is easy to screw things up if you are not careful enough. This is one
of the reasons why you should first test your code on an emulator.
Okay, that said, I would go on discussing the loop itself. The first instruction
after the label is ld (hl),a
, which copies the value of
A into the byte pointed by HL, as we already know. When
the program first enters the loop, A contains 2, the value of the
first element we want to set, and HL points to the first element.
So this instruction loads the proper value into the element. After this,
you can see inc hl
. This instruction advances HL by
1, so that it will point to the next element to be processed. Then we have
add a,2
that calculates the value to be put into the next element.
The most important instruction this time is djnz
. It always
works with register B, that's just one special register
role I was talking about in the Registers
section. djnz
is very useful to create loops, but it is essential
to memorise that it is not a "loop instruction", as such
things don't exist in assembly. It is however a built-in conditional
branch instruction that first decrements B by one, then it jumps
to the address given if the result is not zero. If B is zero
when the instruction is executed, it will overflow and take the value of
255, and since this is not zero, the jump will be taken. If I had written
0 instead of 10, the core of the loop would have been executed 256 times
for this reason. To sum up, djnz
is equivalent to "jump
if B is not equal to one or continue if it is, and decrease it
anyway".
After the loop, what do we have in the registers? B will be zero,
as this is the condition of leaving the loop. A will hold 22, which
would be the value of the 11th element of the array, if there was such a
thing. HL also points to this virtual 11th element, which is actually
the same as OtherStuff. If we were to do an ld c,(hl)
for instance, C would take the value of 100. Note that we have easily
entered the area of another variable, and if I had loaded 11 into B
initially, this byte would also have been overwritten. This is just another
source of error: there is nothing to prevent you from corrupting other variables.
The price of freedom is responsibility. Take that seriously.
Forget ret
for a while, I will get back to it later.
Most of the programs do not simply consist of a series of instructions that are consecutively executed, but there are many places where you need to decide which way to take. This is where the flags register comes into sight. Just as always, we will look at a simple example (actually the loop above is already one). We have a signed number in A. We want to take its absolute value and write it back into A. Let's start with the code:
cp $80 ; comparing the unsigned A to 128 jr c,A_Is_Positive ; if it is less, then jump to the label given neg ; multiplying A by -1 A_Is_Positive: ; after this label, A is between 0 and 128
To take the absolute value, we first have to find out whether the number
is negative or not. If it is, then it must be multiplied by -1. The first
instruction does this with a little trick. As we know, all negative numbers
in binary representation start with 1. In other words, if we consider them
to be unsigned integers, they are all greater than 127. The cp
instruction does the following thing: it subtracts the value of the operand
- either a 8-bit constant, a 8-bit register or (HL),
(IX+n), (IY+n) - from A, but does not
write the result anywhere. However, it modifies the flags register
(F). If the virtual subtraction results in zero, i. e. A
is equal to the operand given, the Z (zero) flag is set. If
A is less than the operand, the subtraction results in a step over
zero, i. e. the C (carry) is set. If A is greater, then
both C and Z are reset (set to zero). For these relations,
both numbers are considered as unsigned integers.
In the end, if A is negative upon entering the piece of code above,
the cp
instruction sets the carry flag. The next one,
jr
is a jump instruction. It can have either one or two operands.
If there is only one operand, then it is a label, and the program jumps to
this label when the instruction is executed. However, in our case there are
two operands. The first is always the condition, and the second is the label
to jump to if the condition is met. This time the jump will be taken if the
carry flag is set. All the possible conditions are listed in the next section.
neg
is an instruction that negates the value of A.
Note that -128=128 when 8 bits are used to represent a number.
You could see a jump instruction in the previous example, so it's
time to look at them more closely. The fact whether a jump is absolute or
relative depends on how you calculate the address of the destination. In
the case of absolute jumps (jp
instruction), address is always
given with respect to the beginning of the memory, while the relative jumps
(jr
) only know where to jump with respect to themselves. It
is easier to list the differences between the two in a table.
Property | Absolute | Relative |
---|---|---|
Instruction |
jp |
jr |
Length of address |
16 bits |
8 bits |
Length of instruction |
3 bytes |
2 bytes |
Speed of execution |
faster |
slower |
Possible destination |
anywhere |
the vicinity (+/- 128 bytes) |
Possible conditions |
c, nc, z, nz, pe, po, m, p |
c, nc, z, nz |
Both kinds of jumps can be either conditional or unconditional, and the
conditions work the same way. They can be one of the following:
c
(C flag set), nc
(C flag
reset), z
(Z set), nz
(Z
reset), and only in the case of absolute jumps: pe
(P
set), po
(P reset), m
(S
set) or p
(S reset). At this point I think I should
mention djnz Label
again, because as we already know how it
works, we can see that it is equivalent to:
dec b ; decrementing B by 1 jr nz,Label ; if it did not become zero, jump to Label
This pair of instructions and djnz
are actually interchangeable;
the only differences are that djnz
is smaller and faster, and
it preserves the flags. Here you can see that the destination of
djnz
is also limited to its vicinity. Therefore, if you want
to take a jump that points farther than 128 bytes (one instruction can be
1, 2, 3 or 4 bytes long), you must use a jp nz
combined with
a dec b
instead of it. Although it is somewhat larger in size,
it is almost as fast as the original djnz
. The assembler will
always tell you if a relative jump cannot be taken, because it is the one
that converts the address of the label into a relative address. When such
an error is generated, you have to use jp
.
Another fundamental element of programming is the use of subroutines. In
assembly, they are handled by two instructions: call
and
ret
. The former is used to enter a subroutine, and the latter
for returning from the subroutine and continue where we left off. This is
achieved with the help of the stack. That's why you should
not play around with SP, unless you really know what you are doing.
Just as with djnz
, we can express call
and
ret
with the help of virtual instructions we can already interpret.
Call
is basically a push pc+3 followed by a jp
Label. (The +3 is needed to jump over the
call
-always 3 bytes long-after returning.)
ret
is even simpler, it is equivalent to a pop pc.
Of course, this decomposition is not entirely true, since every time an
instruction is executed PC is altered as well. What's
important to keep in mind that each time a call
occurs the address
of the next instruction is pushed onto the stack, and ret
will
always continue execution from the address that is stored on the top of the
stack. Hopefully I have confused you enough, so here is a working example:
call MakeArray ; calling the subroutine presented in the first example ld a,(Numbers) ; loading the first element into A (i. e. 2) ld c,(hl) ; if everything went right, C will hold 100 after this
To be short: the call
pushes the address of the ld
a,(Numbers)
instruction on the top of the stack and jumps to
MakeArray. MakeArray, as we know, fills the 10 bytes
beginning at Numbers with the first 10 even numbers. When the
program reaches the ret
I put at the end of the subroutine,
the return address will be retrieved from the stack. After returning, we
continue from the address saved by call
. As I explained above,
C should be loaded with 100, because the loop in the subroutine
causes HL to point at that value.
To ease our lives, both call
and ret
can be used
with conditions in the very same manner as absolute jumps, i. e. all
the eight conditions can be investigated. If you write for instance call
c,Label
, the call only occurs if the carry is set. Similarly,
ret z
will only return if the zero flag is set. This
way the subroutines can be combined with conditions in a compact way.
Finally, here is a bit advanced example. With the help of call
,
you can also determine the value of PC:
call NextInstr ; calling the NEXT instruction NextInstr: ; the label we jump to pop hl ; popping the address of the instruction into HL
This is equivalent to the otherwise impossible ld hl,pc. Although
there is no ret
, the code will not cause any problem, because
the only important thing, the stack is properly handled. I just showed this
example to you in order to demonstrate again the freedom assembly gives to
you.