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

Assembly Language Step by Step 1992

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

The Phantoms of the Opcodes

TEST performs an AND logical operation between two operands, and then sets the flags as AND would, without altering the destination operation, as AND would. Here's the

TEST instruction syntax:

TEST <operand>.<bit mask>

The bit mask operand should contain a 1 bit in each position where a 1-bit is to be sought in the operand, and 0 bits in all the other bits.

TEST ANDs the operand against the bit mask, and set the flags as AND would. The operand doesn't change. For example, if you want to determine if bit 3 of AX is set to 1, you would use this instruction:

TEST AX , 3 ; 3 in binary is 00001000B

AX doesn't change as a result of the operation, but the AND truth table is asserted between AX and the binary pattern 00001000. If bit 3 in AX is a 1 bit, then ZF is cleared to 0. If bit 3 in AX is a 0 bit, then ZF is set to 1. Why? If you AND 1 (in the bit mask) with 0 (in AX) you get 0. (Look it up in the AND truth table.) And if all 8 bitwise AND operations come up 0, the result is 0, and ZF is raised to 1, indicating that the result is 0. Key to understanding TEST is thinking of TEST as a sort of "Phantom of the Opcode," where the opcode is AND. TEST pretends it is AND, but doesn't follow through with the results of the operation. It simply sets the flags as though an AND operation had occurred.

CMP is another "Phantom of the Opcode," and bears the same relation to SUB as TEST bears to AND. CMP subtracts its second operand from its first, but doesn't follow through and store the result in the first operand. It just sets the flags as though a subtraction had occurred.

TEST Pointers

Here's something important to keep in mind: TEST is only useful for finding 1 bits. If you need to identify 0 bits, you must first flip each bit to its opposite state with the logical NOT instruction, as I explained in Section 9.1. NOT changes all 1 bits to 0 bits, and all 0 bits to 1 bits. Once all 0 bits are flipped to 1 bits, you can test for a 1 bit where you need to find a 0 bit. (Sometimes it helps to map it out on paper to keep it all straight.)

Also, TEST will not reliably test for two or more 1 bits in the operand at one time. TEST doesn't check for the presence of a bit pattern; TEST checks for the presence of a single 1 bit. In other words, if you need to check to make sure that both bits 4 and 5 are set to 1, TEST won't hack it.

And unfortunately, that's what we have to do in DispID What we're looking for in the last part of DispID is the monochrome code in bits 4 and 5, which is the value 30 (both bits 4 and 5 set to 1). Don't make the mistake (as I did once) of assuming that you can use

TEST to spot the two 1 bits in bits 4 and 5:

both = 1, it's an MDA

test AL,30H

: If bits 4 & 5 are

jnz CGA

; otherwise it's a

CGA

This doesn't work! The Zero flag will be set only if both bits are 0. If either bit is 1, ZF will become 0, and the branch will be taken. However, we only want to take the branch if both bits are 1.

Here's where your right brain can sometimes save both sides of your butt. TEST only spots a single 1 bit at a time. We need to detect a condition where two 1 bits are present. So let's get inspired and flip the state of all bits in the Equipment Identification Byte with NOT, and then look at the byte with TEST. After using NOT, what we need to find are two 0 bits, not two 1 bits. And if the two bits in question (4 and 5) are now both 0, the whole byte is 0, and ZF will be set and ready to test via JNZ:

not

AL

; Invert

all bits

in the equipment

ID byte

test AL ,30H

: See

if

either of bits 4 or 5

are

1-bits

jnz

CGA

;

If both = 0, they originally

were both 1's,

 

 

;

and

the adapter

is a monochrome

 

Tricky, tricky. But as you get accustomed to the instruction set and its quirks, you'll hit upon lots of non-obvious solutions to difficult problems of that kind.

So get that right brain working: how would you test for a specific pattern that was a mix of 0 bits and 1 bits?

9.4 Assembler Odds'n'Ends

