7
Machine Instructions and Their Operands
As we said earlier, MOV copies data from a source to a destination. MOV is an extremely versatile instruction, and understanding its versatility demands a little study of this notion of source and a destination.
Source and Destination Operands
Most machine instructions, MOV included, have one or more operands. (Some instructions have no operands.) In the machine instruction MOV AX,1, there are two operands. The first is AX, and the second is the digit 1.
By convention in assembly language, the first operand belonging to a machine instruction is the destination operand. The second operand is the source operand.
With the MOV instruction, the sense of the two operands is pretty literal: The source operand is copied to the destination operand. In MOV AX,1, the source operand 1 is copied into the destination operand AX. The sense of source and destination is not nearly so literal in other instructions, but a rule of thumb is this: Whenever a machine instruction causes a new value to be generated, that new value is placed in the destination operand.
There are three different flavors of data that may be used as operands. These are memory data, register data, and immediate data. I've laid some example MOV instructions out on the dissection pad in Table 7.1 to give you a flavor for how the different types of data are specified as operands to the MOV instruction.
|
|
|
|---|---|---|
MOV AX, | 1 | Source is immediate data. |
MOV BX, | CX | Both are 16-bit register data. |
MOV DL, | BH | Both are 8-bit register data. |
MOV [BP], | DI | Destination is memory data at SS:BP. |
MOV DX, | [SI] | Source is memory data at DS:SI. |
MOV BX, | [ES:BX] | Source is memory data at ES:BX. |
Immediate data is by far the easiest to understand. We look at it first.
Immediate Data
The MOV AX,1 machine instruction that I had you enter into DEBUG was a good example of what we call immediate data accessed through an addressing mode called immediate addressing. Immediate addressing gets its name from the fact that the item being addressed is immediate data built right into the machine instruction. The CPU does not have to go anywhere to find immediate data. It's not in a register, nor is it stored in a data segment somewhere out in memory. Immediate data is always right inside the instruction being fetched and executed.
Immediate data must be of an appropriate size for the operand. In other words, you can't move a 16-bit immediate value into an 8-bit register half such as AH or DL. Neither DEBUG nor the stand-alone assemblers will allow you to assemble an instruction like this:
MOV CL,67EF
CL is an 8-bit register, and 67EFH is a 16-bit quantity. Won't go!
Because it's built right into a machine instruction, you might think immediate data would be quick to access. This is true only to a point: Fetching anything from memory takes more time than fetching anything from a register, and instructions are, after all, stored in memory. So, while addressing immediate data is somewhat quicker than addressing ordinary data stored in memory, neither is anywhere near as quick as simply pulling a value from a CPU register.
Also keep in mind that only the source operand may be immediate data. The destination operand is the place where data goes, not where it comes from. Since immediate data consists of literal constants (numbers such as 1, 0, or 7F2BH), trying to copy something into immediate data rather than from immediate data simply has no meaning and is always an error.
Register Data
Data stored inside a CPU register is known as register data, and accessing register data directly is an addressing mode called register addressing. Register addressing is done by simply naming the register we want to work with. Here are some entirely legal examples of register data and register addressing:
MOV AX,BX
MOV BP,SP
MOV BL,CH
MOV ES,DX
ADD DI,AX
AND DX,SI
The last two examples point up the fact that we're not speaking only of the MOV instruction here. Register addressing happens any time data in a register is acted on directly, irrespective of what machine instruction is doing the acting.
The assembler keeps track of certain things that don't make sense, and one such situation is having a 16-bit register and an 8-bit register half within the same instruction. Such operations are not legal-after all, what would it mean to move a 2-byte source into a 1-byte destination? And while moving a 1-byte source into a 2-byte destination might seem more reasonable, the CPU does not support it and it cannot be done.
Playing with register addressing is easy using DEBUG. Bring up DEBUG and assemble the following series of instructions:
MOV AX,67FE
MOV BX,AX
MOV CL,BH
MOV CH,BL
Now, reset the value of IP to 0100 using the R command. Then execute the four machine instructions by issuing the T command four times in a row. The session under DEBUG would look like this:
- A
333F:0100 MOV AX,67FE
333F:0103 MOV BX,AX
333F:0105 MOV CL,BH
333F:0107 MOV CH,BL
333F:0109
- R IP
IP 0100
:0100
- R
AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=333F ES=333F SS=333F CS=333F IP=0100 NV UP EI PL NZ NA PO NC
333F:0100 B8FE67 MOV AX,67FE
- T
AX=67FE BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=333F ES=333F SS=333F CS=333F IP=0103 NV UP EI PL NZ NA PO NC
333F:0103 89C3 MOV BX,AX
- T
AX=67FE BX=67FE CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=333F ES=333F SS=333F CS=333F IP=0105 NV UP EI PL NZ NA PO NC
333F:0105 88F9 MOV CL,BH
- T
AX=67FE BX=67FE CX=0067 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=333F ES=333F SS=333F CS=333F IP=0107 NV UP EI PL NZ NA PO NC
333F:0107 88DD MOV CH,BL
- T
AX=67FE BX=67FE CX=FE67 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=333F ES=333F SS=333F CS=333F IP=0109 NV UP EI PL NZ NA PO NC
333F:0109 1401 ADC AL,01
Keep in mind that the T command executes the instruction displayed in the third line of the most recent R command display. The ADC instruction in the last register display is yet another garbage instruction, and although executing this particular instruction would not cause any harm (it's just an ADC: Add with Carry), I recommend against executing random instructions just to see what happens. Executing certain jump or interrupt instructions could wipe out sectors on your hard disk or, worse, cause internal damage to DOS that would not show up until later on.
Let's recap what these four instructions accomplished. The first instruction is an example of immediate addressing: The hexadecimal value 067FEH was moved into the AX register. The second instruction used register addressing to move register data from AX into BX. Keep in mind that the way the operands are written is slightly contrary to the common-sense view of things. The destination operand comes first. Moving something from AX to BX is done by executing MOV BX,AX. Assembly language is just like that sometimes-if that were the most peculiar thing about it, I for one would be mighty grateful ...
The third instruction and fourth instruction both move data between register halves rather than full, 16-bit registers. These two instructions accomplish something interesting. Look at the last register display, and compare the value of BX and CX. By moving the value from BX into CX a byte at a time, it was possible to reverse the order of the two bytes making up BX. The high half of BX (what we sometimes call the most significant byte, or MSB, of BX) was moved into the low half of CX. Then the low half of BX (what we sometimes call the least significant byte, or LSB, of BX) was moved into the high half of CX. This is just a sample of the sorts of tricks you can play with the general-purpose registers.
Just to disabuse you of the notion that the MOV instruction should be used to exchange the two halves of a 16-bit register, let me suggest that you do the following: Before you exit DEBUG from your previous session, assemble this instruction and execute it using the T command:
XCHG CL,CH
The XCHG instruction exchanges the values contained in its two operands. What was interchanged before is interchanged again, and the value in CX will match the values already in AX and BX. A good idea while writing your first assembly language programs is to double-check the instruction set periodically to see that what you have cobbled together with four or five instructions is not possible using a single instruction. The x86 instruction set is very good at fooling you in that regard! (One caution: Later on, you might find that cobbling something together from simple instructions might run more quickly than the same thing accomplished by a single specialized instruction, especially on the newest Pentium-class CPUs. Pentium optimization is a truly peculiar business-but we're way ahead of ourselves now in speaking of what's fast and what's not. Learn how it works first-and then we can explore how fast it is!)
Memory Data
Immediate data is built right into its own machine instruction. Register data is stored in one of the CPU's limited collection of internal registers. In contrast, memory data is stored somewhere in the megabyte vastness of real mode memory. Specifying that address is much more complicated than simply reaching into a machine instruction or naming a register.
You should recall that a memory location must be specified in two parts: a segment address, which is one of 65,536 segment slots spaced every 16 bytes in memory, and an offset address, which is the number of bytes by which the specified byte is offset from the start of the segment. Within the CPU, the segment address is kept in one of the four segment registers, while the offset address (generally just called the offset) may be in one of a select group of general-purpose registers that includes only BP, BX, SI, and DI. (Register SP is a special case and addresses data located on the stack, as I explain in Chapter 8. To pin down a single byte anywhere within real mode's megabyte of memory, you need both the segment and offset components. We generally write them together, specified with a colon to separate them, as either literal constants or register names: 0B00:0167, DS:SI or CS:IP.
BX's Hidden Agenda
One of the easiest mistakes to make early on is to assume that you can use any of the general-purpose registers to specify an offset for memory data. Not so! If you try to specify an offset in AX, CX, or DX, the assembler will flag an error.
In real mode, only BP, BX, SI, and DI may hold an offset for memory data.
(This isn't true for more advanced CPUs working in protected mode, as we'll see toward the end of this book.) So, in fact, general-purpose registers AX, CX, and DX aren't quite so general after all. Why was general-purpose register BX singled out for special treatment? Think of it as the difference between dreams and reality for Intel. In the best of all worlds, every register could be used for all purposes. Unfortunately, when CPU designers get together and argue about what their nascent CPU is supposed to do, they are forced to face the fact that there are only so many transistors on the chip to do the job.
Each chip function is given a budget of transistors (sometimes numbering in the tens or even hundreds of thousands). If the desired logic cannot be implemented using that number of transistors, the expectations of the designers have to be brought down a notch and some CPU features shaved from the specification.
The early x86 CPUs including the 8086 and 8088 are full of such compromises. There were not enough transistors available at design time to allow all general-purpose registers to do everything, so in addition to the truly general-purpose ability to hold data, each 8086/8088 register has what I call a "hidden agenda." Each register has some ability that none of the others share. I describe each register's hidden agenda at some appropriate time in this book, and I call it out as such.
In the 20-odd years since the 8086 was created, Intel has hugely expanded the power of its x86 family of CPUs. And sure enough, when you get into 32-bit protected mode, most of the limitations imposed by early transistor budgets go away, and general-purpose registers become almost completely general. However, when acting in real mode (as we're speaking of here), the Pentium, 486, and 386 CPUs take on just about all the characteristics of the 8086 and 8088, including this sort of limitation, which is built into the logic that decodes the instruction set for real mode.
Should you, then, be learning this sort of bad-old-days limitation? I think so. What it teaches you is that limitations exist and need to be remembered. Even the mighty Pentium II has limitations and restrictions. You need to develop a grasp of them, or you'll be floundering around wondering why things don't work.
Using Memory Data
With one or two important exceptions (the string instructions, which I cover to a degree-but not exhaustively-later on), only one of an instruction's two operands may specify a memory location. In other words, you can move an immediate value to memory, or a memory value to a register, or some other similar combination, but you can't move a memory value directly to another memory value. This is just an inherent limitation of the CPU, and we have to live with it, inconvenient as it gets at times.
Specifying a memory address as one of an instruction's operands is a little complicated. The offset address must be resident in one of the general-purpose registers that can legally hold an offset address. (Remember, that's only BP, BX, SI, and DI-not any of the others such as AX, CX, or DX.) To specify that we want the data at the memory location contained in the register rather than the data in the register itself, we use square brackets around the name of the register. In other words, to move the word at address DS:BX into register AX, we would use the following instruction:
MOV AX,[BX]
Similarly, to move a value residing in register DX into the word at address DS:DI, you would use this instruction:
MOV [DI],DX
Segment Register Assumptions
The only problem with these examples is this: "DS" isn't anywhere in either instruction. Where does it say to use DS as the segment register?
It doesn't. To keep addressing notation simple, the x86 CPUs in real mode make certain assumptions about certain instructions in combinations with certain registers. There is no comprehensible system to these assumptions, and like dates in history or Spanish irregular verbs, you'll just have to memorize them, or at least know where to look them up. (The where is in Appendix B in this book.)
One of these assumptions is that in working with memory data, the MOV instruction uses the segment address stored in segment register DS unless you explicitly tell it otherwise. In the case of the two preceding examples, we did not tell the MOV instruction to use some segment register other than DS, so it fell back on its assumptions and used DS. However, had you specified the offset as residing in register SP instead of BX or DI, the MOV instruction would have assumed the use of segment register SS instead. This assumption involves a memory mechanism known as the stack, which we won't really address until the next chapter.
Overriding Segment Assumptions for Memory Data
But what if you want to use ES as a segment register for memory data addressed in the MOV instruction? It's not difficult. The instruction set includes what are called segment override prefixes. These are not precisely instructions, but are more like the filters that may be snapped in front of a camera lens. The filter is not itself a lens, but it alters the way the lens operates.
There is one segment override prefix for each of the four segment registers: CS, DS, SS, and ES. In assembly language they are written as the name of the segment register followed by a colon, as shown in Table 7.2.
|
|
|---|---|
CS: | Forces use of code segment register CS |
DS: | Forces use of the data segment register DS |
SS: | Forces use of the stack segment register SS |
ES: | Forces use of the extra segment register ES |
In use, the segment override prefix is placed immediately in front of the memory data reference whose segment register assumption is to be overridden. For example, to force a MOV instruction to copy a value from the AX register into a location at some offset (contained in SI) into the code segment, you would use this instruction:
MOV [CS:SI],AX
Without the CS: override prefix, this instruction would move the value of AX into the data segment, at an address specified as DS:SI.
Prefixes in use are very reminiscent of how an address is written; in fact, understanding how prefixes work will help you keep in mind that in every reference to memory data within an instruction, there is a ghostly segment register assumption floating in the air. You may not see the ghostly DS: assumption in your MOV instruction, but if you forget that it's there, the whole concept of memory data will begin to seem arbitrary and magical.
Every reference to memory data includes either an assumed segment register or else a segment override prefix to specify a segment register other than the assumed segment register.
At the machine-code level, a segment override prefix is a single binary byte. The prefix byte is placed in front of rather than within a machine instruction. In other words, if the binary bytes comprising a MOV AX,[BX] instruction are 8BH 07H, adding the ES segment override prefix to the instruction (MOV AX,[ES:BX]) places a single 26H in front of the opcode bytes, giving us 26H 8BH 07H as the full binary equivalent.
If you're sharp, the question will already have occurred to you: What about the flat models? Recall that in both real mode flat model and protected mode flat model, the segment registers all point to the same place and are not changed during the run of the program. In the flat models you do not use segment overrides. What I have explained previously about segment overrides applies only to the real mode segmented model!
Real Mode Memory Data Summary
Real mode memory data consists of a single byte or word in memory, addressed by way of a segment value and an offset value. The register containing the offset address is enclosed in square brackets to indicate that the contents of memory, rather than the contents of the register, are being addressed. The segment register used to address memory data is usually assumed according to a complex set of rules. Optionally, a segment override prefix may be placed in the instruction to specify some segment register other than the default segment register.
Figure 7.1 shows diagrammatically what happens during a MOV AX,[ES:BX] instruction. The segment address component of the full 20-bit memory address is contained inside the CPU in segment register ES. Ordinarily, the segment address would be in register DS, but the MOV instruction contains the ES: segment override prefix. The offset address component is specified to reside in the BX register.
The CPU sends out the values in ES and BX to the memory system side by side. Together, the two values pin down one memory location where MyWord begins. MyWord is actually two bytes, but that's fine-all the x86 CPUs working in real mode (except for the 8088) can bring both bytes into the CPU at once, while the 8088 brings both bytes in separately, one after the other. The CPU handles details like that and you needn't worry about it. Because AX is a 16-bit register, of course, two 8-bit bytes can fit into it quite nicely.
The segment address may reside in any of the four segment registers: CS, DS, SS, or ES. However, the offset address may reside only in registers BX, BP, SP, SI, or DI. AX, CX, and DX may not be used to contain an offset address during real mode memory addressing.
Limitations of the MOV Instruction
The MOV instruction can move nearly any register to any other register. For reasons having to do with the limited budget of transistors on the 8086 and 8088 chips, MOV can't quite do any move you can think of-in real mode, at least. Here's a list of MOV's real mode limitations:
MOV cannot move memory data to memory data. In other words, an instruction like MOV [SI],[BX] is illegal. Either of MOV's two operands may be memory data, but both cannot be at once.
MOV cannot move one segment register into another. Instructions like MOV CS,SS are illegal. This could have been handy, but it simply can't be done.
MOV cannot move immediate data into a segment register. You can't code up MOV CS,0B800H. Again, it would be handy but you just can't do it.
MOV cannot move one of the 8-bit register halves into a 16-bit register, nor vice versa. There are easy ways around any possible difficulties here, and preventing moves between operands of different sizes can keep you out of numerous kinds of trouble.
These limitations, of course, are over and above those situations that simply don't make sense: moving a register or memory into immediate data, moving immediate data into immediate data, specifying a general-purpose register as a segment register to contain a segment, or specifying a segment register to contain an offset address. Table 7.3 shows numerous illegal MOV instructions that illustrate these various limitations and nonsense situations.
|
|
|---|---|
MOV 17,1 | Only one operand may be immediate data. |
MOV 17,BX | Only the source operand may be immediate data. |
MOV CX,DH | The operands must be the same size. |
MOV [DI],[SI] | Only one operand may be memory data. |
MOV DI,[DX:BX] | DX is not a segment register. |
MOV ES,0B800 | Segment registers may not be loaded from immediate data. |
MOV DS,CS | Only one operand may be a segment register. |
MOV [AX],BP | AX may not address memory data (nor may CX or DX). |
MOV SI,[CS] | Segment registers may not address memory data. |
Some Notes on Assembler Syntax
Although we haven't talked about it a whole lot just yet, this book focuses on a particular assembler called NASM. And if this book is your first exposure to assembly language, nothing I've said so far should cause you any cognitive dissonance with your earlier experience, since you have no earlier experience. However, if you've played with assembly language using other assemblers, you will soon begin to see small differences between what you once learned in writing assembly language mnemonics and what I'm teaching in this book. These differences are matters of syntax, and they may become important, especially if you ever try to convert source code to NASM from another assembler such as MASM, TASM, or A86.
In the best of all worlds, every assembler would respond in precisely the same way to all the same mnemonics and directives set up all the same ways. In reality, syntax differs. Here's a common example: In Microsoft's MASM, memory data that includes a segment override must be coded like this:
MOV AX,ES:[BX]
Note here that the segment override "ES:" is outside the brackets enclosing BX. NASM places the overrides inside the brackets:
MOV AX,[ES:BX]
These two lines perform precisely the same job. The people who wrote NASM feel (and I concur) that it makes far more sense to place the override inside the brackets than outside. The difference is purely one of syntax. The two instructions mean precisely the same thing, right down to generating the very same binary machine code: 3E 8B 07.
Worse, when you enter the same thing in DEBUG, it must be done this way:
ES: MOV AX,[BX]
Differences in syntax will drive you crazy on occasion, especially when flipping between NASM and DEBUG. It's best to get a firm grip on what the instructions are doing, and understand what's required to make a particular instruction assemble correctly. I point out some common differences between NASM and MASM throughout this book, since MASM is by far the most popular assembler in the x86 world, and more people have been exposed to it than any other.
< Day Day Up > |
Reading and Using an Assembly Language Reference
The MOV instruction is a good start. Like a medium-sized screwdriver, you'll end up using it for normal tasks and maybe some abnormal ones, just as I use screwdrivers to pry nails out of boards, club black widow spiders in the garage bathroom, discharge large electrolytic capacitors, and other intriguing things over and above workaday screw turning. (Not all of these are a good idea ... but then again, many have said that assembly language programming isn't a good idea ...) The x86 instruction set contains dozens of instructions, however, and over the course of the rest of this book, I mix in descriptions of various other instructions with further discussions of memory addressing and program logic and design.
Remembering a host of tiny, tangled details involving dozens of different instructions is brutal and unnecessary. Even the Big Guys don't try to keep it all between their ears at all times. Most keep a blue card or some other sort of reference document handy to jog their memories about machine instruction details.
Blue Cards
A blue card is a reference summary printed on a piece of colored card stock. It folds up like a road map and fits in your pocket. The original blue card may actually have been blue, but knowing the perversity of programmers in general, it was probably bright orange.
Blue cards aren't always cards anymore. One of the best is a full sheet of very stiff shiny plastic, sold by Micro Logic Corporation of Hackensack, New Jersey. The one sold with Microsoft's MASM is actually published by Intel and has grown to a pocket-sized booklet stapled on the spine.
Blue cards contain very terse summaries of what an instruction does, which operands are legal, which flags it affects, and how many machine cycles it takes to execute. This information, while helpful in the extreme, is often so tersely put that newcomers might not quite fathom which edge of the card is up.
An Assembly Language Reference for Beginners
In deference to people just starting out in assembly language, I have put together a beginner's reference to the most common x86 instructions and called it Appendix A. It contains at least a page on every instruction I cover in this book, plus a few additional instructions that everyone ought to know. It does not include descriptions on every instruction, but only the most common and most useful. Once you've gotten skillful enough to use the more arcane instructions, you should be able to read the NASM documentation (or that of some other assembler) and run with it.
On page 213 is a sample entry from Appendix A. Refer to it during the following discussion.
The instruction's mnemonic is at the top of the page, highlighted in a box to make it easy to spot while flipping quickly through the appendix. To the mnemonic's right is the name of the instruction, which is a little more descriptive than the naked mnemonic.
Flags
Immediately beneath the mnemonic is a minichart of machine flags in the Flags register. I haven't spoken in detail of flags yet, but the Flags register is a collection of 1-bit values that retain certain essential information about the state of the machine for short periods of time. Many (but by no means all) x86 instructions change the values of one or more flags. The flags may then be individually tested by one of the JMP instructions, which then change the course of the program depending on the state of the flags.
We'll get into this business of tests and jumps in Chapter 10. For now, simply understand that each of the flags has a name, and that for each flag is a symbol in the flags minichart. You'll come to know the flags by their two-character symbols in time, but until then, the full names of the flags are shown to the right of the minichart. The majority of the flags are not used frequently in beginning assembly language work. Most of what you'll be paying attention to, flags-wise, is the Carry flag (CF). It's used, as you might imagine, for keeping track of binary arithmetic when an arithmetic operation carries out of a single byte or word.
There will be an asterisk (*) beneath the symbol of any flag affected by the instruction. How the flag is affected depends on what the instruction does. You'll have to divine that from the Notes section. When an instruction affects no flags at all, the word <none> will appear in the minichart.
In the example page, the minichart indicates that the NEG instruction affects the Overflow flag, the Sign flag, the Zero flag, the Auxiliary carry flag, the Parity flag, and the Carry flag. The ways that the flags are affected depend on the results of the negation operation on the operand specified. These ways are summarized in the second paragraph of the Notes section.
NEG Negate (Two's Complement; That Is, Multiply by −1)
Flags affected:
O D I T S Z A P C OF: Overflow flag TF: Trap flag AF: Aux carry
F F F F F F F F F DF: Direction flag SF: Sign flag PF: Parity flag
* * * * * * IF: Interrupt flag ZF: Zero flag CF: Carry flag
Legal forms: 8086/8 286 386 486 Pentium
NEG r8 X X X X X
NEG m8 X X X X X
NEG r16 X X X X X
NEG m16 X X X X X
NEG r32 X X X
NEG m32 X X X
Examples:
NEG AL
NEG ECX
NEG BYTE [BX] ; Negates byte quantity at DS:BX
NEG WORD [DI] ; Negates word quantity at DS:BX
Notes:
This is the assembly language equivalent of multiplying a value by −1. Keep in mind that negation is not the same as simply inverting each bit in the operand. (Another instruction, NOT, does that.) The process is also known as generating the two's complement of a value. The two's complement of a value added to that value yields zero. −1 = $FF; −2 = $FE; −3 = $FD; and so forth.
If the operand is 0, CF is cleared and ZF is set; otherwise, CF is set and ZF is cleared. If the operand contains the maximum negative value (−128 for 8-bit or −32768 for 16-bit), the operand does not change, but OF and CF are set. SF is set if the result is negative, else SF is cleared. PF is set if the low-order 8 bits of the result contain an even number of set (1) bits; otherwise, PF is cleared.
| Note | You must use a type override specifier (BYTE or WORD) with memory data. |
r8 = AL AH BL BH CL CH DL DH r16 = AX BX CX DX BP SP SI DI
sr = CS DS SS ES
m8 = 8-bit memory data m16 = 16-bit memory data
i8 = 8-bit immediate data i16 = 16-bit immediate data
d8 = 8 bit signed displacement d16 = 16-bit signed displacement
Legal Forms
A given mnemonic represents a single x86 instruction, but each instruction may include more than one legal form. The form of an instruction varies by the type and order of the operands passed to it.
What the individual forms actually represent are different binary number opcodes. For example, beneath the surface, the POP AX instruction is the number 58H, whereas the POP SI instruction is the number 5EH.
Sometimes there will be special cases of an instruction and its operands that are shorter than the more general cases. For example, the XCHG instruction, which exchanges the contents of the two operands, has a special case when one of the operands is register AX. Any XCHG instruction with AX as one of the operands is represented by a single-byte opcode. The general forms of XCHG (for example, XCHG r16,r16) are always 2 bytes long instead. This implies that there are actually two different opcodes that will do the job for a given combination of operands; for example, XCHG AX,DX. True enough—and some assembler programs are smart enough to choose the shortest form possible in any given situation. If you are hand-assembling a sequence of raw opcode bytes, say, for use in a higher-level language INLINE statement, you need to be aware of the special cases, and all special cases will be marked as such in the Legal forms section.
When you want to use an instruction with a certain set of operands, make sure you check the Legal forms section of the reference guide for that instruction to make sure that the combination is legal. The MOV instruction, for example, cannot move one segment register directly into another, nor can it move immediate data directly into a segment register. Neither combination of operands is a legal form of the MOV instruction, though they make sense and would be nice to have.
In the example reference page on the NEG instruction, you see that a segment register cannot be an operand to NEG. (If it could, there would be a NEG sr item in the Legal forms list.) If you want to negate the value in a segment register, you'll first have to use MOV to move the value from the segment register into one of the general-purpose registers before using NEG on the general-purpose register, and finally moving the negated value back into the segment register. (Note well that using NEG on a segment register is an almighty peculiar thing to do, and for that reason, that form of NEG was not given any transistor budget in the real mode portion of the x86 CPUs.)
Operand Symbols
The symbols used to indicate the nature of the operands in the Legal forms section are summarized at the bottom of every page in the reference appendix. They're close to self-explanatory, but I'll take a moment to expand upon them slightly here:
r8— An 8-bit register half, one of AH, AL, BH, BL, CH, CL, DH, or DL.
r16— A 16-bit general-purpose register, one of AX, BX, CX, DX, BP, SP, SI, or DI.
sr— One of the four segment registers, CS, DS, SS, or ES.
m8— An 8-bit byte of memory data.
m16— A 16-bit word of memory data.
m32— A 32-bit word of memory data.
i8— An 8-bit byte of immediate data.
i16— A 16-bit word of immediate data.
i32— A 32-bit word of immediate data.
d8— An 8-bit signed displacement. We haven't covered these yet, but a displacement is a distance between the current location in the code and another place in the code to which we want to jump. It's signed (that is, either negative or positive) because a positive displacement jumps you higher (forward) in memory, whereas a negative displacement jumps you lower (back) in memory. We examine this notion in detail in Chapter 10.
d16— A 16-bit signed displacement. Again, for use with jump and call instructions. See Chapter 10.
d32— A 32-bit signed displacement.
Examples
Whereas the Legal forms section shows what combinations of operands is legal for a given instruction, the Examples section shows examples of the instruction in actual use, just as it would be coded in an assembly language program. I've tried to put a good sampling of examples for each instruction, demonstrating the range of different possibilities with the instruction. This includes situations that require type override specifiers, which I cover in the next section.
Notes
The Notes section of the reference page describes the instruction's action briefly and provides information on how it affects the flags, how it may be limited in use, and any other detail that needs to be remembered, especially things that beginners would overlook or misconstrue.
What's Not Here ...
Appendix A differs from most detailed assembly language references in that it does not have the binary opcode encoding information, nor indications of how many machine cycles are used by each form of the instruction.
The binary encoding of an instruction is the actual sequence of binary bytes that the CPU digests and recognizes as the machine instruction. What we would call POP AX, the machine sees as the binary number 58H. What we call ADD SI,07733H, the machine sees as the 4-byte sequence 81H 0C6H 33H 77H. Machine instructions are encoded into anywhere from one to four (rarely more) binary bytes depending on what instruction they are and what their operands are. Laying out the system for determining what the encoding will be for any given instruction is extremely complicated, in that its component bytes must be set up bit by bit from several large tables. I've decided that this book is not the place for that particular discussion and have left encoding information out of the reference appendix.
Finally, I've included nothing anywhere in this book that indicates how many machine cycles are expended by any given machine instruction. A machine cycle is one pulse of the master clock that makes the PC perform its magic. Each instruction uses some number of those cycles to do its work, and the number varies all over the map depending on criteria that I won't be explaining in this book.
Furthermore, as Michael Abrash explains in his immense book Michael Abrash's Graphics Programming Black Book (Coriolis Group Books, 1997), knowing the cycle requirements for individual instructions is rarely sufficient to allow even an expert assembly language programmer to calculate how much time a given series of instructions will take. He and I both agree that it is no fit subject for beginners, and I will let him take it up in his far more advanced volume.
Rally Round the Flags, Boys!
We haven't studied the Flags register as a whole. Flags is a veritable junk drawer of disjointed little bits of information, and it's tough (and perhaps misleading) to just sit down and describe all of them in detail at once. What I do is describe the flags as we encounter them in discussing the various instructions in this and future chapters.
Flags as a whole is a single 16-bit register buried inside the CPU. Of those 16 bits, 9 are actually used as flags in real mode on the x86. The remaining 7 bits are undefined in real mode and ignored. You can neither set them nor read them. Some of those 7 bits become defined and useful in protected mode on the 386 CPU and its successors, but their uses are fairly arcane and I won't be covering them in this book.
A flag is a single bit of information whose meaning is independent from any other bit. A bit can be set to 1 or cleared to 0 by the CPU as its needs require. The idea is to tell you, the programmer, the state of certain conditions inside the CPU, so that your program can test for and act on the states of those conditions.
I often imagine a row of country mailboxes, each with its own little red flag on the side. Each flag can be up or down, and if the Smiths' flag is up, it tells the mailman that the Smiths have placed mail in their box to be picked up. The mailman looks to see if the Smiths' flag is raised (a test) and, if so, opens the Smiths' mailbox and picks up the waiting mail.
Each of the Flags register's nine flags has a two-letter symbol by which most programmers know them. I use those symbols most of the time, and you should become familiar with them. The flags, their symbols, and brief descriptions of what they stand for follows:
OF— The Overflow flag is set when the result of an operation becomes too large to fit in the operand it originally occupied.
DF— The Direction flag is an oddball among the flags in that it tells the CPU something that you want it to know, rather than the other way around. It dictates the direction that activity moves (up-memory or down-memory) during the execution of string instructions. When DF is set, string instructions proceed from high memory toward low memory. When DF is cleared, string instructions proceed from low memory toward high memory. I take this up again when I discuss the string instructions.
IF— The Interrupt enable flag is a two-way flag. The CPU sets it under certain conditions, and you can set it yourself using the STI and CLI instructions. When IF is set, interrupts are enabled and may occur when requested. When IF is cleared, interrupts are ignored by the CPU.
TF— When set, the Trap flag allows DEBUG's Trace command to do what it does, by forcing the CPU to execute only a single instruction before calling an interrupt routine. This is not an especially useful flag for ordinary programming and I won't have anything more to say about it.
SF— The Sign flag becomes set when the result of an operation forces the operand to become negative. By negative, we only mean that the highest-order bit in the operand (the sign bit) becomes 1 during a signed arithmetic operation. Any operation that leaves the sign positive will clear SF.
ZF— The Zero flag becomes set when the results of an operation become zero. If the operand becomes some nonzero value, ZF is cleared.
AF— The Auxiliary carry flag is used only for BCD arithmetic. BCD arithmetic treats each operand byte as a pair of 4-bit "nybbles" and allows something approximating decimal (base 10) arithmetic to be done directly in the CPU hardware by using one of the BCD arithmetic instructions. These instructions are not much used anymore; I discuss BCD arithmetic only briefly later on.
PF— The Parity flag will seem instantly familiar to anyone who understands serial data communications, and utterly bizarre to anyone who doesn't. PF indicates whether the number of set (1) bits in the low-order byte of a result is even or odd. For example, if the result is 0F2H, PF will be cleared because 0F2H (11110010) contains an odd number of 1 bits. Similarly, if the result is 3AH (00111100), PF will be set because there is an even number (four) of 1 bits in the result. This flag is a carryover from the days when all computer communications were done through a serial port, for which a system of error detection called parity checking depends on knowing whether a count of set bits in a character byte is even or odd. PF has no other use and I won't be describing it further.
CF— The Carry flag is by far the most useful flag in the Flags register, and the one you will have to pay attention to most. If the result of an arithmetic or shift operation "carries out" a bit from the operand, CF becomes set. Otherwise, if nothing is carried out, CF is cleared.
Check That Reference Page!
What I call "flag etiquette" is the way a given instruction affects the flags in the Flags register. You must remember that the descriptions of the flags on the previous pages are generalizations only and are subject to specific restrictions and special cases imposed by individual instructions. Flag etiquette for individual flags varies widely from instruction to instruction, even though the sense of the flag's use may be the same in every case.
For example, some instructions that cause a zero to appear in an operand set ZF, while others do not. Sadly, there's no system to it and no easy way to keep it straight in your head. When you intend to use the flags in testing by way of conditional jump instructions (see Chapter 10), you have to check each individual instruction to see how the various flags are affected.
Flag etiquette is a highly individual matter. Check the reference for each instruction to see if it affects the flags. Assume nothing!
A simple lesson in flag etiquette involves two new instructions, INC and DEC, and yet another interesting ability of DEBUG.
Adding and Subtracting One with INC and DEC
Several x86 machine instructions come in pairs. Simplest among those are INC and DEC, which increment and decrement an operand by one, respectively.
Adding one to something or subtracting one from something are actions that happen a lot in computer programming. If you're counting the number of times a program is executing a loop, or counting bytes in a table, or doing something that advances or retreats one count at a time, INC or DEC can be very quick ways to make the actual addition or subtraction happen.
Both INC and DEC take only one operand. An error will be flagged by DEBUG or by your assembler if you try to use either INC or DEC with two operands, or without any operands.
Try both by using the Assemble command and the Trace command under DEBUG. Assemble this short program, display the registers after entering it, and then trace through it:
MOV AX,FFFF
MOV BX,002F
DEC BX
INC AX
The session should look very much like this:
- A
1980:0100 MOV AX,FFFF
1980:0103 MOV BX,002D
1980:0106 INC AX
1980:0107 DEC BX
1980:0108
- R
AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=1980 ES=1980 SS=1980 CS=1980 IP=0100 NV UP EI PL NZ NA PO NC
1980:0100 B8FFFF MOV AX,FFFF
- T
AX=FFFF BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=1980 ES=1980 SS=1980 CS=1980 IP=0103 NV UP EI PL NZ NA PO NC
1980:0103 BB2D00 MOV BX,002D
- T
AX=FFFF BX=002D CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=1980 ES=1980 SS=1980 CS=1980 IP=0106 NV UP EI PL NZ NA PO NC
1980:0106 40 INC AX
- T
AX=0000 BX=002D CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=1980 ES=1980 SS=1980 CS=1980 IP=0107 NV UP EI PL ZR AC PE NC
1980:0107 4B DEC BX
- T
AX=0000 BX=002C CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=1980 ES=1980 SS=1980 CS=1980 IP=0108 NV UP EI PL NZ NA PO NC
1980:0108 0F POP CS
Watch what happens to the registers. Decrementing BX predictably turns the value 2DH into value 2CH. Incrementing 0FFFFH, on the other hand, rolls over the register to 0 since 0FFFFH is the largest unsigned value that can be expressed in a 16-bit register. Adding 1 to it rolls it over to zero, just as adding 1 to 99 rolls the rightmost two digits of the sum to zero in creating the number 100. The difference with INC is that there is no carry. The Carry flag is not affected by INC, so don't try to use it to perform multidigit arithmetic.
Using DEBUG to Watch the Flags
When INC rolled AX over to zero, the Carry flag was not affected, but the Zero flag (ZF) became set (that is, equal to 1). The Zero flag works that way: When the result of an operation becomes zero, ZF is almost always set.
DEC sets the flags in the same way. If you were to execute a DEC DX instruction when DX contained 1, DX would become zero and ZF would be set.
Apart from looking at a reference guide, how can you tell what flags are affected by a given instruction? DEBUG allows you to see the flags as they change, just as it lets you dump memory and examine the values in the general-purpose and segment registers. The second line of DEBUG's three-line register display contains eight cryptic symbols at its right margin. You've been seeing them, I'm sure, without having a clue as to their meaning.
Eight of the nine 8086/8088 flags are represented by two-character symbols. (The odd flag out is Trap flag TF, which is reserved for exclusive use by DEBUG itself and cannot be examined while DEBUG has control of the machine.) Unfortunately, the symbols DEBUG uses are not the same as the standard flag symbols that programmers call the flags by. The difference is that DEBUG's flag symbols do not represent the flags' names but rather the flags' values. Each flag can be set or cleared, and DEBUG displays the state of each flag by having a unique symbol for each state of each flag, for a total of 16 distinct symbols in all. The symbols' meanings are summarized in Table 7.4.
|
|
|
|
|---|---|---|---|
OF | Overflow flag | OV | NV |
DF | Direction flag | DN | UP |
IE | Interrupt enable flag | EI | DI |
SF | Sign flag | NG | PL |
ZF | Zero flag | ZR | NZ |
AF | Auxiliary carry flag | AC | NA |
PF | Parity flag | PE | PO |
CF | Carry flag | CY | NC |
The best I can say for this symbol set is that it's not obviously obscene. It is, however, nearly impossible to memorize. You'd best keep a reduced copy of this table (perhaps taped to the back of a business card) near your keyboard if you intend to watch the waving of the x86 CPU flags.
When you first run DEBUG, the flags are set to their default values, which are these:
NV UP EI PL NZ NA PO NC
You'll note that all these symbols are clear symbols except for EI, which must be set to allow interrupts to happen. Whether you are aware of it or not, interrupts are happening constantly within your PC. Each keystroke you type on the keyboard triggers an interrupt. Every 55 milliseconds, the system clock triggers an interrupt to allow the BIOS software to update the time and date values kept in memory as long as the PC has power. If you disabled interrupts for any period of time, your real-time clock would stop and your keyboard would freeze up. Needless to say, IE must be kept set nearly all the time.
Each time you execute an instruction with the Trace command, the flags display will be updated. If the instruction that was executed affected any of the flags, the appropriate symbol will be displayed over the previous symbol.
With Table 7.4 in hand, go back and examine the flags display for the four-instruction DEBUG trace shown a few pages back. The first display shows the default values for all the flags, since no instructions have been executed yet. No change appears for the second and third flags displays, because the MOV instruction affects none of the flags.
But look closely at the flags display after the INC AX instruction executes. Three of the flags have changed state: ZF has gone from NZ (clear) to ZR (set), indicating that the operand of INC went to zero as a result of the increment operation. AF has gone from NA to AC. (Let's just skip past that one; explaining what it means would be more confusing than helpful.) Parity flag PF has gone from PO to PE. This means that as a result of the increment operation, the number of bits present in the low byte of BX went from odd to even.
Finally, look at the last flags display, the one shown after the DEC BX instruction was executed. Again, ZF, AF, and PF changed. ZF went to NZ, indicating that the DEC instruction left a nonzero value in its operand. PF, moreover, went from PE to PO, indicating that the number of bits in the low byte of BX was odd after the DEC BX instruction.
One thing to keep in mind is that even when a flag doesn't change state from display to display, it was still affected by the previously executed instruction. Five out of nine flags are affected by every INC and DEC instruction that the CPU executes. Not every DEC instruction decrements its operand down to zero, but every DEC instruction causes some value to be asserted in ZF. The same holds true for the other four affected flags: Even if the state of an affected flag doesn't change as a result of an instruction, the state is asserted, even if only reasserted to its existing value.
Thorough understanding of the flags comes with practice and dogged persistence. It's one of the more chaotic aspects of assembly language programming, but as we'll see when we get to conditional branches, flags are what make the CPU truly come alive to do our work for us.
Using Type Specifiers
Back on the sample reference appendix page (see page 212), notice the following example uses of the NEG instruction:
NEG BYTE [BX] ; Negates byte quantity at DS:BX
NEG WORD [DI] ; Negates word quantity at DS:BX
Why BYTE [BX]? Or WORD [DI]? Used in this way, BYTE and WORD are what we call type specifiers, and you literally can't use NEG (or numerous other machine instructions) on memory data without one or the other. They are not instructions in the same sense that NEG is an instruction. They exist in the broad class of things we call directives. Directives give instructions to the assembler. In this case, they tell the assembler how large the operand is when there is no other way for the assembler to know.
The problem is this: The NEG instruction negates its operand. The operand can be either a byte or a word; in real mode, NEG works equally well on both. But ... how does NEG know whether to negate a byte or a word? The memory data operand [BX] only specifies an address in memory, using DS as the assumed segment register. The address DS:BX points to a byte-but it also points to a word, which is nothing more than two bytes in a row somewhere in memory. So, does NEG negate the byte located at address DS:BX? Or does it negate the two bytes (a word) that start at address DS:BX?
Unless you tell it somehow, NEG has no way to know.
Telling an instruction the size of its operand is what BYTE and WORD do. Several other instructions that work on single operands only (such as INC, DEC, and NOT) have the same problem and use type specifiers to resolve this ambiguity.
Types in Assembly Language
Unlike nearly all high-level languages such as Pascal and C++, the notion of type in assembly language is almost wholly a question of size. A word is a type, as is a byte, a double word, a quad word, and so on. The assembler is unconcerned with what an assembly language variable means. (Keeping track of such things is totally up to you.) The assembler only worries about how big it is. The assembler does not want to have to try to fit 10 pounds of kitty litter in a 5-pound bag, which is impossible, nor 5 pounds of kitty litter in a 10-pound bag, which can be confusing and under some circumstances possibly dangerous.
Register data always has a fixed and obvious type, since a register's size cannot be changed. BL is one byte and BX is two bytes.
The type of immediate data depends on the magnitude of the immediate value. If the immediate value is too large to fit in a single byte, that immediate value becomes word data and you can't load it into an 8-bit register half. An immediate value that can fit in a single byte may be loaded into either a byte-sized register half or a full word-sized register; its type is thus taken from the context of the instruction in which it exists and matches that of the register data operand into which it is to be loaded. But if you try to load immediate data into a destination that's too small for it, the assembler will give you an error. Here's a trivial example:
MOV BL,0FFFFH
When it encounters this, NASM will complain by saying, "Warning: Byte value exceeds bounds." BL can hold values from 0 to 0FFH. (0 to 255). The value 0FFFFH is out of bounds because it is much larger than 0FFH.
Memory data is something else again. We've spoken of memory data so far in terms of registers holding offsets without considering the use of named memory data. I discuss named memory data in the next chapter, but in brief terms, you can define named variables in your assembly language programs using such directives as DB and DW. It looks like this:
Counter DB 0
MixTag DW 32
Here, Counter is a variable allocated as a single byte in memory by the DB (Define Byte) directive. Similarly, MixTag is a variable allocated as a word in memory by the DW (Define Word) directive.
By using DB, you give variable Counter a type and hence a size. You must match this type when you use the variable name Counter in an instruction to indicate memory data. The way to do this is to use the BYTE directive, as I mentioned a little earlier. This, for example, will be accepted by the assembler:
MOV BL,BYTE [Counter]
This instruction will take the current value located in memory at the address represented by the variable name Counter and will load that variable into register half BL. You might wonder: Why do I need to put the BYTE directive there? The assembler should know that Counter is 1 byte in size because it was defined using the directive DB.
In some assemblers, including Microsoft's MASM, it would. However, NASM's authors feel that it's important to be as explicit with assemblers as possible and leave little or nothing for the assembler to infer from context. So, although NASM uses the DB directive to allocate one byte of memory for the variable Counter, it does not remember that Counter takes up only one byte when you insert Counter as an operand in a machine instruction. You must build that specification into your source code, by using the BYTE directive. This will force you to think a little bit more about what you're doing at every point that you do it; that is, right where you use variable names as instruction operands. Doing so may help you avoid certain really stupid mistakes-like the ones I used to make all the time while I was working with MASM, most of which came out of trying to let the assembler do my thinking for me.
To me, this is a wonderful thing, and one of the main reasons I chose NASM as the focus of this book.
Now here's another case, one that NASM will assemble without a burp:
MOV BL,BYTE MixTag
This looks innocent enough until you remember that MixTag is actually 2 bytes (one word) in size, having been defined with the DW directive. You might think this is an error, because MixTag isn't the same size as BL. True enough-but the key is that there's no ambiguity here. The assembler knows what you want, even if what you want is peculiar. The type specifier BYTE forces the assembler to look upon MixTag as being 1 byte in size. MixTag is not byte-sized, however, so what actually happens is that the least significant (lowermost) byte of MixTag will be loaded into BL, with the most significant byte left high and dry.
Is this useful? It can be. Is it dangerous? You bet. It is up to you to decide whether overriding the type of memory data makes sense and is completely your responsibility to ensure that doing so doesn't sprinkle your code with bugs. But nothing is left for the assembler to decide. That's what type specifiers are for: to make it clear to the assembler in every case what it is supposed to do. Whether that in fact makes sense is up to you. Use your head-and know what you're doing. That's more important in assembly language than anywhere else in computer programming.






