Copy Link
Add to Bookmark
Report

Assembly Language for Veggies (And C programmers) Part 2

So, you've wound your way through part one, lashed out and bought the book and now you're about to leap into things in a big way, right?

OK... well here is where we make a bit of a start on things! We'll be looking first off at a simple routine that displays a number on the screen. sound simple? You Wait!

One of the more vital routines one uses from time to time [read all the time!] is a simple write number to screen routine. consider what is required here.. to take a number held wither in a register or memory and display it on the screen. simple? you think about the work required to do it and you'll start to understand just how hard it is to do...

Firstly, consider the biggest number you wish to write... if it's 8 bits or less (0 -255) then you can use one routine, whilst a 16 bit (0-65535) routine would be more versatile, BUT if you need to do really big numbers (like free space on a hard disk for example) then a 32 bit routine would be called for...

Perhaps the most common of all is the 16 bit routine....

It is coded thus:

WRITE_ASCII:         PUSH AX 
PUSH CX
PUSH DX
PUSH SI
MOV AX,DX
MOV SI,10
XOR CX,CX

NON_ZERO: XOR DX,DX
DIV SI
PUSH DX
INC CX
OR AX,AX
JNE NON_ZERO

WRITE_DIGIT_LOOP: POP DX
CALL WRITE_HEX_DIGIT
LOOP WRITE_DIGIT_LOOP

END_DECIMAL: POP SI
POP DX
POP CX
POP AX
RET

WRITE_HEX_DIGIT: PUSH DX
CMP DL,10
JAE HEX_LETTER
ADD DL,'0'
JMP SHORT WRITE_DIGIT

HEX_LETTER: ADD DL,'A'-10

WRITE_DIGIT: CALL PRT1
POP DX
RET


PRT1: PUSH AX
MOV AH,02
INT 021
POP AX
RET

That is one BIG routine..... the Pascal version of which would be :

program write_num 

Var
number : word;

begin
number := 5285; {Say}
write(number);
end.

We actually have three separate routines here, one called WRITE_ASCII which is the one we call, and two more subroutines which write_ascii calls itself. Can you name them? They are referenced by CALL instructions... They are write_hex_digit and prt1. write_hex_digit calls prt1 so we basically have a 3 level nested loop arrangement.

Looking complex yet?

Time to step into the code in more detail... When the routine is first called, the number to be displayed should be present in the DX register.

The first job our routine has to do is to convert the number in the register to decimal, and then into actual ASCII digits suitable for writing to the screen. Here is where we get tricky... We use a loop and some simple maths to derive the decimal equivalent, and the number of loops is equal to the number of digits in the final ascii number.

The first instruction you see is the PUSH command. What this does is put the contents of the named register upon the stack. In effect, this saves the contents in a more or less indestructable area for later recall.. One can safely fiddle with the contents of the register, safe in the knowledge that it's original contents can be recalled with the use of the POP instruction. note that PUSH's and POP's must be done in order... if one does a push AX, push bx, then later does a pop ax, pop bx, the contents of ax and bx will be exchanged... it's the same as any other stack operation, so keep things in order!!

We save the AX, CX, DX snd SI registers.... WHY?? Think about this for a second or two.. We save these registers because we modify them in our routine! now, why is this important?? Because the main program may too be using them!

If you find this hard to understand, follow this analogy... you have a Car radio in your car, tuned to your favourite station (MMM-FM 105!) .. you take it to the garage for a service. When you get it back you'd expect it to still be on MMM wouldn't you? Of course!! Now the average garage jock likes FOX better, so he sticks it on fox. If he returned the car to you still on fox, you'd be upset and annoyed.... if he was kind enough to return it to MMM before giving it back, you'd be none the wizer and go about your way happy as larry as the saying goes.

To save our main program from being upset (read crashed) by alteration of it's registers, our routine saves them on entry and (as you'll soon see) recalls them just before exiting. Clear?

OK... now the register-to-decimal routine... involves the loop from NON_ZERO: to the JNE instruction BEFORE the WRITE_DIGIT_LOOP: label. before entry, some registers are setup... AX is loaded with our initial value. (DX still holds this as well) SI is loaded with 10 decimal and cx is made equal to zero (boolean logic dictates that an xor of any number with itself results in 0) by the XOR cx,cx instruction.

