Google
 

Tuesday, February 27, 2007

Assembly Language Step-by-Step Programming with DOS and Linux Second Edition Reading Note

Chapter 7: Following Your Instructions Meeting Machine Instructions up Close and Personal

Rally Round the Flags, Boys!

Adding and Subtracting One with INC and DEC

1. Simplest among those are INC and DEC, which increment and decrement an operand by one, respectively.

2. Both INC and DEC take only one operand.

3. 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

1. Eight of the nine 8086/8088 flags are represented by two-character symbols.

2. 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.

3. DEBUG’s Flag State Symbols

FLAG

NAME

SET SYMBOL

CLEAR SYMBOL

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

4. 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.

5. 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.

Using Type Specifiers

1. Telling an instruction the size of its operand is what BYTE and WORD do. Used in this way, BYTE and WORD are what we call type specifies. 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.

Types in Assembly Language

1. 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.

2. 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.

3. Register data always has a fixed and obvious type, since a register's size cannot be changed.

4. The type of immediate data depends on the magnitude of the immediate value.

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
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.
         MOV BL,BYTE [Counter]

5. 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.

Chapter 8: Our Object All Sublime Creating Programs that Work

Overview

This is the best way to learn to assemble: By pulling apart programs written by those who know what they're doing.

The Bones of an Assembly Language Program

1. This issue of comprehensibility is utterly central to quality assembly language programming.

2. One of the aims of assembly language coding is to use as few instructions as possible in getting the job done. This does not mean creating as short a source code file as possible.

3. Comments are neither time nor space wasted. IBM used to say, One line of comments per line of code. That's good-and should be considered a minimum for assembly language work.

EAT.ASM file

The Simplicity of Flat Model

1. I recommend placing a summary comment block like this at the top of every source code file you create.

2. Beneath the comment block is a short sequence of commands directed to the assembler. These commands are placed in square brackets so that NASM knows that they are for its use, and are not to be interpreted as part of the program.

3. The BITS command tells NASM that the program it's assembling is intended to be run in real mode, which is a 16-bit mode. Using [BITS 32] instead would have brought into play all the marvelous 32-bit protected mode goodies introduced with the 386 and later x86 CPUs.

4. "ORG" is an abbreviation of origin, and what it specifies is sometimes called the origin of the program, which is where code execution begins. Code execution begins at 0100H for this program. The 0100h value (the h and H are interchangeable) is loaded into the instruction pointer IP by DOS when the program is loaded and run. Why 0100H? The real mode flat model (which is often called the .COM file model) has a 256-byte prefix at the beginning of its single segment. This is the Program Segment Prefix (PSP) and it has several uses that I won't be explaining here. The PSP is basically a data buffer and contains no code. The code cannot begin until after the PSP, so the 0100H value is there to tell DOS to skip those first 256 bytes.

5. NASM divides your programs into what it calls sections. These sections are less important in real mode flat model than in real mode segmented model, when sections map onto segments. (More on this later.) In flat model, you have only one segment. But the SECTION commands tell NASM where to look for particular types of things. In the .text section, NASM expects to find program code. In the .data section, NASM expects to find the definitions for your initialized variables. A third section is possible, the .bss section, which contains uninitialized data.

Labels

1. A label is a sort of bookmark, holding a place in the program code and giving it a name that's easier to remember than a memory address. The START: label indicates where the program begins. Technically speaking, the START: label isn't necessary in EAT.ASM. You could eliminate the START: label and the program would still assemble and run. However, I think that every program should have a START: label as a matter of discipline. That's why EAT.ASM has one.

2. Labels are used to indicate where JMP instructions should jump to.

3. The only distinguishing characteristic of labels is that they're followed by colons. Some rules govern what constitutes a valid label:

· Labels must begin with a letter or with an underscore, period, or question mark. These last three have special meanings (especially the period), so I recommend sticking with letters until you're way further along in your study of assembly language and NASM.

· Labels must be followed by a colon when they are defined. This is basically what tells NASM that the identifier being defined is a label. NASM will punt if no colon is there and will not flag an error, but the colon nails it, and prevents a misspelled mnemonic from being mistaken for a label. So use the colon!

· Labels are case sensitive. So yikes:, Yikes:, and YIKES: are three completely different labels. This differs from practice in a lot of languages (Pascal particularly) so keep it in mind.

4. The colon is only placed where the label is defined, not where it is referenced.

Variables for Initialized Data

1. The identifier eatmsg defines a variable. Specifically, eatmsg is a string variable (more on which follows) but still, as with all variables, it's one of a class of items we call initialized data: something that comes with a value, and not just a box that will accept a value at some future time. A variable is defined by associating an identifier with a data definition directive. Data definition directives look like this:

  MyByte      DB 07H            ; 8 bits in size
  MyWord      DW 0FFFFH         ; 16 bits in size
  MyDouble    DD 0B8000000H     ; 32 bits in size

Think of the DB directive as "Define Byte." DB sets aside one byte of memory for data storage. Think of the DW directive as "Define Word." DW sets aside one word of memory for data storage. Think of the DD directive as "Define Double." DD sets aside a double word in memory for storage, typically for full 32-bit addresses.

2. I find it useful to put some recognizable value in a variable whenever I can, even if the value is to be replaced during the program's run.

String Variables

1. String variables are an interesting special case. A string is just that: a sequence or string of characters, all in a row in memory.

2. Strings are a slight exception to the rule that a data definition directive sets aside a particular quantity of memory. The DB directive ordinarily sets aside one byte only. However, a string may be any length you like, as long as it remains on a single line of your source code file. Because there is no data directive that sets aside 16 bytes, or 42, strings are defined simply by associating a label with the place where the string starts. The eatmsg label and its DB directive specify one byte in memory as the string's starting point. The number of characters in the string is what tells the assembler how many bytes of storage to set aside for that string.

