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

Shifting Bits

The other way of manipulating bits within a byte is a little more straightforward: You shift them to one side or the other. There are a few wrinkles to the process, but the simplest shift instructions are pretty obvious: SHL SHifts its operand Left, whereas SHR SHifts its operand Right.

All of the shift instructions (including the slightly more complex ones I describe a little later) have the same general form, illustrated here by the SHL instruction:

SHL <register/memory>,<count>

The first operand is the target of the shift operation, that is, the value that you're going to be shifting. It can be register data or memory data, but not immediate data. The second operand specifies the number of bits by which to shift.

Shift by What?

This <count> operand is a little peculiar. On the 8086 and 8088, it can be one of two things: the immediate digit 1, or else the register CL. (Not CX!) If you specify the count as 1, then the shift will be by one bit. If you wish to shift by more than one bit at a time, you must load the shift count into register CL. Counting things is CX's (and hence CL's) hidden agenda; it counts shifts, loops, string elements, and a few other things that we look at later in this book. That's why it's sometimes called the count register and can be remembered by the C in count.

Although you can load a number as large as 255 into CL, it really only makes sense to use count values up to 32. If you shift any bit in a double word by 32, you shift it completely out of the double word-not to mention out of any byte or word!

Starting with the 286, the <count> operand may be any immediate value from 1 to 255. If you're quite sure your code will never have to run on an 8086 or 8088, using an immediate operand instead of loading CL can save you an instruction and a little time.

How Bit Shifting Works

Understanding the shift instructions requires that you think of the numbers being shifted as binary numbers, and not hexadecimal or decimal numbers. (If you're fuzzy on binary notation, again, take another slip through Chapter 1.) A simple example would start with register AX containing a value of 0B76FH. Expressed as a binary number (and hence as a bit pattern), 0B76FH is as follows:

1011011101101111

Keep in mind that each digit in a binary number is one bit. If you execute an SHL AX,1 instruction, what you'd find in AX after the shift is the following:

0110111011011110

A 0 has been inserted at the right-hand end of the number, and the whole shebang has been bumped toward the left by one digit. Notice that a 1 bit has been bumped off the left end of the number into cosmic nothingness.

Bumping Bits into the Carry Flag

Well, not exactly cosmic nothingness . . . The last bit shifted out is bumped into a temporary bucket for bits called the Carry flag, often abbreviated as CF. The Carry flag is one of those odd bits lumped together as the Flags register, which I described in Chapter 6. You can test the state of the Carry flag with a branching instruction, as I explain later in this chapter.

Keep in mind when using shift instructions, however, that a lot of different instructions use the Carry flag as well as the shift instructions. If you bump a bit into the Carry flag with the intent of testing that bit

to see what it is, test it before you execute another instruction that affects the Carry flag. This includes all the arithmetic instructions, all the bitwise logical instructions, a few miscellaneous instructions-and, of course, all the other shift instructions.

If you shift a bit into the Carry flag and then immediately execute another shift instruction, the first bit will be bumped off the end of the world and into cosmic nothingness.

Byte2Str: Converting Numbers to Displayable Strings

As we've seen, DOS has a fairly convenient method for displaying text to your screen. The problem is that it only displays text-if you want to display a numeric value from a register as a pair of digits, DOS won't help. You first have to convert the numeric value into its string representation, and then display the string representation through DOS.

Converting hexadecimal numbers to hexadecimal digits isn't difficult, and the routine to do the job demonstrates several of the new concepts we're exploring in this chapter. Read the code for procedure

Byte2Str carefully:

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

;Byte2Str -- Converts a byte passed in AL to a string at

;

DS:SI

; Last update 9/18/99

;

;1 entry point:

;Byte2Str:

;Caller must pass:

;AL : Byte to be converted

;DS : Segment of destination string

;SI : Offset of destination string

;This routine converts 8-bit values to 2-digit hexadecimal

;string representations at DS:SI. The "H" specifier is

;*not* included. Four separate output examples:

;02 B7 FF 6C

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

 

 

 

Byte2Str:

; Duplicate byte in DI

 

mov DI,AX

of DI

and DI,000FH

; Mask

out high 12 bits

mov BX,Digits

; Load

offset of Digits

into DI

mov AH,BYTE [BX+DI] ; Load digit from table

into AH

mov [SI+1],AH

; and store digit into string

xor AH,AH

; Zero out AH

 

mov DI,AX

; And move byte into DI

 

; WARNING: The following instruction requires 286 or better!

shr DI,4

; Shift high nybble of byte to low

mov AH,BYTE [BX+DI] ; Load digit from table

into AH

mov [SI],AH

; and store digit into string

ret

; We're done-go home!

 

Note that this is a procedure, and not a macro. (It could be turned into a macro, however. Why not give it a shot?)

To call Byte2Str, you must pass the value to be converted to a string in AL and the address of the string into which the string representation is to be stored as DS:SI. Typically, DS will already contain the segment address of your data segment, so you most likely will only need to pass the offset of the start of the string in SI.

In addition to the code shown here, Byte2Str requires the presence of a second string in the data segment. This string, whose name must be Digits, contains all 16 of the digits used to express

hexadecimal numbers. The definition of Digits looks like this:

Digits DB '0123456789ABCDEF'

The important thing to note about Digits is that each digit occupies a position in the string whose offset from the start of the string is the value it represents. In other words, "0" is at the start of the string, zero bytes offset from the string's beginning. The character "7" lies seven bytes from the start of the string, and so on. Digits is what we call a lookup table and it represents (as I explain in the following sections) an extremely useful mechanism in assembly language.

Splitting a Byte into Two Nybbles

Displaying the value stored in a byte requires two hexadecimal digits. The bottom four bits in a byte are represented by one digit (the least-significant, or rightmost, digit) and the top four bits in the byte are represented by another digit (the most significant, or leftmost, digit). Converting the two digits must be done one at a time, which means that we have to separate the single byte into two 4-bit quantities, which are often called nybbles.

To split a byte in two, we need to mask out the unwanted half. This is done with an AND instruction. Note in Byte2Str that the first instruction, MOV DI,AX, copies the value to be converted (which is in AL) into DI. You don't need to move AH into DI here, but there is no instruction to move an 8-bit register half such as AL into a 16-bit register such as DI. AH comes along for the ride, but we really don't need it. The second instruction masks out the high 12 bits of DI using AND. This eliminates whatever might have earlier been in free rider AH, as well as the high 4 bits of AL. What's left in DI is all we want: the lower 4 bits of what was originally passed to the routine in AL.

Using a Lookup Table

The low nybble of the value to be converted is now in DI. The address of Digits is loaded into BX. Then the appropriate digit character is copied from Digits into AH. The whole trick of using a lookup table lies in the way the character in the table is addressed:

MOV AH,BYTE [BX+DI]

DS:BX points to the start of Digits, so [BX] would address the first character in Digits. To get at the desired digit, we must index into the lookup table by adding the offset into the table to BX. There is an x86 addressing mode intended precisely for use with lookup tables, called base indexed addressing. That sounds more arcane than it is; what it means is that instead of specifying a memory location at [BX], we add an index contained in register DI to BX and address a memory location at [BX+DI].

If you recall, we masked out all of DI but the four lowest bits of the byte we are converting. These bits will contain some value from 0 through 0FH. Digits contains the hexadecimal digit characters from 0 to F. By using DI as the index, the value in DI will select its corresponding digit character in Digits. We are using the value in DI to look up its equivalent hexadecimal digit character in the lookup table Digits. See Figure 10.4.

Figure 10.4: Using a lookup table.

So far, we've read a character from the lookup table into AH. Now, we use yet another addressing mode to move the character from AX back into the second character of the destination string, whose address was passed to Byte2Str in DS:SI. This addressing mode is called indirect displacement addressing, though I question the wisdom of memorizing that term. The mode is nothing more than indirect addressing (that is, addressing the contents of memory at [SI]) with the addition of a literal displacement:

MOV [SI+1],AH

This looks a lot like base indexed addressing (which is why the jargon may not be all that useful) with the sole exception that what is added to SI is not a register but a literal constant.

Once this MOV is done, the first of the two nybbles passed to Byte2Str in AL has been converted to its character equivalent and stored in the destination string variable at DS:SI.

Now we have to do it again, this time for the high nybble.

Shifting the High Nybble into the Low Nybble

The high nybble of the value to be converted has been waiting patiently all this time in AL. We didn't mask out the high nybble until we moved AX into DI and did our masking on DI instead of AX. So, AL is still just as it was when Byte2Str began.

The first thing to do is clear AH to 0. Byte2Str uses the XOR AH,AH trick I described in the last section. Then we copy AX into DI with MOV. All that remains to be done is to somehow move the high nybble of the low byte of DI into the position occupied by the low nybble. The fastest way to do this is simply to shift DI to the right by four bits. This is what the four SHR DI,4 instructions in Byte2Str do.

The low nybble is simply shifted off the edge of DI, into the Carry flag, and then out into nothingness. After the shift, what was the high nybble is now the low nybble, and once again, DI can be used as an index into the Digits lookup table to MOV the appropriate digit into AH.

One minor caution: The instruction SHR DI,4 does not exist on the 8086 and 8088 CPUs. Prior to the 286, you could not provide any immediate operand to the shift instructions except for 1. Now, any immediate value that may be expressed in 8 bits may be used as the shift count operand. If your code must be able to run on any x86 CPU, you have to replace SHR DI,4 with four SHR DI,1 instructions. It's a good idea to flag any use of instructions that do not exist on early CPUs in your source code. While it's true that there are damned few 8088s and 8086s left out there, there are a few, and the reaction of old chips to undefined instructions is always a crapshoot and may produce some crazy bugs.

Finally, there is the matter of storing the digit into the target string at DS:SI. Notice that this time, there is no +1 in the MOV instruction:

MOV [SI],AH

Why not? The high nybble is the digit on the left, so it must be moved into the first byte in the target string. Earlier, we moved the low nybble into the byte on the right. String indexing begins at the left and works toward the right, so if the left digit is at index 0 of the string, the right digit must be at index 0+1.

Byte2Str does a fair amount of data fiddling in only a few lines. Read it over a few times while following the preceding discussion through its course until the whole thing makes sense to you.

Converting Words to Their String Form

Having converted a byte-sized value to a string, it's a snap to convert 16-bit words to their string forms. In fact, it's not much more difficult than calling Byte2Str . . . twice:

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

;Word2Str -- Converts a word passed in AX to a string at

;DS:SI

;Last update 9/18/99

;

;1 entry point:

;Word2Str:

;Caller must pass:

;AX : Word to be converted

;DS : Segment of destination string

;SI : Offset of destination string

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

 

Word2Str:

; Save a copy of convertee in CX

mov CX,AX

xchg AH,AL

; Swap high and low AX bytes to do high first

call Byte2Str

; Convert AL to string at DS:SI

add SI,2

; Bump SI to point to second 2 characters

mov AX,CX

; Reload convertee into AX

call Byte2Str

; Convert AL to string at DS:SI

ret

; And we're done!

The logic here is fairly simple-if you understand how Byte2Str works. Moving AX into CX simply saves an unmodified copy of the word to be converted in CX. Something to watch out for here: If Byte2Str were to use CX for something, this saved copy would be mangled, and you might be caught wondering why things weren't working correctly. This is a common enough bug for the following reason: You create Byte2Str, and then create Word2Str to call Byte2Str. The first version of Byte2Str does not make use of CX, so it's safe to use CX as a storage bucket.

However-later on you beef up Byte2Str somehow, and in the process add some instructions that use CX. You plum forgot that Word2Str stored a value in CX while Word2Str was calling Byte2Str. It's

pointless arguing whether the bug is that Byte2Str uses CX, or that Word2Str assumes that no one else is using CX. To make things work again, you would have to stash the value somewhere other than in CX. Pushing it onto the stack is your best bet if you run out of registers. (You might hit on the idea of stashing it in an unused segment register such as ES-but I warn against it! Later on, if you try to use these utility routines in a program that makes use of ES, you'll be in a position to mess over your memory addressing royally, and once you move to protected mode you can't play with the segment registers at all. Let segment registers hold segments. Use the stack instead.)

Virtually everything that Word2Str does involves getting the converted digits into the proper positions in the target string. A word requires four hexadecimal digits altogether. In a string representation, the high byte occupies the left two digits, and the low byte occupies the right two digits. Since strings are indexed from the left to the right, it makes a certain sense to convert the left end of the string first.

This is the reason for the XCHG instruction. It swaps the high and low bytes of AX, so that the first time

Byte2Str is called, the high byte is actually in AL instead of AH. (Remember that Byte2Str converts the value passed in AL.) Byte2Str does the conversion and stores the two converted digits in the first two bytes of the string at DS:SI.

For the second call to Byte2Str, AH and AL are not exchanged. Therefore, the low byte will be the one converted. Notice the following instruction:

ADD SI,2

This is not heavy-duty math, but it's a good example of how to add a literal constant to a register in assembly language. The idea is to pass the address of the second two bytes of the string to Byte2Str as though they were actually the start of the string. This means that when Byte2Str converts the low byte of AX, it stores the two equivalent digits into the second two bytes of the string.

For example, if the high byte was 0C7H, the digits C and 7 would be stored in the first two bytes of the string, counting from the left. Then, if the low byte were 042H, the digits 4 and 2 would be stored at the third and fourth bytes of the string, respectively. The whole string would look like this when the conversion was complete:

C742

As I've said numerous times before: Understand memory addressing and you've got the greater part of assembly language in your hip pocket. Most of the trick of Byte2Str and Word2Str lies in the different ways they address memory. If you study them, study the machinery behind the lookup table and target string addressing. The logic and shift instructions are pretty obvious and easy to figure out by comparison.