Firstly DX is zeroed.
AX is ten divided by SI (10) ... the result is a decimal digit in AX (The quotient) and a remainder in AX.

This bit of math goes like this:

Let's say you called the program with 36h in DX. 36h goes into AX, and 10d into CX. 36 hex is the same as 54 decimal. 36h(ex) divided by 10(decimal) equals 5 in AX and 4 in DX.

DX now holds our lest significant digit (the 1's unit of you like) - the number 4. DX is saved on the stack for use in a second.

CX is incremented from 0 to one. this counts the fact that 1 digit has been saved so far.

the or AX,AX basically checks to see if ax is zero or not. the JNE stands for Jump not equal.. this decodes into if the previous math operation (or ax,ax) worked out to be equal(ie zero!) then go to the next instruction. if it was Not Equal, then go to the label NON_ZERO. it would go to NON_ZERO because AX has a 5 in it, and a 0 is needed to not jump!

In the next loop we would then see the remainder of 5 in AX still, and again the same thing happens...

6 divided by 10 is a result of 0 (into AX) and a remainder (ie 0.6) into DX.

Again DX is saved on the stack, and CX in incremented. We now have 2 elements on the stack, and CX counts them. Ax is now equal to 0, so an or ax,ax proves true and the JNE results in a no jump, and the program reaches the WRITE_DIGIT_LOOP label for the first time.

At this point we have the hex number stored on the stack in MSB --> LSB format (just right for writing to screen!) and a count of the number of digits in CX.

now, here's a trick...

There's some special instructions built into the 80xxx series that make use of the CX register... One of them is the LOOP command.

LOOP does this: Decrements CX by 1. if CX=0, then it goes to the next instruction. If CX is not one, however, it jumps to the label (in this case WRITE_HEX_DIGIT) coded next to it.

so there's a loop of 3 instructions to be done CX times. what happens is DX is popped off the stack, (that's the MSB, the number 5) and the subroutine WRITE_HEX_DIGIT is called. in the next loop, the number 4 appears in DX and the same routine is called... note that the LOOP command actually decrements CX by one, saving us the job of doing it! Quite neat but a trap for young programmers... At this point, CX will be equal to 0.

2 elements have been popped off the stack, so the stack is back at the point where we did the PUSH SI..

You may have guessed, write_hex_digit actually does the displaying, and we'll look at that in a tic.. but at this point the routine has done it's job and it's time to return to the calling program. We restore all the registers we saved with the POP commands.. (note how they go in exact reverse order to the PUSH's) then go back to our caller with a RET (Short for RETURN [Just like BASIC!])

That's all there is to the main loop!

Now, on to the displaying a digit bit...

this should be fairly clear to you.. it's name indicates that it is also capable of displaying HEX letters as well... we don't need to worry about that however, as all our numbers will be between 0 and 9...

the number comes into the routine in the DX register (I like using DX for number passing :-) )

See if you can guess how it works... The numbers 0-9 appear in the ASCII table sequentially starting at number 30. The real value of '0' is 30.. got it??

If you want to work out the hex bit, JAE stands for jump if above or equal, whilst the ADD dl,'a'-10 must pick the letter, right?

It is important to realize that all this time we've been worrying on the 16 bit DX register, but we've only been concerned with the bottom 4 bits!! a bit of a waste, possibly, but it works and is just as functional as using the dl register. I've randomly mixed reference to dl and dx knowing that dl works on the BOTTOM (Remember l for lower, h for higher!) 8 bits of dx.

The contents of dl will now be 30+the original number, which is ascii for the digit 0-9. All that remains is to actually throw this digit onto the screen.

That's what PRT1 does..

By now, port1 should be self explanatory. Get a pen. Get paper. Scribble down how YOU think prt1 works. Take 2 minutes maximum. THEN and only THEN look at the next 2 lines... you may use the book I recommended to look up the INT 021 function (in fact, I insist you do!)