3. Either single quote (') or double quote (") characters may be used to delineate a string, and the choice is up to you, unless you're defining a string value that itself contains one or more quote characters.

4. You may combine several separate substrings into a single string variable by separating the substrings with commas.

5. Both add a dollar sign ($) in quotes to the end of the main string data. The dollar sign is used to mark the end of the string for the mechanism that displays the string to the screen.

6. What, then, of the "13,10" in eatmsg? Inherited from the ancient world of electromechanical Teletype machines, these two characters are recognized by DOS as meaning the end of a line of text that is output to the screen. If anything further is output to the screen, it will begin at the left margin of the next line below. You can concatenate such individual numbers within a string, but you must remember that they will not appear as numbers.

7. A string is a string of characters. A number appended to a string will be interpreted by most operating system routines as an ASCII character.

Directives versus Instruction Mnemonics

1. Data definition directives look a little like machine instruction mnemonics, but they are emphatically not machine instructions.

2. There is no binary opcode for DW, DB, and the other directives.

3. Machine instructions, as the name implies, are instructions to the CPU itself. Directives, by contrast, are instructions to the assembler.

4. Understanding directives is easier when you understand the nature of the assembler's job. The assembler scans your source code text file, and as it scans your source code file it builds an object code file on disk. It builds this object code file step by step, one byte at a time, starting at the beginning of the file and working its way through to the end. When it encounters a machine instruction mnemonic, it figures out what binary opcode is represented by that mnemonic and writes that binary opcode (which may be anywhere from one to six actual bytes) to the object code file. When the assembler encounters a directive such as DW, it does not write any opcode to the object code file. DW is a kind of signpost to the assembler, reading "Set aside two bytes of memory right here, for the value that follows." The DW directive specifies an initial value for the variable, and so the assembler writes the bytes corresponding to that value in the two bytes it set aside. The assembler writes the address of the allocated space into a table, beside the label that names the variable. Then the assembler moves on, to the next directive (if there are further directives) or to whatever comes next in the source code file.

The Difference between a Variable's Address and Its Contents

1. When you place a variable's identifier in a MOV instruction, you are accessing the variable's address, as explained previously. By contrast, if you want to work with the value stored in that variable, you must place the variable's identifier in square brackets.

2. There are many situations in which you need to move the address of a variable into a register rather than the contents of the variable. In fact, you may find yourself moving the addresses of variables around more than the contents of the variables, especially if you make a lot of calls to DOS and BIOS services.

3. In assembly language, knowing where a variable is located is essential in order to do lots of important things.

Making DOS Calls

1. Calling DOS is done with something called a software interrupt.

2. The DOS call has certain requirements that must be set up before the call is made. It must know what particular call you want to make, and each call has a number. This number must be placed in AH

Exiting the Program and Setting ERRORLEVEL

1. Another DOS service, 4CH (Terminate Process), handles the mechanics of courteously disentangling the machine from EAT.ASM's clutches. Terminate Process doesn't need the address of anything, but it will take whatever value it finds in the AL register and place it in the ERRORLEVEL DOS variable. DOS batch programs can test the value of ERRORLEVEL and branch on it.(I tried to test the ERRORLEVEL’value after executed the EAT.COM, but it is not zero. Why?)

Assembling and Running EAT.ASM

1. The "BIN" indicates "binary image."

2. The other key thing about .COM files is the 0100H code origin, but that's handled in the source code, as I explained earlier.

What Happens When a .COM File Runs

1. DOS treats its two kinds of executable programs a little differently when it runs them. .COM files are the simpler of the two.

2. .COM files are a simple image of the instructions and data assembled out of the source code file.

3. When you execute a .COM program from the DOS command line, here's what happens:

1. The .COM file is loaded into memory at a location of DOS's choosing. It doesn't change the file when it loads the file; the file is loaded exactly as it was saved to disk by the assembler.

2. AX, BX, DX, BP, SI, and DI are set to 0.

3. The instruction pointer IP is set to 0100H.

4. The number of bytes loaded from disk and stored into memory is placed in the CX register.

5. The stack pointer is set to the highest address in the program's segment, minus one.

6. All four segment registers CS, SS, DS, and ES are set to the same value: the segment address of the single segment in which the .COM program must run. DOS chooses this value.

7. DOS transfers control to the instruction at CS:IP, and your program is off and running!

4. You'll sometimes hear the real mode flat model referred to as the Tiny model. This is a term that originated in the C programming community, which has separate names for several different arrangements of code and data, depending on whether there is a single segment for code and data or multiple segments.

One Program, Three Segments

The main problem with real mode flat model is that everything you do must fit into 64K of memory. This isn't much of a pinch for learning assembly language and just playing around writing small utilities, but once you try to create something ambitious-say, a word processor or database-driven e-mail client-you find that code and data begin to crowd one another in a big hurry. So, for all its trouble, real mode segmented model was the only way to make full use of real mode's megabyte of memory.

Three Segments

1. Assembly language programs written for real mode segmented model must contain at least three segments: One for code, one for data, and one for the stack. Larger programs may contain more than one code segment and more than one data segment, but real mode programs may contain only one stack segment at a time.

2. The code segment, pretty obviously, contains the machine instructions that do the program's work. The data segment contains initialized variables. The stack segment contains the program's stack.

3. Each of the three segments is declared using the SEGMENT directive, which is a command that tells NASM that a segment begins here. The SEGMENT directive must be followed by the segment's name. You can name the segments whatever you like, but custom suggests that when you have only three segments, they be called stack,data, and code.

The segment containing the stack has some special considerations attached to it, especially regarding the linking of several files together into one executable program. One of these considerations is that the stack has the type "stack" attached to it. This tells the linker (as I explain later) that this particular segment is special-it's a stack segment and not just a data segment.
 SEGMENT stack stack

The SEGMENT directive is creating a stack named "stack" that is of the type "stack." The first identifier is the name; the second is the type. You could change the name of the segment to MyStack or GreasedPig if you like, but it's important to let the type of the stack segment be precisely stack.

Don't ASSUME…

1. In the real mode flat model, the operating system sets all four segment registers to the same value (one that it selects) when the program is loaded into memory and run.

2. In the real mode segmented mode, the different segments are indeed different and distinct regions of memory and are not all the same place. When the program begins running, DOS doesn't set the segment registers to anything. Your program must do that on its own. (DOS does, of course, set CS to the start of the code segment before giving control to your program. The other segment registers it leaves alone.)

3. Keep in mind that you can only load a segment register from a general-purpose register-you can't load it from anything else, either immediate data or memory data. This is why the segment addresses have to pass through AX to get into DS and SS. (Because we're not using ES to govern a segment defined at assembly time right there in our program, we don't need to load ES with anything right off the bat.)

4. Difference between MASM and NASM: MASM attempts to associate segment names with segment types. NASM does not. With one small exception done as a courtesy to the linker, NASM does not know which segment is the code segment, nor which segment is the data segment, nor which segment is the stack segment. In MASM, Microsoft defines the ASSUME directive, which associates segment names with segment registers. This allows MASM to generate segment prefixes automatically when it creates the opcodes called out by a particular mnemonic in your source code. So, NASM has nothing like ASSUME. When you move away from the default addressing of memory variables relative to DS, you must include the segment register prefix inside the square brackets of all memory variable references!

Naming the Stack Segment

1. NASM allows you to say which segment is the stack segment.

2. This is not for NASM's benefit-it will not take any action of its own based on knowing that the segment named MyStack is in fact the stack segment. But some linkers need to know that there is a stack segment defined in the program.

3. Some linkers check to see whether there is a segment in a program designated as the stack segment, and to keep such linkers quiet NASM allows you to give the stack type to a segment defined with SEGMENT. This is a good idea and I recommend that you do it.

Choosing a Starting Point

1. Code execution has to begin somewhere, and you need to tell the assembler (and especially the linker) where code execution must begin. This is the purpose of the ..start: label.

2. DOS needs to know at what address to begin execution when it loads and runs the program. (DOS sets code segment register CS when it loads your program into memory prior to executing it.) You might think DOS could assume that execution would begin at the start of the code segment, but there may be more than one code segment, and under most circumstances the programmer does not specify the order of multiple code segments within a single program. (The linker has the power to rearrange multiple code segments for reasons that I can't explain in this book.) Better to have no doubt about it, and for that reason you the programmer should pick a starting point and tell the assembler what it is.

Assembling and Linking EATSEG.ASM

1. Although NASM can generate a .COM file (for a real mode flat model program) directly, it can't generate a .EXE file for a real mode segmented model program in the same way. Once you move away from a single segment in real mode flat model, NASM needs the help of a linker to generate the final .EXE file.

2. NASM-IDE was intended for writing programs in real mode flat model, so it relies exclusively on NASM and does not have any machinery for invoking a linker. That means that NASM-IDE won't be able to do the assemble and link tasks for us.

3. Using linker:

C:\>NASM16 EATSEG.ASM -f obj -o EATSEG.OBJ
C:\>ALINK EATSEG.OBJ

(This command line will assemble EATSEG.ASM to the file EATSEG.OBJ, in the standard .OBJ linkable file format.)

Last In, First Out via the Stack

1. The x86 CPUs contain the machinery to create and manage a vital storage area called the stack.

Five Hundred Plates an Hour

We call it a last in, first out, or LIFO stack.

An Upside-Down Segment

1.Two of the x86 registers team up to create and maintain the stack. Like everything else in 86-land, the stack must exist within a segment. The SS (Stack Segment) register holds the segment address of the segment chosen to be the stack segment, and the SP (Stack Pointer) register points to locations within the stack segment.

2.As with all other segments in real mode, the stack segment may be as much as 65,536 bytes long, but it may be any length less than that as well.

3.The stack segment begins at SS:0, but the truly odd thing about it is that all the stack action happens at the opposite end of the stack segment. When a stack segment is set up, the SS register points to the base or beginning of the stack segment, and SP is set to point to the end of the stack segment. To store something in the stack segment (which we usually call "pushing something onto the stack"), we move SP "down the stack" (that is, closer to SS) and then copy the item to the memory location pointed to by SS:SP.

4.In real mode flat model, SS is set to the base of the stack segment by DOS when the program is loaded and begins running. (And all the other segment registers are set to the same address.) In real mode segmented model, you set SS from the address of the segment that you define within the program in two steps, first using NASM's SEGMENT directive. Then you need a couple of MOV instructions to get the address of segment stack into SS.

5.Defining a stack segment just provides a starting point address for that segment. No room is actually reserved for the stack by the SEGMENT directive.

6.RESB means "REServe Byte." And it means just that: It tells the assembler to set aside some bytes starting at the beginning of the stack segment and not to let anything else (such as memory variables) be defined in that reserved space.

7.Note that you don't need to use RESB to reserve stack space if you're working in real mode flat model. The stack in that model exists at the very highest addresses of the single segment the program lives in. The space isn't reserved in the strictest sense, and you have to be careful not to let your code or data get so high in memory that it collides with your stack. This is called a stack crash.

8. Note that you don't need to use RESB to reserve stack space if you're working in real mode flat model. The stack in that model exists at the very highest addresses of the single segment the program lives in. The space isn't reserved in the strictest sense, and you have to be careful not to let your code or data get so high in memory that it collides with your stack. This is called a stack crash.

9. SP is set to the far (that is, the high, address-wise) end of the stack segment. Again, if you're working in real mode flat model, DOS does it when your program is loaded in any case, something fairly high rather than close to 0000H.

10. And if you're working in real mode segmented model, you have to set SP yourself. resb 64 ; Reserve 64 bytes for the program stack

    stacktop:       ; It's significant that this label points to
                  ;  the *last* of the reserved 64 bytes, and
                  ;  not the first!

Note that the label stacktop: is immediately after the RESB 64 directive. The label stacktop: represents an address at the very end of the block of reserved memory locations set aside by RESB. Although the position of the two lines on the source code listing suggests that stacktop: points beyond the block of memory set aside by RESB, that's not the case. The stacktop: label resolves to the offset of the last byte in that block of 64 bytes.

Pushing Data

1. You can place data onto the stack in numerous ways, but the most straightforward way involves a trio of related machine instructions, PUSH, PUSHF, and PUSHA(PUSHAD). The three are similar in how they work, and differ as to what they push onto the stack. PUSHF pushes the Flags register onto the stack. PUSHA(PUSHAD) pushes all eight of the 16-bit general-purpose registers. PUSH pushes a 16-bit register or memory value that is specified by you in your source code.

2. PUSH and PUSHF work this way: First SP is decremented by one word (two bytes) so that it points to an empty area of the stack segment that is two bytes long. Then whatever is to be pushed onto the stack is written to memory in the stack segment at the offset address in SP. One thing to remember is that PUSHA is a newer instruction that doesn't exist on the 8086 and 8088. It first appeared with the 286. PUSHAD was added with the 386, and it pushes all eight 32-bit general-purpose registers onto the stack in one blow.

3. All memory between SP's initial position and its current position (the top of the stack) contains real data that was explicitly pushed on the stack and will presumably be fetched from the stack (we say popped from the stack) later on.

4. In real mode segmented model, the stack exists in a separate segment, and memory between SS and SP is considered free and available and is used to store new data that is to be pushed onto the stack. This is not the case in real mode flat model, where the stack shares the same segment that everything else in the program is using.

5. What can and cannot be pushed onto the stack is complicated and depends on what CPU you're using.

a. None of the x86 CPUs can push 8-bit registers onto the stack.

b. Segment registers and 32-bit extended general-purpose registers can be pushed in real mode, assuming you have a 386 or later CPU.

c. Similarly, immediate data can be pushed onto the stack, but only if you have a 286 or later CPU.

6. Stack crashes are serious business, at least in part because there is only one stack in action at a time in real mode.

POP Goes the Opcode

1. Getting a word of data off the stack is done with another trio of instructions, POP, POPF, andPOPA. As you might expect, POP is the general-purpose one-at-a-time popper, while POPF is dedicated to popping the flags off of the stack. POPA pops 16 bytes off the stack into the eight general-purpose 16-bit registers. POPAD is the flip side of PUSHAD and pops the top 32 bytes off the stack into the eight general-purpose 32-bit registers.

2. As with PUSH, POP only operates on word-sized operands. Don't try to pop data from the stack into an 8-bit register such as AH or CL.

3. POP works pretty much the way PUSH does, but in reverse: First the word of data at SS:SP is copied from the stack and placed in POP's operand, whatever you specified that to be. Then, SP is incremented (rather than decremented) by two bytes, so that in effect it moves two bytes up the stack, away from SS.

4. It's significant that SP is decremented before placing a word on the stack at push time, but incremented after removing a word from the stack at pop time. Certain other CPUs work in the opposite manner, which is fine—just don't get confused. Unless the stack is empty, SP points to real data, not empty space.

5. Its least significant byte is at SS:SP, and its most significant byte is at SS:SP+1. (Remember that both bytes are pushed onto the stack at once, as a unit!)

Storage for the Short Term

1. The stack should be considered a place to stash things for the short term.

2. Last in, first out, remember. LIFO!

3. One excellent use of the stack allows the all-too-few registers to do multiple duty. If you need a register to temporarily hold some value to be operated on by the CPU and all the registers are in use, push one of the busy registers onto the stack. Its value will remain safe on the stack while you use the register for other things. When you're finished using the register, pop its old value off the stack—and you've gained the advantages of an additional register without really having one. (The cost, of course, is the time you spend moving that register's value onto and off of the stack. It's not something you want to do in the middle of an often-repeated loop!)

Using DOS Services through INT

1. DOS and BIOS both offer services, which are simple tasks that your programs would have to do themselves if the services were not provided.

2. Quite apart from saving you the programmer a lot of work, having DOS and BIOS services helps guarantee that certain things will be done in identical fashion on all machines, which (especially in terms of disk storage) is a major reason software written for DOS runs on so many different machines: All the machine-dependent stuff is done the same way.

3. So—how do we use DOS and BIOS services? The way is as easy to use as it is tricky to understand: through software interrupts.

An Interrupt That Doesn't Interrupt Anything

1. The solution is ingenious. At the very start of real mode memory, down at segment 0, offset 0, is a special table with 256 entries. Each entry is a complete address including segment and offset portions, for a total of 4 bytes per entry. The first 1,024 bytes of memory in any x86 machine are reserved for this table, and no code or data may be placed there.

2. Each of the addresses in the table is called an interrupt vector. The table as a whole is called the interrupt vector table. Each vector has a number, from 0 to 255. The vector occupying bytes 0 through 3 in the table is vector 0. The vector occupying bytes 4 through 7 is vector 1, and so on.

3. None of the addresses is burned into permanent memory the way BIOS routines are. When your machine starts up, DOS and BIOS fill many of the slots in the interrupt vector table with addresses of certain service routines within themselves. Each version of DOS knows the location of its innermost parts, and when you upgrade to a new version of DOS, that new version will fill the appropriate slots in the vector table with upgraded and accurate addresses.

4. What doesn't change from DOS version to DOS version is the number of the interrupt that holds a particular address.

5. The x86 CPUs include a machine instruction that makes use of the interrupt vector table. The INT (INTerrupt) instruction is.

6. When DOS loads itself at boot time, one of the many things it does to prepare the machine for use is put correct addresses in several of the vectors in the interrupt vector table.

7. DOS service 09H has a slightly odd requirement: That the end of the string be marked with a dollar sign ($).

8. Given that DOS does not ask us to pass it a value indicating how long the string is, the end of the string has to be marked somehow, and the dollar sign is DOS's chosen way. It's a lousy way, unfortunately, because with the dollar sign acting as a marker, there is no way to display a dollar sign. If you intend to talk about money on the PC's screen, don't use DOS service 9! As I said, this is the easiest, but certainly not the best way to display text on the screen.

Getting Home Again

1. Just as the INT instruction pushes a return address onto the stack and then jumps to the address stored in a particular vector, there is a "combination" instruction that pops the return address off the stack and then jumps to the address. The instruction is IRET (for Interrupt RETurn), and it completes this complex but reliable system of jumping toan address when you really don't know the address.

Software Interrupts versus Hardware Interrupts

1. Software interrupts evolved from an older mechanism that did involve some genuine interrupting: hardware interrupts. A hardware interrupt is your CPU's mechanism for paying attention to the world outside itself.

2. There is a fairly complex electrical system built into your PC that allows circuit boards to send signals to the CPU. An actual metal pin on the CPU chip is moved from one voltage level to another by a circuit board device like a disk drive controller or a serial port board. Through this pin, the CPU is tapped on the shoulder by the external device. The CPU recognizes this tap as a hardware interrupt. Like software interrupts, hardware interrupts are numbered, and for each interrupt number there is a slot reserved in the interrupt vector table. In this slot is the address of an interrupt service routine (ISR) that performs some action relevant to the device that tapped the CPU on the shoulder.

3. Most properly, any routine that lies at the end of a vector address in the interrupt vector table is an ISR, but the term is usually reserved for hardware interrupt service routines.

4. The only difference between hardware and software interrupts is in the event that triggers the trip through the interrupt vector table. With a software interrupt the triggering event is part of the software; that is, an INT instruction. With a hardware interrupt, the triggering event is an electrical signal applied to the CPU chip itself without any INT instruction taking a hand in the process. The CPU itself pushes the return address on the stack when it recognizes the electrical pulse that triggers the interrupt; however, when the ISR is done, a RET instruction sends execution home, just as it does for a software interrupt.

5. Hardware ISRs can be (and usually are) written in assembly language.

Chapter 9: Dividing and Conquering Using Procedures and Macros to Battle Complexity

Boxes within Boxes

1. This sounds like Eastern mysticism, but it's just an observation from life: Within any action is a host of smaller actions. Look inside your common activities.

2. This process doesn't (and can't) go on forever, but it should go on as long as it needs to in order to satisfy this criterion: The contents of any one box should be understandable with only a little scrutiny. No single box should contain anything so subtle or large and involved that it takes hours of hair-pulling to figure it out.

Procedures as Boxes for Code

1. All computer languages have procedures of one sort or another, and assembly language is no exception.

2. Your assembly language program may have numerous procedures. There's no limit to the number of procedures, as long as the total number of bytes of code contained by all the procedures together does not exceed 65,536 (one segment).

Calling and Returning

1. CALL first pushes the address of the next instruction after itself onto the stack. Then CALL transfers execution to the address represented by the name of the procedure. The instructions contained in the procedure execute. Finally, the procedure is terminated by CALL's alter ego: RET (for RETurn). The RET instruction pops the address off the top of the stack and transfers execution to that address. Since the address pushed was the address of the first instruction after the CALL instruction, execution continues as though CALL had not changed the flow of instruction execution at all.

2. The structure of a procedure is simple and easy to understand. The important points are these: A procedure must begin with a label, which is (as you should recall) an identifier followed by a colon. Also, somewhere within the procedure, and certainly as the last instruction in the procedure, there must be at least one RET instruction. There may be more than one RET instruction.

Calls within Calls

1. Within a procedure you can do anything that you can do within the main program.

2. Calling procedures from within procedures requires you to pay attention to one thing: stack space. Remember that each procedure call pushes a return address onto the stack. This return address is not removed from the stack until the RET instruction for that procedure executes.

3. Stack space isn't allocated in real mode flat model; instead the stack pointer points to the high end of the program's single segment, and the stack uses as much of the segment as it needs.

4. Things are different when you move to real mode segmented model. In that model, you have to explicitly allocate a stack segment of some specific size, and that is all the space that the stack has to work with. So, ironically, in a program that can potentially make use of the full megabyte of real mode memory, it's much easier to foment a stack crash in segmented model than flat model.

5. Nonetheless, I recommend allocating 512 bytes of stack to get you in the habit of not being stingy with stack space. Obviously, you won't always be able to keep a 128-to-1 ratio of need-to-have, but consider 512 bytes a minimum for stack space allocation in any reasonable program that uses the stack at all. (We allocated only 64 bytes of stack in EATSEG.ASM simply to show you what stack allocation was. The program does not, in fact, make any use of the stack at all.) If you need more, allocate it. Don't forget that there is only one stack in the system, and while your program is running, DOS and the BIOS and any active memory resident programs may well be using the same stack. If they fill it, you'll go down with the system—so leave room!

When to Make Something a Procedure

1. The single most important purpose of procedures is to manage complexity in your programs by replacing a sequence of machine instructions with a descriptive name.

2. In general, when looking for some action to turn into a procedure, see what actions tend to happen a lot in a program. Furthermore, once you've written and tested them, they may be reused in future programs as well without adding to the burden of code that you must test for bugs.

3. On the other hand, a short sequence (5 to 10 instructions) that is only called once or perhaps twice within a middling program (that is, over hundreds of machine instructions) is a poor candidate for a procedure.

4. You may find it useful to define large procedures that are called only once when your program becomes big enough to require breaking it down into functional chunks. A thousand-line assembly language program might split well into a sequence of 9 or 10 largish procedures. Each is only called once from the main program, but this allows your main program to be very indicative of what the program is doing.

5. This is clean and readable and provides a necessary view from a height when you begin to approach a thousand-line assembly language program. Remember that the Martians are always hiding somewhere close by, anxious to turn your program into unreadable hieroglyphics.

Using BIOS Services

1. There's another provider of services in your machine that lives even deeper than DOS: the ROM BIOS. ROM is an acronym for read-only memory, and it indicates memory chips whose contents are burned into their silicon and do not vanish when power is turned off. BIOS is an acronym for Basic Input/Output System, and it is just that: a collection of fundamental routines for dealing with your computer's input and output peripherals. These include disk drives, displays, printers, and the like. DOS uses BIOS services as part of some of the services that it provides.

2. Like DOS, BIOS services are accessed through software interrupts. Unlike DOS, which channels nearly all requests for its services through the single interrupt 21H, BIOS uses numerous interrupts (about 10) and groups similar categories of services beneath the control of different interrupts.

3. The overall method for using BIOS services, however, is very similar to that of DOS. You load a service number and sometimes other initial values into the registers and then execute an INT instruction, where the n depends on the category of services you're requesting.

Positioning the Hardware Cursor

1. So far, in writing to the display, we've simply let the text fall where it may. In general this means one line of text following another, and when the screen fills, DOS scrolls the screen upward to make room on the bottom line for more text. This makes for dull programs, very similar to programming in the bad old days when everything was done on clunky mechanical printers called Teletypes.

2. BIOS service 10H offers a simple service to position the hardware cursor on the text screen. The service number is loaded into AH. This is a common thread through all BIOS services: The service number is placed into AH. A 0 must be placed in BH unless you intend to tinker with multiple text display pages.

3. The new position of the cursor must be loaded into the two halves of the DX register. Cursor positions are given as X,Y coordinate pairs. The X component of the cursor position is the number of character columns to the right of the left margin where we want the cursor to be. The Y component is the number of lines down from the top of the screen where we want the cursor to be. The The X component is loaded into DL, and the Y component is loaded into DH.

4. There's another VIDEO service that can do the job. Service 6 is an interesting and powerful one: Not only does it clear the screen, it can scroll the screen as well, by any specified number of lines. Furthermore, it can clear or scroll the entire screen, or only a rectangular portion of the screen, leaving the rest of the screen undisturbed.

Using VIDEO Service 6

1. Service 6 acts upon a rectangular region of the display.

2. Because screen coordinates are always smaller than 255 (which is the largest value that can be expressed in 8 bits), the register halves of CX and DX are used independently to carry the X and Y values.

3. The upper-left corner's X coordinate is passed in CL, and the upper-left corner's Y coordinate is passed in CH. These are zero-based coordinates, meaning that they count from 0 rather than 1. Similarly, the lower-right corner's X coordinate is passed in DL, and the lower-right corner's Y coordinate is passed in DH. (Again, counting from 0.)

4. Service 6 either scrolls or clears the region. It can scroll the screen upward by any arbitrary number of lines. This number is passed to service 6 in register AL. Clearing the region is a special case of scrolling it: When you specify that 0 lines be scrolled, the entire region is cleared instead.

Procedures with Multiple Entry Points

1. The method involves writing a single procedure that has four different entry points. Each entry point is a label, which may be called with a CALL instruction. When a given entry point's label is called, execution begins at the instruction specified by that label. There is only one RET instruction, so the procedure is in fact one procedure.

2. The multiple entry points exist only to allow you to skip certain portions of the procedure that set up values that you don't want set.

Memory Data or Immediate Data?

1. Avoid dropping immediate values into code (we call this hard coding) whenever you can. A better strategy, which I follow from now on, uses variables in the data segment initialized with currently correct values when the program begins running.

Use Comment Headers!

1. The libraries of available procedures that most high-level language vendors supply with their compilers just don't exist with assembly language. By and large, you create your own.

2. You must document the essential facts about each individual procedure or you'll forget them, or remember them incorrectly and act on bad information.

3. I recommend adding a comment header to every procedure you write, no matter how simple. Such a header should contain the following information:

· The name of the procedure

· The date it was last modified

· What it does

· What data items the caller must pass it to make it work correctly

· What data is returned by the procedure, if any, and where it is returned (for example, in register CX)

· What other procedures, if any, are called by the procedure

· Any "gotchas" that need to be kept in mind while writing code that uses the procedure

4. A typical workable procedure header is this:

  ;---------------------------------------------------------------
  ;  WRITELN -- Displays information to the screen via DOS
  ;             service 9 and issues a newline
  ;  Last update 9/11/99
  ;
  ;  1 entry point:
  ;
  ;  Writeln:
  ;   Caller must pass:
  ;   DS: The segment of the string to be displayed
  ;   DX: The offset of the string to be displayed
  ;       String must be terminated by "$"
  ;   Action: Displays the string at DS:DX up to the "$" marker
  ;           marker, then issues a newline. Hardware cursor
  ;           will move to the left margin of the following
  ;           line. If the display is to the bottom screen
  ;           line, the screen will scroll.
  ;   Calls: Write
  ;---------------------------------------------------------------

5. A comment header does not relieve you of the responsibility of commenting the individual lines of code within the procedure. It's a good idea to put a short comment to the right of every line that contains a machine instruction mnemonic, and also (in longer procedures) a comment block describing every major functional block within the procedure.

Building External Libraries of Procedures

1. The answer is to break the utility procedures out into an external library that you can assemble only once, and then link into every program that uses its procedures without assembling the library every time you assemble the program. This is called modular programming, and it is an extremely effective tool for programming efficiently in any language, assembly language not excluded.

2. A program might consist of three or four separate .ASM files, each of which is assembled separately to a separate .OBJ file. To produce the final executable .EXE file, the linker weaves all of the .OBJ files together, resolving all of the references from one to the other, finally creating an .EXE file.

3. Each .ASM file is considered a module, and each module contains one or more procedures and possibly some data definitions. When all the declarations are done correctly, all of the modules may freely call one another, and any procedure may refer to any data definition.

Public and External Declarations

1. The answer is to declare a procedure external. This works very much like it sounds: The assembler is told that a given label will have to be found outside the program somewhere, in another module. Once told that, that assembler is happy to give you a pass on an undefined label. You've promised the assembler that you'll provide it later, and the assembler accepts your promise and keeps going without flagging the undefined label.

EXTERN ClrScr

2. You forge the hook by declaring the procedure global, meaning that other modules anywhere in the program may freely reference the procedure. Declaring a procedure global is simplicity itself:

GLOBAL ClrScr

3. At link time, the linker takes the two .OBJ files generated by the assembler, one from your program and the other from the module containing the procedure, and combines them into a single .EXE executable file. (The number of .OBJ files isn't limited to two; you can have almost any number of separately assembled external modules.) When the .EXE file is loaded and run, the program can call the procedure as cleanly and quickly as though both had been declared in the same source code file.

4. What works for procedures works for data as well, and it can work in either direction. Your program can declare a variable as GLOBAL, and that variable may then be used by any module in which the same variable name is declared as external with the EXTERN directive.

5. We sometimes say that a program or module containing procedures or variables declared as public exports those items. Also, we say that a program or module that uses procedures or variables that are external to it imports those items.

The Mechanics of Globals and Externals

1. External modules are similar to programs. There are two major differences, concerning things that external modules lack:

  • External modules do not contain a main program and hence have no start address. That is, no label ..start: exists to indicate to the linker that this is the point at which code execution is to begin. External modules are not intended to be run by themselves, so a start address is both unnecessary and (if one were added) a temptation to chaos.
  • External modules have no stack segment. This is not an absolute requirement (there are few such requirements in assembler work), but for simple assembly language programming it's true enough. Your stack segment should be defined in your main program module. External modules should have none—they use the one defined by the programs that call them. Recall that in real mode segmented model, there is only one stack in operation at any given time, and that the one you define in your program is used by everything running on your machine—including the operating system—while your program has control.

2. External modules may have a data segment. If the external is to define a variable that is to be shared by the main program or by other externals, it obviously must have a data segment for that variable to reside in. But less obviously, if the external is to share a variable with another external or with the main program, it must still define a data segment, even if that data segment is empty except for the external declaration.

3. If a segment (whether a code segment or a data segment) is to export anything, that segment must be declared public. This is done with the PUBLIC keyword, which instructs the linker to make access to the segment possible from outside the module that contains the segment:

SEGMENT data PUBLIC

SEGMENT code PUBLIC

4. A common mistake made by beginners is to declare the procedures and variables public, but not the segments that they reside in. Nonobvious it may be, but essential nonetheless: Make your module segments public if they contain any public declarations!

5. The preceding EXTERN statement indicates that two variables referenced within the module are to be imported from somewhere. You don't have to specify from where. The names of the variables have to be there, and that's all.

6. Unlike many assemblers, NASM is case-sensitive for variable names and labels (though not for keywords and instruction mnemonics), so be careful of character case.

7. That is, with NASM you don't have to say whether a variable is a byte, a word, a double word, and so on. NASM assumes that the size of the item where it is declared governs its use in all placesand the burden of enforcing that falls on you. If you declare an EXTERN to be a byte (say, with DB) and then try to link that EXTERN to a GLOBAL that is in fact a word, what actually happens may not be what you want.

Dividing a Segment across Module Boundaries

1. Regardless of the number of external modules that link with your main program, the program as a whole contains only one code segment and one data segment. Until your data requirements and code size get very large, you won't need more than a single code and data segment. There are ways to use multiple code and data segments within a single assembly language program.

2. As long as the code and data segments are declared with the PUBLIC directive in all the modules sharing the segments, the linker will consider all to be part of the same code and data segments.

Your Main Program Module

1. This is easy to forget but you must keep it in mind: The segments containing imported or exported items as well as the imported or exported items themselves must be declared as public.

Linking Multiple Modules

1. To link multiple modules, you must specify the name of the .OBJ file for each module on the linker command line.

Batch Files for Building Modular Programs

1. So, if you're going to be doing a lot of work in real mode segmented model, you might consider creating batch files containing the commands you need to issue to the assembler and the linker.

External Module Summary

1. The assumption here is that the final program will consist of only one code segment and only one data segment. Larger arrangements of segments are possible (especially with multiple code segments) but require additional considerations.

2. Declare the code segments PUBLIC in all modules, and give them all the same name.

3. Declare the data segments PUBLIC in all modules, and give them all the same name.

4. Declare all exported procedures, entry points, and variables as GLOBAL. Put the exported declaration statement inside the segment where the exported items are declared.

5. Declare all imported procedures, entry points, and variables as EXTERN. Put the imported declaration statement inside the segment where the imported items are to be used. Data is used in the data segment, code in the code segment.

6. Finally, don't forget to add the names of all external modules to the linker command line in the link step.

Creating and Using Macros

1. The mechanism for calling and returning from procedures is built right into the CPU and is independent of any given assembler product.

2. There is more than one way to split an assembly language program into more manageable chunks.

3. Today's major assemblers provide another complexity-management tool that works a little differently: macros.

4. Whereas procedures are implemented by the use of CALL and RET instructions built right into the instruction set, macros are a trick of the assembler and do not depend on any particular instruction or group of instructions.

5. Most simply put, a macro is a label that stands for some sequence of text lines. When the assembler encounters the macro label in a source code file, it replaces the macro label with the text lines that the macro label represents. This is called expanding the macro, because the name of the macro (occupying one text line) is replaced by several lines of text, which are then assembled just as though they had appeared in the source code file all along. (Of course, a macro doesn't have to be several lines of text. It can be only one-but then there's a lot less advantage to using them!)

6. The source code as stored on disk has a definition of the macro, bracketed between %macro and %endmacro directives. Later in the file, the name of the macro appears several times. When the assembler processes this file, it copies the macro definition into a buffer somewhere in memory. As it assembles the text read from disk, the assembler drops the statements contained in the macro into the text wherever the macro name appears. The disk file is not affected; the expansion of the macros occurs only in memory.

Macros versus Procedures: Pro and Con

1. There are advantages to macros over procedures.

2. One of them is speed. It takes time to execute the CALL and RET instructions that control entry to and exit from a procedure. In a macro, neither instruction is used. Only the instructions that perform the actual work of the macro are executed, so the macro's work is performed as quickly as possible.

3. There is a cost to this speed, and the cost is in extra memory used, especially if the macro is called a great many times. Every time a macro is called, all of its instructions are duplicated in the program another time.

4. By and large, think macros for speed and procedures for compactness.

5. The Mechanics of Macro Definition

6. Note that the %endmacro directive is on the line after the last line of the macro. Don't make the mistake of treating %endmacro like a label that marks the macro's last line.

7. Macros can have only one entry point.

8. Macros are called simply by naming them. Don't use the CALL instruction! Just place the macro name on a line.

Defining Macros with Parameters

1. What makes macros really sing is their ability to mimic high-level language subroutines and take arguments through parameters.

2. Macro parameters are, again, artifacts of the assembler. They are not pushed on the stack or set into COMMON or anything like that. The parameters are simply placeholders for the actual values (called arguments) that you pass to the macro through its parameters.

3. This is another area where NASM differs radically from MASM. MASM allows you to use symbolic names-such as the words NewX and NewY-to stand for parameters. NASM relies on a simpler system that declares the number of parameters, and then refers to the parameters by number rather than symbolic name.

4. Macro parameters are a kind of label, and they may be referenced anywhere within the macro-but only within the macro.

5. The actual values passed into the parameters are referred to as arguments. Don't confuse the actual values with the parameters. The macro's parameters are the labels following the name of the macro in the line in which it is defined. The arguments are the values specified on the line where the macro is invoked.

The Mechanics of Macro Parameters

1. A macro may have as many parameters as will fit on one line.

2. When a macro is invoked, arguments are separated by commas. The arguments are dropped into the macro's parameters in order, from left to right. If you pass only two arguments to a macro with three parameters, you're likely to get an error message from the assembler, depending on how you've referenced the unfilled parameter.

3. If you pass more arguments to a macro than there are parameters to receive the arguments, the extraneous arguments will be ignored.

Local Labels within Macros

1. Labels in assembly language programs must be unique, and yet a macro is essentially duplicated in the source code as many times as it is invoked.

2. Local items have no meaning outside the immediate framework within which they are defined. Labels local to a macro are not known outside the macro definition.

3. All labels defined within a macro are considered local to the macro and are handled specially by the assembler.

4. A label in a macro is made local by beginning it with two percent signs: "%%." When marking a location in the macro, the local label should be followed by a colon. When used as an operand to a jump or call instruction (such as JB and LOOP in the preceding), the local label is not followed by a colon.

5. Because labels must in fact be unique within your program, NASM takes a formal label such as %%Tester and generates an actual label from it that will be unique in your program by using the prefix "..@" plus a four-digit number and the name of the label. Each time your macro is invoked, NASM will change the number, and thus generate unique synonyms for each local label within the macro.

Macro Libraries

1. A macro library is really nothing but a text file that contains the source code for the macros in the library. Unlike procedures gathered into a module, macro libraries are not separately assembled and must be passed through the assembler each time the program is assembled.

2. This is a problem with macros in general, not only with macros that are gathered into libraries. Programs that manage complexity by dividing code up into macros will assemble more slowly than programs that have been divided up into separately assembled modules.

3. Macro libraries are used by including them into your program's source code file. The means to do this is the %include directive. The %include directive precedes the name of the macro library: %include "MYLIB.MAC"

4. Technically this statement may be anywhere in your source code file, but you must keep in mind that all macros must be fully defined before they are invoked.

5. If the macro file you want to include in a program is not in the same directory as NASM itself, you may need to provide a more complete DOS path specification as part of the %include directive: %include "BOOK\MYLIB.MAC"

6. The danger there is that unless you name your macros carefully and document them both in their macro library files and on the lines where they are invoked, your programs will not be any more comprehensible for their presence. Dividing complexity into numerous compartments is only half the job-labeling the compartments is just as (or more) important!

Chapter 10: Bits, Flags, Branches, and Tables Easing into Mainstream Assembly Programming

Overview

1. You don't take off until all your flight checks are made.

2. I've found that machine instructions aren't the most important part of assembly language programming. What's most important is understanding your machine and your tools and how everything fits together.

Bits Is Bits (and Bytes Is Bits)

1. Assembly language is big on bits.

2. one essential assembly language skill is building bytes and taking them apart again.

3. A technique called bit mapping is widely used in assembly language. Bit mapping assigns special meanings to individual bits within a byte to save space and squeeze the last little bit of utility out of a given amount of memory.

4. There is a family of instructions in the x86 instruction set that allows you to manipulate the bits within the bytes by applying Boolean logical operations to the bytes on a bit-by-bit basis. These are the bitwise logical instructions: AND, OR, XOR, and NOT.

5. Another family of instructions allows you to slide bits back and forth within a single byte or word. These are the most-used shift/rotate instructions: ROL, ROR, RCL, RCR, SHL, and SHR. (There are a few others that I will not be discussing in this book.)

Bit Numbering

1. By convention, bits in assembly language are numbered, starting from 0, at the least-significant bit in the byte, word, or other item we're using as a bit map. The least-significant bit is the one with the least value in the binary number system.It's also the bit on the far right, if you write the value down as a binary number.

2. When you count bits, start with the bit on the right, and number them from 0.

"It's the Logical Thing to Do, Jim . . ."

1. Boolean logic sounds arcane and forbidding, but remarkably, it reflects the realities of ordinary thought and action.

2. When mathematicians speak of Boolean logic, they manipulate abstract values called True and False.

3. There are in fact four different combinations of the two input values, so logical operations between two values are usually summarized in a form called a truth table.

4. Keeping that in mind, we can rewrite AND's truth table to make it more meaningful for assembly language work.

The AND Instruction

1. The AND instruction embodies this concept in the x86 instruction set. The AND instruction performs the AND logical operation on two like-sized operands and replaces its first operand with the result of the operation.

2. In other words, consider this instruction:

AND AL,BL

What will happen here is that the CPU will perform a gang of eight bitwise AND operations on the eight bits in AL and BL. Bit 0 of AL is ANDed with bit 0 of BL, bit 1 of AL is ANDed with bit 1 of BL, and so on. Each AND operation generates a result bit, and that bit is placed in the first operand (here, AL) after all eight AND operations occur.

3. This is a common thread among machine instructions that perform some operation on two operands and produce a result: The result replaces the first operand, not the second!

Masking Out Bits

1. A major use of the AND instruction is to isolate one or more bits out of a byte value or a word value. Isolate here simply means to set all unwanted bits to a reliable 0 value.

2. Don't forget: The result of the AND operation replaces the first operand after the operation is complete.

The OR Instruction

1. Closely related to the AND logical operation is OR, which, like the AND logical operation, has an embodiment with the same name in the x86 instruction set. Structurally, the OR instruction works identically to AND. Only its truth table is different

The XOR Instruction

1. In a class by itself is the exclusive OR operation, embodied in the XOR instruction. XOR, again, does in broad terms what AND and OR do: It performs a logical operation on two operands, and the result replaces the first operand. The logical operation, however, is exclusive or, meaning that the result is 1 only if the two operands are different (that is, 1 and 0 or 0 and 1).

2. XORing any value against itself yields 0. In the old days, this was faster than loading a 0 into a register from immediate data. Although that's no longer the case, it's an interesting trick to know.

The NOT Instruction

1. Easiest to understand of all the bitwise logical instructions is NOT. The truth table for NOT is simpler than the others we've looked at because NOT only takes one operand. And what it does is simple as well: NOT takes the state of each bit in its single operand and changes it to its opposite state. What was 1 becomes 0 and what was 0 becomes 1.

Segment Registers Don't Respond to Logic!

1. One limitation of the segment registers CS, DS, SS, ES, FS, and GS is that they cannot be used with any of the bitwise logical instructions.

2. If you need to perform a logical operation on a segment register, you must first copy the segment register's value into one of the registers AX, BX, CX, DX, BP, SI, and DI; perform the logical operation on the new register; and then copy the result back into the segment register.

3. Banging bits in segment registers is a dicey business if what's in the segment register is truly a segment address. Sometimes it would be handy to use segment registers as spare, general-purpose registers, but this can only be done in real mode.

4. In real work in today's world, you're likely to be working in protected mode, where segment registers can only be used as segment registers. So, it's really not losing any genuine chip features, but gaining 4 gigabytes of memory instead. That's a bargain I can live with.

Shifting Bits

1. The other way of manipulating bits within a byte is a little more straightforward: You shift them to one side or the other.

2. 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 ,

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?

1. This 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.

2. Although you can load a number as large as 255 into CL, it really only makes sense to use count values up to 32.

3. Starting with the 286, the operand may be any immediate value from 1 to 255.

How Bit Shifting Works

1. Understanding the shift instructions requires that you think of the numbers being shifted as binary numbers, and not hexadecimal or decimal numbers.

Bumping Bits into the Carry Flag

1. The last bit shifted out is bumped into a temporary bucket for bits called the Carry flag, often abbreviated as CF.

2. Keep in mind when using shift instructions, however, that a lot of different instructions use the Carry flag as well as the shift instructions.

3. 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.

4. 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

1. 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.

2. 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.

3. Digits is what we call a lookup table and it represents an extremely useful mechanism in assembly language.

Splitting a Byte into Two Nybbles

1. Displaying the value stored in a byte requires two hexadecimal digits.

2. 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.

3. To split a byte in two, we need to mask out the unwanted half. This is done with an AND instruction.

Using a Lookup Table

1. 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].

2. Now, we use yet another addressing mode to move the character from AX back into the second character of the destination string. 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.

Shifting the High Nybble into the Low Nybble

1. 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.

2. 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.

Converting Words to Their String Form

1. 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.

2. Let segment registers hold segments. Use the stack instead.

3. A word requires four hexadecimal digits altogether.

4. Since strings are indexed from the left to the right, it makes a certain sense to convert the left end of the string first.

5. 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.

Flags, Tests, and Branches

1. Besides, I felt it wise to teach how to manage complexity before teaching the number one means of creating complexity.

Unconditional Jumps

1. A jump is just that: an abrupt change in the flow of instruction execution.

2. There are two kinds of jumps: conditional and unconditional.

3. An unconditional jump is a jump that always happens. It takes this form: JMP

4. When this instruction executes, the sequence of execution moves to the instruction located at the label specified by

Conditional Jumps

1. When executed, a conditional jump tests something, usually one of the flags in the Flags register. If the flag being tested happens to be in a particular state, execution may jump to a label somewhere else in the code segment, or it may simply fall through to the next instruction in sequence.

2. This two-way nature is important. A conditional jump instruction either jumps, or it falls through. Jump, or no jump.

3. Note the use of an unconditional jump instruction to close the loop.

Beware Endless Loops!

Assembly language programming requires concentration and endless attention to detail. If you pay attention to what you're doing, you'll make fewer "stupid" errors like the preceding one.

Jumping on the Absence of a Condition

1. Think of it this way: A condition raises a flag. "Raising a flag" means setting the flag to 1. When one of numerous instructions forces an operand to a value of zero (which is the condition), the Zero flag is raised. The logic of the instruction refers to the condition, not to the flag.

2. 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

1. Most instructions that perform some operation on an operand (such as 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.)

2. 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.

3. Before you write code that depends on flags, check your instruction reference to make sure that you have the flag etiquette down correctly.

Comparisons with CMP

1. One major use of flags is in controlling loops. Another is in comparisons between two values.

2. The CMP (CoMPare) instruction is what sets the flags for comparison tasks.

3. CMP's use is straightforward and intuitive. The second operand is compared with the first, and several flags are set accordingly:

cmp , ; Sets OF, SF, ZF, AF, PF, and CF

4. The sense of the comparison can be remembered if you simply recast the comparison in arithmetic terms: Result =

5. CMP is very much 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 other flags are set to appropriate values.

6. After a CMP instruction, you can jump based on several arithmetic conditions: Equal, Not equal, Greater than, Less than, Greater than or equal to, and Less than or equal to

A Jungle of Jump Instructions

1. 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 preceding conditions.

2. Complication arises out of the fact that there are two mnemonics for each machine instruction. 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.

3. Another complication is that there is a separate set of instructions for signed and unsigned comparisons.

4. A signed quantity is one in which the high bit of the quantity is considered a built-in flag indicating whether the quantity is negative. If that bit is 1, the quantity is considered negative. If that bit is 0, the quantity is considered positive.

5. Signed arithmetic in assembly language is complex and subtle, and not as useful as you might immediately think.

6. 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 versus Above

1. To tell the signed jumps apart from the unsigned jumps, the mnemonics use two different expressions for the relationships between two values.

2. Signed values are thought of as being greater than or less than.

3. Unsigned values are thought of as being above or below.

4. 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.

Table 10.6: Arithmetic Jump Mnemonics and Their Synonyms

MNEMONICS

SYNONYMS

JA

Jump If Above

JNBE

Jump If Not Below or Equal

JAE

Jump If Above or Equal

JNB

Jump If Not Below

JB

Jump If Below

JNAE

Jump If Not Above or Equal

JBE

Jump If Below or Equal

JNA

Jump If Not Above

JE

Jump If Equal

JZ

Jump If Result is Zero

JNE

Jump If Not Equal

JNZ

Jump If Result is Not Zero

JG

Jump If Greater

JNLE

Jump If Not Less Than or Equal

JGE

Jump If Greater or Equal

JNL

Jump If Not Less

JL

Jump If Less

JNGE

Jump If Not Greater or Equal

JLE

Jump If Less or Equal

JNG

Jump If Not Greater

Table 10.7: Arithmetic Tests Useful After a CMP Instruction

CONDITION

PASCAL OPERATOR

UNSIGNED VALUES

JUMPS WHEN

SIGNED VALUES

JUMPS WHEN

Equal

=

JE

ZF=1

JE

ZF=1

Not Equal

<>

JNE

ZF=0

JNE

ZF=0

Greater than

>

JA

CF=0 and

JG

ZF=0 or

Not Less than or equal to

JNBE

ZF=0

JNLE

SF=OF

Less than

<

JB

CF=1

JL

SF<>OF

Not Greater than or equal to

JNAE

JNGE

Greater than or equal to

>=

JAE

CF=0

JGE

SF=OF

Not Less than

JNB

JNL

Less than or equal to

<=

JBE

CF=1 or ZF=1

JLE

ZF=1 and SF<>OF

Not Greater than

JNA

JNG

Detecting the Installed Display Adapter

1. If you work with older machines (and one reason to work in assembly is to create software that works quickly enough on older machines), it may be useful for sorting out what any given machine can do from a text video standpoint.

2. 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 memory address of the video display buffer is one address for the color monitor and a different address for the monochrome monitor.

Table 10.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

3.

Task left:

1. Try macros with parameters

2. Try Byte2Str and Word2Str functions done ok

3.

No comments: