Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Assembly Language Step by Step 1992

.pdf
Скачиваний:
143
Добавлен:
17.08.2013
Размер:
7.98 Mб
Скачать

exotic in the Seventies.

The program grew over a period of a week to about 600 lines of squirmy little APL symbols. I got it to work, and it worked fine—as long as I didn't try to format a column that was more than 64 characters wide. Then everything came out scrambled.

Whoops. I printed the whole thing out and sat down to do some serious debugging. Then I realized with a feeling of sinking horror that, having finished the last part of the program, I had no idea how the first part worked.

The APL symbol set was only part of the problem. I soon came to realize that the most important mistake I had made was writing the whole thing as one 600-line monolithic block of code lines. There were no functional divisions, nothing to indicate what any 10line portion of the code was trying to accomplish.

The Martians had won. I did the only thing possible: I scrapped it. And I settled for ragged margins in my text.

8.2 Boxes Within Boxes

This sounds like Eastern mysticism, but it's just an observation from life: Within any action is a host of smaller actions. Look inside your common activities. When you "brush your teeth," what you're actually doing is:

Picking up your toothpaste tube

Unscrewing the cap

Placing the cap on the sink counter

Picking up your toothbrush

Squeezing toothpaste onto the brush from the middle of the tube

Putting your toothbrush into your mouth

Working the brush back and forth vigorously

and so on. The original list went the entire page. When you brush your teeth, you perform every one of those actions. However, when you think about brushing your teeth, you don't consciously run through each action on the list. You bring to mind the simple concept "brushing teeth."

Furthermore, when you think about what's behind the action we call "getting up in the morning," you might assemble a list of activities like this:

Shut off the clock radio

Climb out of bed

Put on your robe

Let the dogs out

Make breakfast

Brush your teeth

Shave

Get dressed

Brushing your teeth is on the list, but within the "brushing your teeth" activity a whole list of smaller actions exist. The same can be said for most of the activities collectively called "getting up in the morning." How many individual actions, for example, does it take to put a reasonable breakfast together? And yet in one small, if sweeping, phrase, "getting up in the morning," you embrace that whole host of small and even smaller actions without having to laboriously trace through each one.

What I'm describing is the "Chinese boxes" method of fighting complexity. Getting up in the morning involves hundreds of little actions, so we divide the mass up into coherent chunks and set the chunks into little conceptual boxes. "Making breakfast" is in one box, "brushing teeth" is in another, and so on. Closer inspection of any box shows that its contents can also be divided into numerous boxes, and those smaller boxes into even smaller boxes.

This process doesn't (and can't) go on forever, but it should go on as long as it needs to in order to satisfy this criterion: the contents of any one box should be understandable with only a little scrutiny. No single box should contain anything so subtle or large and involved that it takes hours of hair pulling to figure it out.

Procedures as Boxes for Code

The mistake I made in writing my APL text formatter is that I threw the whole collection of 600 lines of APL code into one huge box marked "text formatter." While I was writing it, I should have been keeping my eyes open for sequences of code statements that worked together at some identifiable task. When I spotted such sequences, I should have set them off as procedures. Each sequence would then have a name that would provide a memory-tag for the sequence's function. If it took ten statements to justify a line of text, those ten statements should have been named JustifyLine, and so on.

Xerox's legendary APL programmer, Jim Dunn, later told me that I shouldn't ever write a procedure that wouldn't fit on a single 25-line terminal screen "More than 25 lines and you're doing too much in one procedure. Split it up, " he said. Whenever I worked in APL after that, I adhered to that rather sage rule of thumb. The Martians still struck from time to time, but when they did, it was no longer a total loss.

All computer languages have procedures of one sort or another, and assembly language

is no exception. You may recall from the previous chapter that the main program is in fact a procedure, and the only thing setting it apart as the main program is the fact that its name is specified after the END directive.

Your assembly-language program may have numerous procedures. There's no limit to the number of procedures, as long as the total number of bytes of code does not exceed 65,536 (one segment). Other complications arise at that point, but nothing that can't be worked around.

But that's a lot of code. You needn't worry for awhile, and certainly not while you're just learning assembly language. (I won't be treating the creation of multiple code segments in this book.) In the meantime, let's take a look at the "Eat at Joe's" program, expanded a little to include a couple of procedures:

EAT2.ASM does about the same thing as EAT.ASM. It prints a two-line slogan, and that's all. The way the two lines of the slogan are displayed, however, bears examination:

lea DX , Eat1 call Writeln

Here's a new instruction: CALL. The label Writeln refers to a procedure. As you might have gathered, (especially if you've programmed in an older language like BASIC or FORTRAN) CALL Writeln simply tells the CPU to go off and execute a procedure named Writeln.

The means by which CALL operates may sound familiar: CALL first pushes the address of the next instruction after itself onto the stack. Then CALL transfers execution to the address represented by the name of the procedure. The instructions contained in the procedure execute. Finally, the procedure is terminated by CALL'S alter ego: RET (for RETurn.) The RET instruction pops the address off the top of the stack and transfers execution to that address. Since the address pushed was the address of the first instruction after the CALL instruction, execution continues as though CALL had not changed the flow of instruction execution at all.

See Figure 8.1.

This should remind you strongly of how software interrupts work. The main difference is that the caller does know the exact address of the routine it wishes to call. Apart from that, it's very close to being the same process. (Also note that RET and IRET are not interchangeable. CALL works with RET just as INT works with IRET. Don't get those return instructions confused!)

The structure of a procedure is simple and easy to understand. Look at the Write procedure from EAT2.ASM:

Write

PROC

 

 

mov AH, 09H

;

Select DOS service 9: Print String

int 21H

 

;

Call DOS

ret

 

; Return to the caller

Write

ENDP

 

 

The important points are these: a procedure must be bracketed by the PROC/ ENDP directives, preceded in both cases by the name of the procedure. Also, somewhere within the procedure, and certainly as the last instruction in the procedure, there must be at least one RET instruction.

The RET instruction is the only way that execution can get back to the caller of the procedure. As I mentioned above, there can be more than one RET instruction in a procedure, although your procedures will be easier to read and understand if there is only one. Using more than one RET instruction requires the use of JMP (JuMP) instructions, which I haven't covered yet but will shortly in Chapter 9.

Calls Within Calls

Within a procedure you can do anything that you can do within the main program. This includes calling other procedures from within a procedure. Even something as simple as EAT2.ASM does that. Look at the Writeln procedure:

Writeln

 

PROC

 

call

Write

 

; Display the string proper through Write

lea DX , CRLF

; Load address of newline string to DS:DX

call

Write

 

; Display the newline string through Write

ret

 

 

; Return to the caller

Writeln

 

ENDP

 

The Writeln procedure displays a string on your screen, and then returns the cursor to the left margin of the following screen line. This procedure is actually two distinct activities, and Writeln very economically uses a mechanism that already exists: the Write procedure. The first thing that Writeln does is call Write to display the string on the screen. Remember that the caller loaded the address of the string to be displayed into DX before calling Writeln. Nothing has disturbed DX, so Writeln can immediately call Write, which will fetch the address from DX and display the string on the screen. Returning the cursor is done by displaying the newline sequence, which is stored in a string named CRLF. Writeln again uses Write to display CRLF. Once that is done, the work is finished, and Writeln executes a RET instruction to return execution to the caller. Calling procedures from within procedures requires you to pay attention to one thing: stack space. Remember that each procedure call pushes a return address onto the stack. This return address is not removed from the stack until the RET instruction for that procedure executes. If you execute another CALL instruction before returning from a procedure, the second CALL instruction pushes another return address onto the stack. If you keep calling procedures from within procedures, one return address will pile up on the stack for each CALL until you start returning from all those nested procedures.

