
Assembly Language Step by Step 1992
.pdfWorkLoop: call DoWork |
; Process the data |
||
dec |
Counter |
; |
Subtract 1 from the Counter |
jnz |
WorkLoop |
; |
If the Counter is 0. we're done! |
< more code >
The JZ instruction has been replaced with a JNZ instruction. That makes much more sense, since to close the loop we have to jump, and we only close the loop while the Counter is greater than 0. The jump back to label WorkLoop will happen only while the counter is greater than 0.
Once the counter decrements to 0, the loop is considered finished. JNZ falls through, and the code that follows the loop (which I don't show here) executes. The next instruction could be a JMP to label AllDone, as shown earlier, or it could be the next bit of work that the assembly-language program has to do. The point is that if you can position the program's next task immediately after the JNZ instruction, you don't need to use the JMP instruction at all. Instruction execution will just flow naturally into the next task that needs performing. The program will have a more natural and less tangled top-to- bottom flow, and will be easier to read and understand.
Flags
Back in Section 6.4 I explained the Flags register and briefly described the purposes of all the flags it contains. Most flags are not terribly useful, especially when you're first starting out as a programmer. The Carry flag (CF) and the Zero flag (ZF) will be 90% of your involvement in flags as a beginner, with the Direction flag (DF), Sign flag (SF) and Overflow flag (OF) together making up an additional 9.998%. It might be a good idea to reread Section 6.4 now, just in case your grasp of flag etiquette has gotten a little rusty. As explained a few pages ago, JZ jumps when ZF is 1, whereas JNZ jumps when ZF is 0. Most instructions that perform some operation on an operand (like AND, OR, XOR, INC, DEC and all arithmetic instructions) set ZF according to the results of the operation. On the other hand, instructions that simply move data around (such as MOV, XCHG, PUSH, and POP) do not affect ZF or any of the other flags. (Obviously, POPF affects the flags by popping the top-of-stack value into them.) One irritating exception is the NOT instruction, which performs a logical operation on its operand but does not set any flags— even when it causes its operand to become 0. Before you write code that depends on flags, check your instruction reference (one is almost certainly provided with your assembler) to make sure you have the flag etiquette down correctly.
Comparisons with CMP
One major use of flags is in controlling loops. Another is in comparisons between two values. Your programs will often need to know whether a value in a register or memory is equal to some other value. Further, you may want to know if a value is greater than a value or less than a value if it is not equal to that value. There is a jump instruction to satisfy every need, but something has to set the flags for the benefit of the jump instruction. The compare (CMP) instruction is what sets the flags for comparison tasks. CMP's use is straightforward and intuitive. The second operand is compared with the first, and several flags are set accordingly:
cmp <opl>,<op2> ; Sets OF, SF, ZF, AF, PF, and CF
The sense of the comparison can be remembered if you simply recast the comparison in arithmetic terms:
Result = <op1 > - <op2 >
CMP is a subtraction operation where the result of the subtraction is thrown away, and only the flags are affected. The second operand is subtracted from the first. Based on the results of the subtraction, the flags are set to appropriate values.
After a CMP instruction, you can jump based on several arithmetic conditions. People who have a fair grounding in math, or are FORTRAN or Pascal programmers will recognize the conditions: equal, not equal, greater than, less than, greater than or equal to, and less than or equal to. The sense of these operators follows from their names, and is exactly like the sense of the equivalent operators in most high-level languages.
A Jungle of Jump Instructions
There is a bewildering array of jump instruction mnemonics, but those dealing with arithmetic relationships sort out well into just six categories, one category for each of the six conditions listed above. Complication arises out of the fact that there are two mnemonics for each machine instruction, for example, JLE (Jump if Less than or Equal) and JNG (Jump if Not Greater than). These two mnemonics are synonyms, in that the assembler generates the identical binary opcode when it encounters either mnemonic.
The synonyms are a convenience to you the programmer, in that they provide two alternate ways to think about a given jump instruction. In the above example, jump if less than or equal to is logically identical to jump if not greater than. (Think about it!) If the importance of the preceding compare was to see if one value is less than or equal to another, you'd use the JLE mnemonic. On the other hand, if you were testing to be sure one quantity was not greater than another, you'd use JNG. The choice is yours.
Another complication is that there is a separate set of instructions for signed and unsigned comparisons. I haven't spoken much about assembly-language math in this book, and thus haven't said much about the difference between signed and unsigned quantities. A signed quantity is one in which the high bit of the quantity is considered a built-in flag that indicates whether or not the quantity is negative. If that bit is 1, the quantity is considered negative; if that bit is 0, the quantity is considered positive. Signed arithmetic in assembly language is complex and subtle, and not as useful as you might immediately think. I won't be covering it in detail in this book, though most all assembly language books treat it to some extent. All you need know to get a high-level understanding of signed arithmetic is that in signed arithmetic, negative quantities are legal. Unsigned arithmetic, on the other hand, does not recognize negative numbers.
Greater Than vs. Above
To tell the signed jumps apart from the unsigned jumps, the mnemonics use two different expressions for the relationships between two values:
•Signed values are thought of as being greater than or less than. For example, to test whether one signed operand is greater than another, you would use the JG (Jump if Greater) mnemonic after a CMP instruction.
•Unsigned values are thought of as being above or below. For example, to tell
whether one unsigned operand is greater (above) another, you would use the JA (Jump if Above) mnemonic after a CMP instruction.
Table 9.6 summarizes the arithmetic jump mnemonics and their synonyms. Any mnemonics containing the words above or below are for unsigned values, while any mnemonics containing the words greater or less are for signed values. Compare the mnemonics with their synonyms and see how the two represent opposite viewpoints from which to look at identical
instructions.
Table 9.6 simply served to expand the mnemonics into a more comprehensible form and associate a mnemonic with its synonym. Table 9.7, on the other hand, sorts the mnemonics out by logical condition and according to their use with signed and unsigned values. Also listed in Table 9.7 are the flags whose values are considered in each jump instruction. Notice that some of the jump instructions require one of two possible flag values in order to take the jump, while others require both of two flag values.
Several of the signed jumps compare two of the flags against one another. JG, for example, will jump when either ZF is 0, or when the Sign flag (SF) is equal to the Overflow flag (OF). I won't spend any further time explaining the nature of the Sign flag or Overflow flag. As long as you have the sense of each instruction under your belt, understanding exactly how the instructions test the flags can wait until you've gained some programming experience.
Some people have trouble understanding how it is that the JE and JZ mnemonics are synonyms, as are JNE and JNZ. Think again of the way a comparison is done within the CPU: the second operand is subtracted from the first, and if the result is 0 (indicating that the two operands were in fact equal), ZF is set to 1. That's why JE and JZ are synonyms: both are simply testing the state of ZF.
.
Detecting the Installed Display Adapter
A useful example of CMP and the conditional JMP instructions in action involves detecting the installed display adapter. Five different mainstream IBM display adapters that can be installed in a PC (from the first generation introduced with the original PC in 1981 to the VGA and MCGA introduced with the PS/2 series in 1987) are currently available. (I don't consider the PGC and the XGA to be mainstream—although the XGA will almost certainly get there in time.) Each adapter has certain unique features, and if you intend to use some of the (rather nifty) hardware assistance offered by the more advanced video boards like the EGA and VGA, you had better be prepared to tell which board is in a given machine. Then your program must decide what special features can and cannot be used.
It isn't quite enough to know which board is installed in a given machine. The way a certain board operates can change severely depending on whether a monochrome or color monitor is attached to the board. The most obvious difference (and the one of most interest to the programmer) is that the memory address of the video display buffer is different for color and monochrome monitors. This schizophrenic quality of the EGA, VGA, and MCGA is so pronounced that it makes sense to consider the EGA/color monitor combination an entirely separate display adapter from the EGA/monochrome

monitor combination.
In my method, I use a separate numeric code to represent each legal adapter/monitor combination. There are nine possibilities in all, summarized in Table 9.8.
The codes are not consecutive; note that there is no code 3, 6 or 9. I didn't make these codes up arbitrarily. They are in fact the display adapter/monitor combination codes returned by one of the VGA/MCGA BIOS services.
The DispID procedure given below determines which display adapter is installed in the machine in which DispID is running. DispID then returns one of the codes listed in Table 9.8.
Table 9.8. Legal PC display adapter/monitor combinations
Code |
Adapter/Monitor |
Segment of Display Buffer |
00 |
None |
None |
01H |
MDA/Monochrome |
0B000H |
02H |
CGA/Color |
0B800H |
04H |
EGA/Color |
0B800H |
05H |
EGA/Monochrome |
0B000H |
07H |
VGA/Monochrome |
0B000H |
08H |
VGA/Color |
0B800H |
0AH |
MCGA/Color (digital) |
0B800H |
0BH |
MCGA/Monochrome |
0B000H |
|
|
|
0CH |
MCGA/Color (analog) |
0B800H |
I recommend that your programs define a byte-sized variable in their data segments where this code can be stored throughout the program's duration. If you detect the adapter with DispID immediately on program startup, your program can inspect the code any time it needs to make a decision as to which video features to use.
Given what I've told you about CMP and conditional jump instructions so far, see if you can follow the logic in DispID before we go through it blow by blow:
DispID is the most complex piece of code shown so far in this book. The overall strategy is not obvious and bears some attention.
IBM's standard display boards appeared in three generations. The first generation consisted of the original Color Graphics Adapter (CGA) and Mono-chrome Display
Adapter (MDA). The second generation consisted of the Enhanced Graphics Adapter (EGA.) Finally, the third generation came in with the PS/2 in April of 1987 and provided the Video Graphics Array (VGA) and Multi-Color Graphics Array (MCGA).
The simplest way to find out what display board is installed in a machine is to "ask the machine" by querying BIOS services. There are BIOS services specific to each generation of display board, and by some quirk of fate all such services are well behaved, by which I mean that querying a service that doesn't exist (because an older generation of video board is installed) will not crash the system. (IBM's BIOS standard is extremely "downward compatible" in that newer generations all contain everything the older generations do.) Furthermore, if a BIOS service specific to a generation of boards is found not to exist, that tells us that the installed board is not a member of that generation or a newer generation.
Assuming that the target machine could have any of the standard IBM display boards in it, it makes sense to test for the presence of the newest boards first. Then, through a process of elimination, we move to the older and older boards.
The first test that DispID makes, then, is for the VGA or MCGA generation. The PS/2 machines contain in their ROM BIOS a service (VIDEO Service 1AH) specifically to identify the installed display adapter. DispID calls VIDEO service 1AH, having cleared AL to 0 via XOR. As it happens, if a PS/2 BIOS is present on the bus, the 1AH service number is returned in register AL. On return from the INT 10H call, we test AL for 1AH using CMP. If 1AH is not found in AL, we know up front that there is no PS/2 BIOS in the system, and therefore no VGA or MCGA.

After the CMP instruction is the JNE TryEGA conditional branch. If the CMP finds
that AL is not equal to 1AH, then control jumps down to the code that tests for the next older generation of video boards: the EGA. If AL is equal to 1AH, then the PS/2 BIOS has placed the display adapter code in BL. DispID then copies BL into AL (which is where DispID returns the display code) and executes a RET instruction to pass control back to the caller.
Testing for the EGA is done a little differently, but the same general idea holds: we call an EGA-specific VIDEO service not present in the oldest generation of boards. The key test, again, is whether a certain register comes back unchanged. There is a twist, however: if BX comes back with the same value it held when the VIDEO call was made, (here, 10H) then an EGA BIOS does not exist in the machine. (Isn't the PC wonderful?) Here, after the CMP BX,10H instruction, we do a JE OldBords and not a JNE as we did when testing for the PS/2 generation. If BX comes back in an altered state, we assume an EGA is present, and that BX contains information on the display configuration.
If an EGA BIOS is found, a value in BH tells us whether the EGA is connected to a monochrome or color monitor. (Remember, there is a different code for each.) The value in BH is not the code itself, as it was with the PS/2 BIOS, so we have to do a little more testing to get the right code into AL. If BH contains 0, then the attached monitor is color. Any other value in BH indicates a monochrome system. The following sequence of instructions from DispID takes care of loading the proper EGA-specific code into AL:
cmp BH,0 |
|
;If BH - 0, it's an EGA/color combo |
|
je EGAColor |
; otherwise it's EGA/mono |
||
mov AL,5 |
|
; Store code 5 for EGA mono |
|
ret |
|
; and go home! |
|
EGAColor: |
mov AL,4 |
; Store code 4 for EGA color |
|
ret |
|
; and go home! |
You'll find yourself writing sequences like this a lot when a single test decides between one of two courses of action. One course here is to load the value 5 into AL, and the other course is to load 4 into AL. Notice that after the appropriate MOV instruction is executed, a RET takes care of passing execution back to the caller. If DispID were not a procedure, but simple a sequence coded into the main line of instructions, you would need an unconditional JMP after each MOV to continue on with instruction execution
somewhere else in the program. Using RET is much neater—which is yet another reason to explore small tasks like display adapter identification in a procedure wrapper. Finally, if neither PS/2 nor EGA are present, DispID realizes that, by default, one of the original generation of display boards is on the bus. Telling MDA from CGA is not done with a BIOS call at all, because the first generation BIOS did not know which display board was present. (That was a feature instituted with the EGA in 1984.) Instead, there is a separate software interrupt, 11H, that returns machine configuration information.
Testing Bits with TEST
Service 11H returns a word's worth of bits in AX. Singly or in twos or threes, the bits tell a tale about specific hardware options on the installed PC. These hardware options are summarized in Figure 9-5.
The bits we need to examine are bits 4 and 5. If both are set to 1, then we know we have a Monochrome Display Adapter. If the two bits are set to any other combination, the adapter must be a Color Graphics Adapter; all other alternatives have by this time been eliminated.
Testing for two 1 bits in a byte is an interesting exercise. The 86-family instruction set recognizes that assembly-language programmers do a lot of bit testing, and provides what amounts to a CMP instruction for bits: TEST.