Practice is the word.

You can do a lot with what you've learned so far, and certainly, you've learned enough to be able to figure out the rest with the help of an assembly-language reference and perhaps a more advanced book on the subject. For the remainder of this chapter we're

going to do some practicing, flexing some assembly-language muscles and picking up a few more instructions in the process.

Yet Another Lookup Table

The DIGITS lookup table (used by Byte2Str and WordZStr in the previous section)

is so obvious that it didn't need much in the line of comments or explanations. Digits simply converted the table's index into the ASCII character equivalent to the value of the index. Digits is only 16 bytes long, and its contents pretty much indicate what it's for:

Digits

DB '0123456789ABCDEF'

Most of the time, your lookup tables will be a little less obvious. A lookup table does not have to be one single DB variable definition. You can define it pretty much as you need to, either with all table elements defined on a single line (as with Digits) or with each table element on its own line.

Consider the lookup table below:

Here's a table where each table element has its own DW definition statement on its own line. This table treats a problem connected with the numerous different kinds of display adapters installable in a PC. There are two different addresses where the video refresh buffer begins. On boards connected to color or color/greyscale monitors, the address is B800:0, whereas on monochrome monitors the address is B000:0. (Refer back to Figure 5.4 and the accompanying text if you've forgotten what the video refresh buffer is.)

If you intend to address video memory directly (and doing so is much faster than working through DOS as we have been) then you have to know at which address the video refresh buffer lies. Knowing which display adapter is installed is the

hardest part—and the DispID procedure described in the previous section answers that question. Each of the nine codes returned by DispID has a video refresh buffer address associated with it. But which goes with which? You could use a long and interwoven series of CMP and JE tests, but that's the hard road, and grossly wasteful of memory and machine cycles. A lookup table is simpler, faster in execution, and much easier to read. The routine below returns the segment portion of the video refresh buffer address in AX. The display adapter code must be passed to VidOrg in AL:

;VidOrg -- Returns origin segment of video buffer ;Last update 3/8/89

;

;1 entry point:

;

;VidOrg:

; Caller must pass:

; AL : Code specifying display adapter type

; VidOrg returns the buffer origin segment in AX

 

 

 

VidOrg

PROC

; Zero AH

xor

AH,AH

mov

DI.AX

; Copy AX (with code in AL) into DI

shl

DI.l

; Multiply code by 2 to act as word index

lea

BX.OriginTbl

; Load address of origin table into BX

mov

AX,[BX+DI]

; Index into table using code as index

ret

ENDP

; Done; go home!

VidOrg

 

This works a lot like the lookup table mechanism in Byte2Str. There's an important difference, however: each entry in the OriginTbl lookup table is two bytes in size, whereas each entry in Digits was one byte in size.

Using Shift Instructions to Multiply by Powers of 2

To use the Digits lookup table, we simply used the value to be converted as the index into the table. Because each element in the table was one byte in size, this worked. When table elements are more than one byte long, you have to multiply the index by the

number of bytes in each table element, or the lookup won't find the correct table element. OriginTbl is a good working example. Suppose you get a code 2 back from DispID, indicating that you have a CGA in the system. Adding the 2 code to the starting address of the table (as we did with Digits) takes us to the start of the second element in the table. Read across to the comment at the right of that second element and see which code it applies to: Code 1, the MDA! Not cool.

If you scan down to find the table element associated with the CGA, you'll find that it starts at an offset of 4 from the start of the table. To index into the table correctly, you have to add 4, not 2, to the offset address of the start of the table. This is where multiplication comes in.

There is a general-purpose multiply instruction in the 8086/8088 CPU, but MUL is outrageously slow as machine instructions go. Even in its fastest case on the 8086/8088 (multiplying an 8-bit register by some value) MUL takes 77 machine cycles to do its work. Considering that most of the instructions we've discussed complete their jobs in 4 to 10 cycles, that's slow indeed.

