By Petter Holmberg
More assembly: Served Fresh every month...
In the previous part of this tutorial, I showed you how to use my program Absolute Assembly - with a little help from Microsoft's DEBUG - to make an assembly routine run in QBASIC. Using that knowledge, we can now start writing assembly pre and see it run. In order to do that we need to know more assembly instructions, so that's what this part mainly is about. When you've read through it you will have enough knowledge to start making your own experiments. But first, let's repeat the important parts from the last time!
I also introduced a sequence of four assembly instructions that you will use a lot in the future:
PUSH BP MOV BP, SP POP BP RETF
They will be the base for many of the assembly routines that you will write,
and you will soon understand why.
ADD Destination, source
SUB destination, source
Just like with MOV, the destination is where the result of the operation is placed. It can be a register or a pointer to a memory address. The source can be a register, a memory pointer or a direct value. Let's consider an example: If you would like to see the result of a subtraction of 4 from the value 5 in the AX register, you could test it like this:
MOV AX, 5 SUB AX, 4
The number 5 gets into the AX register with MOV, and then it's subtracted by 4 using SUB. The result, 1, will be in the AX register. Another example: Let's imagine you want to add 2 to the byte value at memory address 3:3, and put the answer in the CH register. Then you could write:
MOV BX, 3 MOV DS, BX MOV SI, 3 MOV CH, (SI) ADD CH, 2As explained in the last part of this tutorial, the first four lines are an example of reading a byte from the memory and putting it into a register. The ADD instruction then adds 2 to the value located in CH.
There are also two special case instructions for additions and subtractions. If you only want to add or subtract the number 1 from a value, you can use the instructions INC or DEC (short for Increase and Decrease). These instructions are performed faster by the processor than ADD and SUB, at least by older processors. They are also very easy to use. They only take one argument: The destination. It's recommended that you use INC X/DEC X instead of ADD X, 1/DEC X, 1. An example: You want to increase the value in the AL register with 1. The you just type:
INC AXCould it be any easier?
There are more things you probably want to do. How about multiplications and divisions? Well, you can certainly do that too, using the instructions MUL and DIV. They are a little harder to use though. The MUL instruction only takes one operand, like this:
The source cannot be a direct number, it has to be either a register or a memory pointer. If the source value is an 8 bit number, it will be multiplied with the value in the AL register, and the answer gets into the AX register. This is because the product of two 8 bit numbers can be 16 bits long. For example, suppose we want to multiply 100 with 200. We can do that like this:
MOV AL, 100 MOV DL, 200 MUL DL
The result, 20000, will fill up the whole AX register since it wouldn't fit into one byte. What about 16 bit numbers then? Well, since DEBUG cannot handle 32 bit registers, the solution looks somewhat different. If we use a 16 bit number as source, it is multiplied with the whole AX register and the result goes into DX:AX. That means that the high 16 bits goes into DX and the low 16 bits goes into AX. In many cases you, the programmer, knows that a multiplication won't have a product that exceeds 16 bits, so you can ignore the DX register. But if you have an important number in the DX register before the multiplication it will be lost anyway. Division is similar to multiplication. The syntax for DIV is:
Like with MUL, the source must be a register or a memory pointer, not a direct value. If the value is 8 bits, the whole 16 bit number in AX is divided by the source. The quotient of the division goes into AL and the remainder into AH. If the value is 16 bits, the number in the DX:AX register pair is divided by the source and the quotient goes into AX and the remainder into DX. This tells us that DIV can only perform integer division, like the \ sign in QBASIC, which is unfortunately true. For the sake of clarity, let's make an example: We want to divide 5 by 2, which would result in a quotinent of 2 and a remainder of 1. Let's try it:
MOV AL, 5 MOV DL, 2 DIV DL
Now the number 2 would be in AL and the number 1 in AH. As you can see, multiplications and divisions are trickier than additions and subtractions. Multiplications and divisions are also very slow compared to additions and subtractions. If you want to make a really fast assembly routine, try to avoid MUL and DIV as much as possible. One of the tricks to do that will be showed later.
There's also another thing about multiplications and divisions I have to tell you about. The MUL and DIV instruction only works with unsigned values, which means values that are positive. If you want to do multiplications or divisions with signed values, values that are negative, you must instead use the instructions IMUL and IDIV. They work with both positive and negative numbers, and works in the same way as MUL and DIV. But if you know that you only have positive values, use MUL and DIV for clarity purposes. That was the important stuff about arithmetic operations in assembler.
There are four arithmetic instructions in assembler: AND, OR, XOR and NOT. They have the same names in QBASIC. The processor has no problems executing these instructions, because it works with logical instructions for almost everything it does. Basic operations like additions and subtractions are performed as a series of logical instructions within the small semiconductive transistors in the CPU. Executing logical assembly instructions are therefore also one of the fastest operations the CPU can perform. Let's begin exploring These instructions!
Logical instructions doesn't treat the numbers in the computer as numbers, they operate on the individual bits in the number. So you won't understand the logical instructions if you look at what happens to the numbers you pass to them. You must study the individual bits.
The first logical instruction we will look at is NOT. It's a little different from the other three, and it's also the easiest to understand. NOT inverts all the bits in a number. So if you perform a NOT on the binary 8 bit number 00001111, you will get the result 11110000. If you NOT the number 10101100, you get 01010011 and so forth. The NOT instruction only takes one argument:
The destination can be a register or a memory pointer. The other three instructions, AND, OR and XOR, takes two arguments:
AND destination, source OR destination, source XOR destination, source
AND works like this: The destination value is compared bitwise against the source value, and only in the case where both corresponding bits are 1, the result will be 1. Otherwise it will be 0. So if you AND the value 11110000 with 10101010, you will get the result 10100000. 11001100 AND 10101010 will give the result 10001000 and so forth. The name of the instruction can be used as a remainder of its use: If bit A AND bit B is a 1, the result will be 1. Otherwise it will be 0.
OR is similar to AND, but it works in the opposite way. If both bits are 0, the result will be 0. Otherwise it will be 1. Or you can also say that if at least one of the bits is 1, the result will be 1. 11110000 OR 10101010 is therefore 11111010, and 11001100 OR 10101010 is 11101110. If you want to use the name as a remainder, you can say that if bit a OR bit b is a 1, the result will be 1. Otherwise it will be 0.
XOR is perhaps the most interesting logical instruction. The names stands for eXclusive OR. It works like OR except for one detail: If both bits are 1, the result is 0. You can also say that the two compared bits must be different if the result should be a 1. 11110000 XOR 10101010 is therefore 01011010, and 11001100 XOR 10101010 is 01100110. If this has made you dizzy, we better summarize the four logical instructions in a table:
AND: ----------- 0 AND 0 = 0 0 AND 1 = 0 1 AND 0 = 0 1 AND 1 = 1 OR: ----------- 0 OR 0 = 0 O OR 1 = 1 1 OR 0 = 1 1 OR 1 = 1 XOR: ----------- 0 XOR 0 = 0 O XOR 1 = 1 1 XOR 0 = 1 1 XOR 1 = 0 NOT: ----------- NOT 0 = 1 NOT 1 = 0
Now, what use can we have for logical instructions then? Well, there are several neat things you can do with them. AND can be used as a filter if you only want to read special bits in a number. For example, if you have a number in the AL register and you want to take away the four highest bits in the number. Then you only AND it with the binary number 00001111. Let's suppose the number in AL was 10010011. The result will then be 00000011. Notice how the four high bits have been filtered away. If you check the table above you can probably figure out why this works.
We covered the binary numbering system in part 1 of this tutorial, and if you remember it, you know that the bits in a binary number are "worth" 1, 2, 4, 8, 16, 32, 64, 128 and so forth if you count from right to left. Notice how only one of these numbers are odd? That's right: the first one. This implies that if a number is odd, it MUST have the binary digit worth 1 set. With a little help from AND, you can then test if any number is odd. Let's test the number 5. 5 in binary is 00000101. If we AND this with 00001111, we get the result 00000001. The result is 1, so 5 must be odd. If we test 10 instead, we get: 00001010 AND 00000001 = 00000000. The result is 0, so 10 must be an even number. Test this in QBASIC!
XOR also have some nice uses. One of the most common ones uses the fact that if both bits compared with XOR are the same, the resulting bit is 0. This means that if you compare a number against itself, the result must always be 0 no matter what the number is, because all bits always are the same. For example, 11001100 XOR 11000011 = 00000000. This works no matter what number you use. Remember what I said about the speed of logical instructions? These two facts together suggests that:
XOR AL, AL
is faster than:
MOV AL, 0
And that is certainly true! At least with older processors. This trick is often used by assembly programmers. Rather than just setting a register to 0, you XOR the register with itself, thus getting the same result faster. XOR can also be used for very simple data encryption. Take any number and XOR it with a number X. You will probably get a result that makes no sence. If you then XOR it again with the same number X, you get the original number back! Cool, huh? Let's test it: We have the top secret number 10011011. Now we use the secret pre key 10110110 to encrypt it, using XOR. The result is 00101101. you can't se any connection to the original number, can you? Then we "unlock" the number with our pre key and XOR again. The result is now 10011011. Wow! Our secret number is back! Of course you won't see Pentagon using this not too sophisticated encryption scheme for their secret documents, but maybe you'll have some private use for it? Test this in QBASIC too and make sure I'm not lying to you!
Shift and rotation opertations:
I'll begin by explaining shifts. Shift instructions are not too hard to understand. Their purpose is to move all the bits in a number a certain amount of bit positions in a certain direction. There are two basic shift instructions SHL and SHR, short for Shift Left and Shift Right. If you have the binary number 00111001 and shift it left one position, you get the number 01110010. If you shift it right one position, you get the number 00011100. Get it? It's like taking away one bit at one end of the number, move all the others one step to fill up the hole, and put a 0 in the empty place at the other end.
The shift instructions are not too hard to use. The basic syntax for the two shift instructions are:
SHL source, count SHR source, count
The source can be a register or a memory pointer. The count value tells the CPU the amount of bits to shift. Here comes a little quirk though: The 8086 processor only allowed the count to be the number 1 or the contents of the CL register. The instruction SHL AL, 2 was therefore not valid. Later Intel processors can use any direct number. BEBUG however, only supports the basic 8086/8088 assembly instructions, so we must use the CL register if we want to shift a number more than one bit position. If we want to shift the contents of the BH register four steps to the right, we must then type:
MOV CL, 4 SHR BH, CL
What can we use shift instructions to then? Well, there's a very neat use for it that often comes in handy. Remember what I said about the slowness of multiplications and divisions? Well, certain multiplications and divisions can be done with shift instructions, making them even faster than additions and subtractions! In the binary world, you double the value of a bit if you move it one step to the left. The binary number 100 is two times as big as the number 10 and so forth. Therefore, shifting a number one step to the left is the same as multiplying it by two. If you shift it two steps you multilply it by four and so on. So if you want to multiply a number with 2^x, for example 128, you can use a SHL instruction instead. Let's try it! Suppose we want to multiply the number 10 with 8, you can either type the slow:
MOV AL, 10 MOV DL, 8 MUL DL
Or, you could type the much faster:
MOV AL, 10 MOV CL, 3 SHL AL, CL
Get it? The same goes for division, but then you use SHR instead. The only thing you have to watch out for is that the shift instructions may push some ones over "the edge" of the register, and then you will get an incorrect answer. So make sure the numbers you want to multiply doesn't get bigger than 8 bits. Or 16 bits if you're dealing with bigger numbers.
Rotations works like shifts, but they don't throw away any bits. The bits that disappears on one side of the number, are put on the other side. So if you rotate the number 10100110 two steps to the left, the result will be 10011010. The 10 that was pushed away at the left edge, are moved to the new, empty spaces at the right edge. A rotation of a byte eight steps in any direction would not modify the byte at all because all the bits would be rotated to the same positions that they were at from the beginning. The two instructions needed are ROL and ROR, short for Rotate Left and Rotate Right. The basic syntax is exactly the same as for SHL and SHR, and the count value can be either 1 or the contents of the CL register. If you want to rotate the byte in AL four steps to the right, you simply type:
MOV CL, 4 ROR AL
Passing values between QBASIC and the assembly program:
Okay: Now We've been going through 16 basic assembly instructions: ADD, SUB, INC, DEC, MUL, IMUL, DIV, IDIV, AND, OR, XOR, NOT, SHL, SHR, ROL and ROR. With these at your hand you can do many things. Now you probably want to test this in reality, using QBASIC. But there's no way you can watch the results of these calculations in QBASIC yet. Therefore, we must learn how to pass variables between QBASIC and assembly routines.
Last time, you saw how an assembly routine could be called with the CALL ABSOLUTE keyword. The syntax for CALL ABSOLUTE is:
Where offset is the offset address of the string/array that contains the machine pre you want to execute. The segment address must be set with a DEF SEG before the call. If you want to pass variables to the routine, you do so by putting them before the offset specification. This is what I mean: Suppose you want to pass the integer variables a% and b% to the assembly routine. You then type:
CALL ABSOLUTE(a%, b%, offset%)
This will ensure that the a% and b% variables are passed to the assembly routine. Exactly how this works will be explained in just a minute. First I must point out that CALL ABSOLUTE can only pass variables of the type INTEGER. LONG variables, SINGLE and DOUBLE variables, strings, user data type variables and arrays can NOT be passed to your assembly routines. If you have the need to do that you must instead pass two integer variables, describing the segment and offset address of the variable you really want to send.
How can you read the variables in your assembly pre then? Now it's time to look back on the four golden lines that were presented in the last part:
PUSH BP MOV BP, SP POP BP RETFWhen QBASIC executes a CALL ABSOLUTE instruction, the segment and offset address of the next QBASIC instruction is pushed onto the stack. That is 4 bytes. If you add variables to the CALL ABSOLUTE line, these are also pushed onto the stack, BEFORE the BASIC segment/offset pair. They are pushed in the order they appear inside the parantheses after the CALL ABSOLUTE statement. Now, the first assembly instruction above pushes the BP register onto the stack. BP tells the program on what offset the bottom of the stack is located. Then, the contents of SP is copied into BP. SP tells the program where the top of the stack is. Now the computer thinks that the stack starts in the end. This comes in handy, because it is possible to fetch a byte from the memory like this:
MOV BX, (BP)
If we use the BX register as destination, we can get the two bytes located at the memory position SS:BP. The SS register is a new register to you. It contains the segment address of the stack. It's rarely used by assembly programmers though. Anyway, it is also possible to get the two bytes at a position RELATIVE to SS:BP. Let's say we want the word (a two byte number is called a word in assembler) 5 bytes above SS:BP. We then type:
MOV BX, (BP + 5)
If we want the word 8 bytes below SS:BP, we type:
MOV BX, (BP - 8)
Now, remember that I said last time that the stack is like a stack of plates turned upside-down, i.e. the stack grows DOWNWARDS as you push values onto it. The values already pushed on the stack are thus at higher memory adresses than the current SP value. Since we put the contents of the SP register in BP, we can now find our variables by searching at memory addresses above SP:BP. The current value of BP points at the value that was last pushed onto the stack. We just pushed the original BP value, so that's what We'll find that. At BP+2, we'll find the next value. Before QBASIC gave our assembly routine the control of the program flow, CALL ABSOLUTE pushed the segment and offset of the next QBASIC instruction onto the stack, so on BP+2 and BP+4 you'll find these values, numbers that are of no interest to us. It is after that, at BP+6 and above, that we'll find our variables. If you wrote:
CALL ABSOLUTE(myvariable%, offset%)
myvariable% would be located at BP+6. If you wrote:
CALL ABSOLUTE(x%, Y%, z%, offset%)
x% would be at BP+10, y% at BP+8 and z% at BP+6. This may sound confusing, but QBASIC pushes the variables in the order they appear inside the parantheses. x% will therefore have the highest memory address in the stack. The last variable will always be the closest one to BP. If you use PUSH in your own assembly pre, you can read them without using POP in the same way you read the QBASIC variables. The first value you push will be at BP-2, the second one at BP-4 and so on.
Let's suppose you want to get the value of the variable x% into the AX register. Then you only type:
PUSH BP MOV BP, SP MOV BX, (BP + 6) MOV AX, BX
As if our problems weren't enough, QBASIC hasn't passed the value of x% to the stack. The number at BP+6 is the OFFSET addres of the x% variable in the memory. This makes things a little harder to grasp, but as you'll see, this can be very useful.
But first, let's solve this new problem in QBASIC. If you use the QBASIC BYVAL clause before the variable, your problems will be solved. This is what I mean:
CALL ABSOLUTE(BYVAL x%, offset%)
Now, QBASIC won't push the offset of the variable x% on the stack. It now pushes the actual value of x%. Now you can use these lines to get the value of x% in the AX register:
PUSH BP MOV BP, SP MOV BX, (BP + 6) MOV AX, BX
Wow! Now we can pass variables from QBASIC to an assembly routine. Now we can
use ADD, OR, SHL or any other instruction to modify the values in cool ways.
Well, even if that's certainly true, we won't have much use for it if we
couldn't pass the modified values back to QBASIC again. How can we do that
then? Well, we need to know the memory addresses of the QBASIC variables
from within our assembly routine in order to change them. If we just let
QBASIC push the values of some variables onto the stack, we can read them,
but we cannot return any values, because we don't know where the variables
reside. This is the reason QBASIC as default pushes the offset of the
variables instead of their values. Let's try to solve the problem without
PUSH BP MOV BP, SP MOV BX, (BP + 6) MOV AX, (BX)
Now, the third line won't get the actual value of x%, but the offset address where x% can be found. BX now knows where the variable is. The last line doesn't just copy the value of BX like before. Now it gets the word located at the MEMORY POSITION pointed out by BX. Wow! That's the value that was in x% from the beginning! Are you getting dizzy yet? ;-) If you're confused, just read the last lines again and again until you get it.
Now you know how to get values of QBASIC variables into a registers both with and without BYVAL. When your assembly routine only needs to read values from QBASIC, you can use any of these two methods. I usually use BYVAL, Because it's easier to read the values from the assembly pre then. When you want to pass values back to QBASIC, you don't have any option: You cannot use BYVAL. Passing values to QBASIC is not any harder than to read them though. Let's imagine we want to do the opposite of the previous example of getting the value of the x% variable into AX: Getting the value of AX into x%, readable by QBASIC: You call the routine like before, but without BYVAL:
CALL ABSOLUTE(x%, offset%)
Then, you only need to type this in assembler:
PUSH BP MOV BP, SP MOV BX, (BP + 6) MOV (BX), AX
The only line that has been changed from the last example is the last one. Instead of getting the value at the offset of BX into AX, we now put the value of AX at the offset of BX. When the control has been returned to QBASIC again, you'll find that the x% variable has changed! Now the topic of variable passing is almost completed. Just a few more things:
First, let's return to the last two lines in the set of four lines that I showed you already in the last part of this tutorial:
POP BP RETF
When you've done what you wanted in your assembly routine, you must return to QBASIC properly. As we pushed the original value of BP before and changed it, we must change it back before we get back to QBASIC. The POP instruction ressurects the old value. Then comes the RETF instruction. RETF, short for Return Far, will POP back the segment and offset that CALL ABSOLUTE pushed onto the stack, and jump to that memory position. The control has returned to QBASIC! But when you passed variables to your assembly routine, CALL ABSOLUTE pushed them on the stack also. RETF alone won't clean up the mess you left in the stack and QBASIC will lock up your computer when it gets the wrong values from the stack. In order to fix things up, you must tell RETF to POP away the extra words that you put there by passing variables to your assembly pre. This is easy to do. Let's say you only passed one variable. One integer variable takes two bytes, so you change the RETF to RETF 2, and two extra bytes will be popped away into cyberspace! If you passed four variables, you must take away 2 * 4 = 8 bytes. RETF 8 fixes it! And finally: Remember that you can ONLY use BX when reading values relative to the offset of BP!
An example program:
CLS PRINT "This program adds two numbers together through an assembly routine." PRINT INPUT "Type the value of number 1: "; a% INPUT "Type the value of number 2: "; b% ' We'll put some assembly pre here later! CALL ABSOLUTE(a%, b%, c%, offset%) PRINT PRINT "The result of the addition is"; c%
Now we have the skeleton pre for our program. Let's start a text editor and write the assembly pre:
PUSH BP MOV BP, SP MOV BX, [BP+0A] MOV AX, (BX) MOV BX, (BP + 8) MOV CX, (BX) ADD AX, CX MOV BX, (BP + 6) MOV (BX), AX POP BP RETF 6
If you feel confused, here's some explanations:
In order to make this program run, you must now use Absolute Assembly like I showed you in the last part of this tutorial. Call the pre string add$, answer yes to the question about appending the destination file and answer no to the question about adding call absolute pre to the program, and then you'll get the following QBASIC program:
CLS PRINT "This program adds two numbers together through an assembly routine." PRINT INPUT "Type the value of number 1: "; a% INPUT "Type the value of number 2: "; b% ' We'll put some assembly pre here later! CALL ABSOLUTE(a%, b%, c%, offset%) PRINT PRINT "The result of the addition is"; c% ' ------ Created with Absolute Assembly 2.1 by Petter Holmberg, -97. ------- ' add$ = "" add$ = add$ + CHR$(&H55) ' PUSH BP add$ = add$ + CHR$(&H89) + CHR$(&HE5) ' MOV BP,SP add$ = add$ + CHR$(&H8B) + CHR$(&H5E) + CHR$(&HA) ' MOV BX,[BP+0A] add$ = add$ + CHR$(&H8B) + CHR$(&H7) ' MOV AX,[BX] add$ = add$ + CHR$(&H8B) + CHR$(&H5E) + CHR$(&H8) ' MOV BX,[BP+08] add$ = add$ + CHR$(&H8B) + CHR$(&HF) ' MOV CX,[BX] add$ = add$ + CHR$(&H1) + CHR$(&HC8) ' ADD AX,CX add$ = add$ + CHR$(&H8B) + CHR$(&H5E) + CHR$(&H6) ' MOV BX,[BP+06] add$ = add$ + CHR$(&H89) + CHR$(&H7) ' MOV [BX],AX add$ = add$ + CHR$(&H5D) ' POP BP add$ = add$ + CHR$(&HCA) + CHR$(&H6) + CHR$(&H0) ' RETF 0006 ' ------ Created with Absolute Assembly 2.1 by Petter Holmberg, -97. ------- '
Now, move up the pre declaration above the CALL ABSOLUTE line and add a DEF SEG to set the segment address of the string before the call, and you'll end up with this program:
CLS PRINT "This program adds two numbers together through an assembly routine." PRINT INPUT "Type the value of number 1: "; a% INPUT "Type the value of number 2: "; b% add$ = "" add$ = add$ + CHR$(&H55) ' PUSH BP add$ = add$ + CHR$(&H89) + CHR$(&HE5) ' MOV BP,SP add$ = add$ + CHR$(&H8B) + CHR$(&H5E) + CHR$(&HA) ' MOV BX,[BP+0A] add$ = add$ + CHR$(&H8B) + CHR$(&H7) ' MOV AX,[BX] add$ = add$ + CHR$(&H8B) + CHR$(&H5E) + CHR$(&H8) ' MOV BX,[BP+08] add$ = add$ + CHR$(&H8B) + CHR$(&HF) ' MOV CX,[BX] add$ = add$ + CHR$(&H1) + CHR$(&HC8) ' ADD AX,CX add$ = add$ + CHR$(&H8B) + CHR$(&H5E) + CHR$(&H6) ' MOV BX,[BP+06] add$ = add$ + CHR$(&H89) + CHR$(&H7) ' MOV [BX],AX add$ = add$ + CHR$(&H5D) ' POP BP add$ = add$ + CHR$(&HCA) + CHR$(&H6) + CHR$(&H0) ' RETF 0006 DEF SEG = VARSEG(add$) CALL ABSOLUTE(a%, b%, c%, SADD(add$)) DEF SEG PRINT PRINT "The result of the addition is"; c%
Notice how I exchanged the offset% variable in the CALL ABSOLUTE line with SADD directly. It's not necessary to put the offset in a variable before using it with CALL ABSOLUTE.
Test this program and you'll see that it works, at least when the numbers you add won't give a result that is above the 16 bit limit. This program can now serve as a base for more experiments. You can test all the instructions I've been presenting in this part of the tutorial with only small modifications of the assembly pre in this example. I encourage you to do so. It's the best way to learn how to use them.
Phew! That was all for this part of my assembly tutorial, the longest one so far. Now you know 20 assembly instructions: MOV, PUSH, POP, RETF, ADD, SUB, INC, DEC, MUL, IMUL, DIV, IDIV, AND, OR, XOR, NOT, SHL, SHR, ROL and ROR. You also know of 10 registers: AX, BX, CX, DX, DS, SI, DI, SS, BP and SP. There are more assembly instructions and registers to cover, but with the ones you master now, together with the knowledge on how to use CALL ABSOLUTE, you know enough to start writing some basic assembly routines yourself. There are many more things to know about assembly programming though. In the next part of this tutorial, we'll look at the possibilities to control the program flow, learn more about memory access, discover the extra features of Absolute Assembly, and finally, open the door into one of the most interesting parts of assembly programming in QBASIC, using an assembly instruction that will make you sit up for whole nights programming!
Until the next time, experiment with the new things I've presented in this part until you feel familiar with them. Now we're really getting somewhere!