ANSWER: We use AH to indicate which INT 021 function we want, so we load it with 02, after saving it's original value on the stack so that when we return the calling program will have all registers the same. The calling program provides the ASCII digit in DL.. the routine expects itt here, so we simply call int 021, restore AX and return to the caller. Simple, eh! did you figure it out?

you've just learnt some valuable instructions and methods used in ASM. the CALL -- RET sequence is the same as BASIC's gosub -- return sequence, the loop function repeats CX times, you can temporarily save registers on the stack..

Add anything else you feel you're getting used to... Quite a bit, isn't it! Don't say I didn't warn you!

If you wish to test this routine for yourself (and I suggest you play with it for a while) then code this is A86:

BEGIN:   mov DX,<stick in a hex value between 00000h and 0ffffh) 
call WRITE_ASCII
int 020

; at this point type in all hte code presented above as it appears. pay no
; regard to case - the assembler is not case sensitive and I do it for clarity
; only!!

WRITE_ASCII:
.
.
.
.

PRT1:
.
.
.
.
RET

Assemble this with a random value. Calculate the decimal version (use a calculator or something!) and see if it works or not....

Hint:  FFFF = 65535     00FF = 256     08C4 = 2244    Honest!!

each time you run it, the number in DX will be printed to the screen.. If you feel confidant, fiddle round and see the effect of changing various bits.. the worst you can do is lock up the machine!

as a challenge, save the CX register somewhere, and display it after the number to count the number of digits in the number!

A hint: Use the prt1 routine tp rint a space character to separate the 2 numbers, and don't use PUSH to save CX... WHY NOT??

Some comments on structured programming

You should all know what that is all about....

If not, it basicaly says that you divide your chunks of code up into sections that only do one, fairly specific job, then call these chunks as you need. Each chunk (Well, subroutine or procedure are other names for them, but I'll use chunks! [just to be different]) should have one entry point (the bit you CALL) and one exit point (IE No JMP's into other chunks, no coding multiple RET's into the one chunk) and should not upset the operation of any other chunk (the reason for the saving registers on the stack)

Examine the above code.. you see the first chunk converts raw hex to a numerical digit. A second chunk is called to do the conversion to ASCII, and a third chunk to display it on screen. Each chunk is independent (save the actual parameters passed to it and it's output) and can operate from any number of calling routines... the WRITE_HEX_DIGIT routine could be called directly with a hex digit in DL and would print a HEX digit to the screen.

This is the very essence of modular (Structured) programming. If one does not follow this system (in any language, but most importantly in ASM where you code tends to become impossible to understand very easily) then things soon become a real shit mess that even you cannot follow, much less debug!

Shit mess code that has evolved without thought to structure is often referred to as SPAGHETTI CODING - because it's like trying to sort out a bowl of said material - near impossible to find the true start and end for all the straggly bits!

Libraries of usefull routines are soon built up from this (I use the above routine ALL the time in my text based programs) that you know you can drop in any program when required and not have to change anything to accommodate it.. I cannot stress enough how much simpler a set of worked out, debugged, easy to call routines makes your programming life!


--------------------


That just about draws together the end of lesson 2 unfortunately... It's grown fair bit larger than I'd first hoped, but who cares, I think if I made it any smaller, you'd loose track of it...

Next lesson : using A86 and D86 together, more programming examples and a look at the INT 021 functions...

In the meantime, read you book from cover to cover. digest as much as you can, and what you can't, don't worry about... you should be beginning to understand the register calling convention used with INT 021 - so I suggest you read up on that a bit! I'll have a lot to say about MS-DOS and it's problems, quirks, hassles etc.... as well as some undocumented stuff as time goes on...

Until then, good coding!

.\\erlin
8/91

← previous
next →
loading
sending ...
New to Neperos ? Sign Up for free
download Neperos App from Google Play
install Neperos as PWA

Let's discover also

Recent Articles

Recent Comments

Neperos cookies
This website uses cookies to store your preferences and improve the service. Cookies authorization will allow me and / or my partners to process personal data such as browsing behaviour.

By pressing OK you agree to the Terms of Service and acknowledge the Privacy Policy

By pressing REJECT you will be able to continue to use Neperos (like read articles or write comments) but some important cookies will not be set. This may affect certain features and functions of the platform.
OK
REJECT