If you run out of stack space, your program will crash and return to DOS, possibly taking DOS and the machine with it. This is why you should take care to allocate considerably more stack space than you think you might ever conceivably need. EAT2.ASM at most uses four bytes of stack space, because it nests procedure calls two deep—Writeln within itself calls Write. Nonetheless, I allocated 512 bytes of stack to get you in the habit of not being stingy with stack space. Obviously you won't always be able to keep a 128-to-l ratio of "need to have," but consider 512 bytes a minimum for stack space allocation. If you need more, allocate it. Don't forget that there is only one stack in the system, and while your program is running, DOS and the BIOS and any active TSRs may well be using the same stack. If they fill it, you'll go down with the system—so leave room!

When to Make Something a Procedure

The single most important purpose of procedures is to manage complexity in your programs by replacing a sequence of machine instructions with a descrip-tive name. While this might seem to be overkill in the case of the Write procedure, which contains only two instructions apart from the structurally-necessary RET instruction.

True. But—the Writeln procedure hides two separate calls to Write behind itself: one to display the string, and another to return the cursor to the left margin of the next line.

If you look back to EAT.ASM, you'll see that it took six instructions to display both the slogan string and the newline string. What took six instructions now takes two, thanks to Writeln. Furthermore, the name Writeln is more readable and descriptive of what the sequence of six instructions do than the sequence of six instructions themselves. Extremely simple procedures like Write don't themselves hide a great deal of complexity. They do give certain actions descriptive names, which is valuable in itself. They also provide basic building blocks for the creation of larger and more powerful procedures, as we'll see later on.

In general, when looking for some action to turn into a procedure, see what actions tend to happen a lot in a program. Most programs spend a lot of time displaying things on the screen. Procedures like Write and Writeln become general-purpose tools that may be used all over your programs. Fur-thermore, once you've written and tested them, they may be reused in future programs as well.

Try to look ahead to your future programming tasks and create procedures of general usefulness. (Tool-building is a very good way to hone your assembly language skills.) I'll be showing you more of this type of procedure by way of examples as we continue.

On the other hand, a short sequence (five to ten instructions) that is only called once or perhaps twice within a middling program (i.e., over hundreds of machine instructions) is a poor candidate for a procedure.

You may find it useful to define large procedures that are called only once when your program becomes big enough to require breaking it down into functional chunks. A thousand-line assembly-language program might split well into a sequence of nine or ten largish procedures. Each is only called once from the main program, but this allows your main program to be very indicative of what the program is doing:

Start: call Initialize call OpenFile Input: call GetRec

call VerifyRec call WriteRec loop Input call CloseFile call Cleanup

call ReturnToDOS

This is clean and readable, and provides a necessary "view from a height" when you begin to approach a thousand-line assembly-language program. Remember that the Martians are always hiding somewhere close by, anxious to turn your program into unreadable hieroglyphics.

There's no weapon against them with half the power of procedures.

8.3 Using BIOS Services

In the last chapter we looked closely at DOS services, which are accessed through the DOS services dispatcher. The DOS dispatcher lives at the other end of software interrupt 21H, and offers a tremendous list of services at the disposal of your programs. There's another provider of services in your machine that lives even deeper than DOS: the ROM BIOS. ROM (Read-Only Memory), indicates memory chips whose contents are burned into their silicon and do not vanish when power is turned off. BIOS (Basic Input/Output System) is a collection of fundamental routines for dealing with your computers input and output peripherals. These peripherals include disk drives, displays, print-ers, and the like. DOS uses BIOS services as part of some of the services that it provides.

Like DOS, BIOS services are accessed through software interrupts. Unlike DOS, which channels nearly all requests for its services through the single interrupt 21H, BIOS uses numerous interrupts (about 10) and groups similar categories of services beneath the control of different interrupts. For example, video display services are accessed through interrupt 10H, keyboard services are accessed through interrupt 16H, printer services are accessed through interrupt 17H, and so on.

The overall method for using BIOS services, however, is very similar to that of DOS. You load a service number and sometimes other initial values into the registers and then execute an INT <n> instruction, where the n depends on the category of services you're requesting.

Nothing difficult about that at all. Let's start building some tools.