There's a better way—in some cases. When you need to multiply a value by some power of 2 (that is, 2, 4, 8, 16, 32, and so on) you can do it by using the SHL instruction. Shifting a value to the left by one bit multiplies the value by 2. Shifting a value to the left by two bits multiplies the value by 4. Shifting a value to the left by three bits multiplies the value by 8, and so on.

Magic? Not at all. Work it out on paper by expressing a number as a bit pattern (in binary form), shifting the bit pattern one bit to the right, and then converting the binary form back to decimal or hex. Like so:

00110101 Binary equivalent of 35H, 53 decimal

<-- by one bit yields

01101010 Binary equivalent of 6AH, 106 decimal

Sharp readers may have guessed that shifting to the right divides by powers of two—and that's also correct. Shifting right by one bit divides by 2; shifting right by two bits divides by 4, and so on.

The advantage to multiplying with shift instructions is that it's fast. Shifting a byte-sized value in a register to the left by one bit takes only 2 machine cycles. 2...as opposed to 77 with MUL.

As we say, no contest.

Once the index is multiplied by 2 using SHL, the index is added to the starting address of the table, just as with Digits. A word-sized MOV then copies the correct segment

address from the table into AX, for return to the caller.

This illustrates how you can realize enormous speed advantages by struc-turing your tables properly. Even if it means leaving some wasted space at the end of each element, do your best to make the length of your table elements equal to some power of 2. That means making each element 1, 2, 4, 8, 16, 32, or some larger power of two in size, but not 3, 7, 12, 20, or 25 bytes in size.

Tables Within Tables

Tables are about the handiest means at your disposal for grouping together and organizing data. Sometimes tables can be as simple as those I've just shown you, which are simply sequences of single values.

In most cases, you'll need something a little more sophisticated, Sometimes you'll need a table of tables, and (surprise!) the 8086/8088 has some built-in machinery to handle such nested tables quickly and easily.

Let's continue on with the issue of video support. In the previous section we looked a table containing the display buffer addresses for each of the display adapters identified by DispID. This is good, but not enough: each adapter has a name, a display buffer address, and a screen size dictated by the size of the current character font. These items comprise a table of information about a display adapter, and if you wanted to put together a summary of all that information about all legal display adapters, you'd have to create such a table of tables.

Below is such a two-level table:

The table consists of twelve subtables, one for each possible code returned by DispID as well as a subtable for several undefined codes. Why a subtable for undefined codes? We're going to follow the same general strategy of indexing into the table based on the value of the code. In other words, to get the information for code 4, we have to look at the fifth table (counting from zero) which requires that tables 0 through 4 already exist. Code 3 is undefined, yet something must hold its place in the table for our indexing scheme to work.

Each subtable occupies three lines, for clarity's sake. Here's a typical subtable:

DB

'EGA with color monitor

; Code 4

DW

OB800H

 

DB

43,25,25

 

The first line is a 27-character quoted string containing the name of the display adapter. The second line is a word-sized address, the segment address of the visible display buffer corresponding to that name. The third line contains three numeric values. These are screen sizes, in lines, relating to the font sizes currently in force. The first value is the number of lines on the screen with the 8-pixel font in force. The second value is the number of lines on the screen with the 14-pixel font in force. The third value is the number of lines on the screen with the 16-pixel font in force. The items stored in the

subtables give you just about everything you'd really need to know about a given display adapter to do useful work with it.

When your assembly-language program begins executing, it should inspect such a table and extract the values pertinent to the currently installed display adapter. These extracted values should be ordinary variables in the data segment, easily accessible without further table searching. These variables should be defined together, as a block, with comments explaining how they are related:

As the comments indicate, a single procedure named VidChek reads values from the two-level lookup table VidInfoTbl and loads those values into the variables shown above.

VidCheck is an interesting creature, and demonstrates the way of dealing with two-level tables. Read it over: