Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Assembly Language Step by Step Programming with DOS and Linux 2nd Ed 2000.pdf
Скачиваний:
156
Добавлен:
17.08.2013
Размер:
4.44 Mб
Скачать

The Notion of an Assembly Language String

Words fail us sometimes by picking up meanings as readily as a magnet picks up iron filings. The word string is a major offender here. It means roughly the same thing in all computer programming, but there is a multitude of small variations on that single theme. If you learned about strings in Turbo Pascal, you'll find that what you know isn't totally applicable when you program in C, or Basic, or assembly.

So here's the Big View: A string is any contiguous group of bytes, of any arbitrary size up to the size of a segment. The main concept of a string is that its component bytes are right there in a row, with no interruptions.

That's pretty fundamental. Most higher-level languages build on the string concept in several ways. Turbo Pascal treats strings as a separate data type, limited to 255 characters in length, with a single byte at the start of the string to indicate how many bytes are in the string. In C, a string may be longer than 255 bytes, and it has no length byte in front of it. Instead, a C string is said to end when a byte with a binary value of 0 is encountered. In Basic, strings are stored in something called string space, which has a lot of built-in code machinery associated with it.

When you begin working in assembly, you have to give up all that high-level language stuff. Assembly strings are just contiguous regions of memory. They start at some specified address, go for some number of bytes, and stop. There is no length byte to tell how many bytes are in the string and no standard boundary characters such as binary 0 to indicate where a string starts or ends.

You can certainly write assembly language routines that allocate Turbo Pascal-style strings or C-style strings and manipulate them. To avoid confusion, however, you must think of the data operated on by your routines to be Pascal or C strings rather than assembly strings.

Turning Your "String Sense" Inside-Out

Assembly strings have no boundary values or length indicators. They can contain any value at all, including binary 0. In fact, you really have to stop thinking of strings in terms of specific regions in memory. You should instead think of strings in much the same way you think of segments: in terms of the register values that define them.

It's slightly inside-out compared to how you think of strings in such languages as Pascal, but it works:

You've got a string when you set up a pair of registers to point to one (or a single register, if you're working in real mode or protected mode flat model). And once you point to a string, the length of that string is defined by the value you place in register CX.

This is key: Assembly strings are wholly defined by values you place in registers. There is a set of assumptions about strings and registers baked into the silicon of the CPU. When you execute one of the string instructions (as I describe a little later), the CPU uses those assumptions to determine which area of memory it reads from or writes to.

Source Strings and Destination Strings

There are two kinds of strings in assembly work. Source strings are strings that you read from.

Destination strings are strings that you write to. The difference between the two is only a matter of registers; source strings and destination strings can overlap. In fact, the very same region of memory can be both a source string and a destination string, all at the same time.

Here are the assumptions the CPU makes about strings when it executes a string instruction:

A source string is pointed to by DS:SI.

A destination string is pointed to by ES:DI.

The length of both kinds of strings is the value you place in CX.

Data coming from a source string or going to a destination string must pass through register AX.

Note that the use of segment registers mostly applies to real mode segmented model. In real mode flat

model, as you should know by now, all the segment registers contain the same value, and are therefore basically factored out of consideration for many things, string work included. (The same is true of protected mode flat model, as you'll learn in later chapters.) The CPU can recognize both a source string and a destination string simultaneously, because DS:SI and ES:DI can hold values independent of one another. However, because there is only one CX register, the length of source and destination strings must be identical when they are used simultaneously, as in copying a source string to a destination string.

One way to remember the difference between source strings and destination strings is by their offset registers. SI means "source index," and DI means "destination index."

REP STOSW, the Software Machine Gun

The best way to cement all that string background information in your mind is to see a string instruction at work. In this section I lay out a very useful video display tool that makes use of the simplest string instruction, STOSW. (Think: STOre String by Word.) The discussion involves something called a prefix, which I haven't gone into yet. Bear with me for now. We'll discuss prefixes in a little while.

Machine-Gunning the Video Display Buffer

The ClrScr procedure we discussed earlier relied on BIOS to handle the actual clearing of the screen. BIOS is very much a black box, and we're not expected to know how it works. The trouble with BIOS is that it only knows how to clear the screen to blanks. Some programs (such as the most recent releases of Borland/Turbo Pascal) give themselves a stylish, sculpted look by clearing the screen to one of the PC's halftone characters, which are character codes 176 to 178. BIOS can't do this. If you want the halftone look, you'll have to do it yourself. It doesn't involve anything more complex than replicating a single word value (two bytes) into every position in your video refresh buffer.

Such things should always be done in tight loops. The obvious way is to put the video refresh buffer segment into the extra segment register ES, the refresh buffer offset into DI, the number of words in your refresh buffer into CX, the word value to clear the buffer to into AX, and then code up a tight loop this way:

Clear: mov ES:[DI],AX

; Copy AX to ES:DI

inc DI

; Bump DI to next *word* in buffer,

inc DI

; which means incrementing by 1 twice

dec

CX

;

Decrement CX by one position

jnz

Clear

;

And loop again until CX is 0

This will work. It's even tolerably fast, especially on newer CPUs. But all of the preceding code is equivalent to this one single instruction:

rep stosw

Really. Really.

There are two parts to this instruction, actually. As I said, REP is a new type of critter, called a prefix. We'll get back to it. Right now, let's look at STOSW. The mnemonic means STOre String by Word. Like all the string instructions, STOSW makes certain assumptions about some CPU registers. It works only on the destination string, so DS and SI are not involved. However, these assumptions must be respected and dealt with:

1.ES must be loaded with the segment address of the destination string (that is, the string into which data will be stored). This is automatically the case in flat model and does not have to be set up by you.

2.DI must be loaded with the offset address of the destination string. (Think: DI, the destination index.)

3.CX (think: the Count register) must be loaded with the number of times the copy of AX is to be stored into the string. Note that this does not mean the size of the string in bytes!

4.AX must be loaded with the word value to be stored into the string.

Executing the STOSW Instruction

Once you set up these four registers, you can safely execute a STOSW instruction. When you do, this

is what happens:

1.The word value in AX is copied to the word at ES:DI.

2.DI is incremented by 2, such that ES:DI now points to the next word in memory following the one

1.

2.

just written to.

Note that we're not machine-gunning here. One copy of AX gets copied to one word in memory. The DI register is adjusted so that it'll be ready for the next time STOSW is executed.

One very important point to remember is that CX is not decremented by STOSW. CX is decremented automatically only if you put the REP prefix in front of STOSW. Lacking the REP prefix, you have to do the decrementing yourself, either explicitly through DEC or through the LOOP instruction, as I explain a little later in this chapter.

So, you can't make STOSW run automatically without REP. However, you can, if you like, execute other instructions before executing another STOSW. As long as you don't disturb ES, DI, or CX, you can do whatever you wish. Then when you execute STOSW again, another copy of AX will go out to the location pointed to by ES:DI, and DI will be adjusted yet again. (You have to remember to decrement CX somehow.) Note that you can change AX if you like, but the changed value will be copied into memory. (You may want to do that—there's no law saying you have to fill a string with only one single value.)

However, this is like the difference between a semiautomatic weapon (which fires one round every time you press and release the trigger) and a fully automatic weapon, which fires rounds continually as long as you hold the trigger down. To make STOSW fully automatic, just hang the REP prefix ahead of it.

What REP does is beautifully simple: It sets up the tightest of all tight loops completely inside the CPU and fires copies of AX into memory repeatedly (hence its name), incrementing DI by 2 each time and decrementing CX by 1, until CX is decremented down to 0. Then it stops, and when the smoke clears, you'll see that your whole destination string, however large, has been filled with copies of AX.

Man, now that's programming!

The following macro sets up and triggers REP STOSW to clear the video refresh buffer. Clear was designed to be used with the block of video information variables initialized by the VidCheck procedure I described in Chapter 10. It needs to be passed a far pointer (which is nothing more than a full 32-bit address consisting of a segment and an offset laid end to end) to the video refresh buffer, the word value to be blasted into the buffer, and the size of the buffer in bytes.

;---------------------------------------------------------------

; CLEAR -- Clears the entire visible screen buffer

;Last update 9/20/99

;Caller must pass:

;In VidAddress: The address of the video refresh buffer

;In ClearAtom: The character/attribute pair to fill the

;

buffer with. The high

byte

contains the

;

attribute and the low

byte

the character.

;In BufLength: The number of *characters* in the visible

;

 

display buffer, *not* the number of bytes!

;

 

This is typically 2000 for a 25-line screen

;

Action:

or 4000 for a 50-line screen.

;

Clears the screen by machine-gunning the

;

 

character/attribute pair in AX into the

;

 

display buffer beginning at VidAddress.

;

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

 

%macro Clear 3 ;VidAddress,ClearAtom,BufLength

 

les DI,[%1]

; Load ES and DI from FAR pointer

 

mov AX,%2

; Load AX with word to blast into memory

 

mov CX,%3

; Load CX with length of buffer in bytes

 

shr CX,1

; Divide size of buffer by 2 for word count

 

cld

; Set direction flag so we blast up-memory

 

rep stosw

; Blast away!

 

GotoXY 0,0

; Move hardware cursor to UL corner of screen

%endmacro

 

Don't let the notion of a far pointer throw you. It's jargon you're going to hear again and again, and this was a good point at which to introduce it. A pointer is an address, quite simply. A near pointer is an offset address only, used in conjunction with some value in some segment register that presumably doesn't change. All pointers to objects inside a real mode flat model program are by definition near pointers.

A far pointer is a pointer that consists of both a segment value and an offset value, both of which may be changed at any time, working together. The video refresh buffer is not usually part of your data segment, so if you're going to work with it, you're probably going to have to access it with a far pointer, as we're doing here. Any time you need to reference something that exists outside the 64K boundaries of a real mode flat model program (such as your system's text video buffer), you're going to have to work with a far pointer.

Note that most of Clear is setup work. The LES instruction loads both ES and DI with the address of the destination string. The screen atom (display character plus attribute value) is loaded into AX.

The handling of CX deserves a little explanation. The value in BufLength is the size in bytes of the video refresh buffer. Remember, however, that CX is assumed to contain the number of times that AX is to be machine-gunned into memory. AX is a word, and a word is 2 bytes long. So, each time STOSW fires, 2 bytes of the video refresh buffer will be written to. Therefore, in order to tell CX how many times to fire the gun, we have to divide the size of the refresh buffer (which is given in bytes) by 2, in order to express the size of the refresh buffer in words.

As I explained in Chapter 10, dividing a value in a register by 2 is easy. All you have to do is shift the value of the register to the right by one bit. This what the SHR CX,1 instruction does: divides CX by 2.

STOSW and the Direction Flag DF

Note the CLD instruction in the Clear macro. I've avoided mentioning it until now to avoid confusing you. Most of the time you'll be using STOSW, you'll want to run it "uphill" in memory; that is, from a lower memory address to a higher memory address. In Clear, you put the address of the start of the video refresh buffer into ES and DI, and then blast character/attribute pairs into memory at successively higher memory addresses. Each time STOSW fires a word into memory, DI is incremented twice to point to the next higher word in memory.

This is the logical way to work it, but it doesn't have to be done that way. STOSW can just as easily begin at a high address and move downward in memory. On each store into memory, DI can be decremented by 2 instead.

Which way STOSW fires—uphill toward successively higher addresses, or downhill toward successively lower addresses—is governed by one of the flags in the Flags register. This is the

Direction flag DF. DF's sole job in life is to control the direction of certain instructions that, like STOSW, can move in one of two directions in memory. Most of these (like STOSW) are string instructions.

The sense of DF is this: When DF is set (that is, when DF has the value 1), STOSW and its fellow string instructions work downhill, from higher to lower addresses. When DF is cleared (that is, when it has the value 0), STOSW and its siblings work uphill from lower to higher addresses. This in turn is simply the direction in which the DI register is adjusted: When DF is set, DI is decremented. When DF is cleared, DI is incremented.

The Direction flag defaults to 0 (uphill) when the CPU is reset. It is generally changed in one of two ways: with the CLD instruction, or with the STD instruction. CLD clears DF, and STD sets DF. (You should keep in mind when debugging that the POPF instruction can also change DF, by popping an entire new set of flags from the stack into the Flags register.) It's always a good idea to place the appropriate one of CLD or STD right before a string instruction to make sure that your machine gun fires in the right direction!

People sometimes get confused and think that DF also governs whether CX is incremented or decremented by the string instructions. Not so! Nothing in a string instruction ever increments CX! You place a count in CX and it counts down, period. DF has nothing to say about it.

The Clear macro is part of the MYLIB.MAC macro library on the CD-ROM for this book. As you build new macro tools, you might place them in MYLIB.MAC as well.