Art of Assembly/Win32 Edition is now available. Let me read that version.
PLEASE: Before emailing me asking how to get a hard copy of this text, read this.
Important Notice: As you have probably discovered by now, I am no longer updating this document. The reason is quite simple: I'm working on a Windows version of "The Art of Assembly Language Programming". In the past I have encouraged individuals to send me corrections to this text. However, as I am no longer updating this material, don't expect those correctioins to appear in a future release. I am collecting errata that I will post to Webster someday, so feel free to continue sending corrections to AoA/DOS (16-bit) to rhyde@cs.ucr.edu. If you're more interested in leading edge material, please see the information about the Win/32 edition, above.
call
instruction, the procedure returns to the caller with the ret
instruction. For example, the following 80x86 instruction calls the UCR Standard Library sl_putcr
routine[1]:
call sl_putcr
sl_putcr
prints a carriage return/line feed sequence to the video display and returns control to the instruction immediately following the call sl_putcr
instruction.
Alas, the UCR Standard Library does not supply all the routines you will need. Most of the time you'll have to write your own procedures. A simple procedure may consist of nothing more than a sequence of instructions ending with a ret
instruction. For example, the following "procedure" zeros out the 256 bytes starting at the address in the bx
register:
ZeroBytes: xor ax, ax mov cx, 128 ZeroLoop: mov [bx], ax add bx, 2 loop ZeroLoop ret
By loading the bx
register with the address of some block of 256 bytes and issuing a call ZeroBytes
instruction, you can zero out the specified block.
As a general rule, you won't define your own procedures in this manner. Instead, you should use MASM's proc
and endp
assembler directives. The ZeroBytes
routine, using the proc
and endp
directives, is
ZeroBytes proc xor ax, ax mov cx, 128 ZeroLoop: mov [bx], ax add bx, 2 loop ZeroLoop ret ZeroBytes endp
Keep in mind that proc
and endp
are assembler directives. They do not generate any code. They're simply a mechanism to help make your programs easier to read. To the 80x86, the last two examples are identical; however, to a human being, latter is clearly a self-contained procedure, the other could simply be an arbitrary set of instructions within some other procedure. Consider now the following code:
ZeroBytes: xor ax, ax jcxz DoFFs ZeroLoop: mov [bx], ax add bx, 2 loop ZeroLoop ret DoFFs: mov cx, 128 mov ax, 0ffffh FFLoop: mov [bx], ax sub bx, 2 loop FFLoop ret
Are there two procedures here or just one? In other words, can a calling program enter this code at labels ZeroBytes
and DoFFs
or just at ZeroBytes
? The use of the proc
and endp
directives can help remove this ambiguity:
Treated as a single subroutine:
ZeroBytes proc xor ax, ax jcxz DoFFs ZeroLoop: mov [bx], ax add bx, 2 loop ZeroLoop ret DoFFs: mov cx, 128 mov ax, 0ffffh FFLoop: mov [bx], ax sub bx, 2 loop FFLoop ret ZeroBytes endp
Treated as two separate routines:
ZeroBytes proc xor ax, ax jcxz DoFFs ZeroLoop: mov [bx], ax add bx, 2 loop ZeroLoop ret ZeroBytes endp DoFFs proc mov cx, 128 mov ax, 0ffffh FFLoop: mov [bx], ax sub bx, 2 loop FFLoop ret DoFFs endp
Always keep in mind that the proc and endp directives are logical procedure separators. The 80x86 microprocessor returns from a procedure by executing a ret instruction, not by encountering an endp directive. The following is not equivalent to the code above:
ZeroBytes proc xor ax, ax jcxz DoFFs ZeroLoop: mov [bx], ax add bx, 2 loop ZeroLoop ; Note missing RET instr. ZeroBytes endp DoFFs proc mov cx, 128 mov ax, 0ffffh FFLoop: mov [bx], ax sub bx, 2 loop FFLoop ; Note missing RET instr. DoFFs endp
Without the ret instruction at the end of each procedure, the 80x86 will fall into the next subroutine rather than return to the caller. After executing ZeroBytes above, the 80x86 will drop through to the DoFFs subroutine (beginning with the mov cx, 128 instruction). Once DoFFs is through, the 80x86 will continue execution with the next executable instruction following DoFFs' endp directive.
An 80x86 procedure takes the form: ProcName proc {near|far} ;Choose near, far, or neither. <Procedure instructions> ProcName endp
The near
or far
operand is optional, the next section will discuss its purpose. The procedure name must be on the both proc
and endp
lines. The procedure name must be unique in the program.
Every proc
directive must have a matching endp
directive. Failure to match the proc
and endp
directives will produce a block nesting error.
call
instruction to call a far procedure or a far call
instruction to call a near procedure. Given this little rule, the next question is "how do you control the emission of a near or far call
or ret
?"call
instruction uses the following syntax:
call ProcName
and the ret
instruction is either[2]:
ret or ret disp
Unfortunately, these instructions do not tell MASM if you are calling a near or far procedure or if you are returning from a near or far procedure. The proc
directive handles that chore. The proc
directive has an optional operand that is either near
or far
. Near
is the default if the operand field is empty[3]. The assembler assigns the procedure type (near or far) to the symbol. Whenever MASM assembles a call
instruction, it emits a near or far call depending on operand. Therefore, declaring a symbol with proc
or proc near
, forces a near call. Likewise, using proc far
, forces a far call.
Besides controlling the generation of a near or far call, proc
's operand also controls ret
code generation. If a procedure has the near operand, then all return instructions inside that procedure will be near. MASM emits far returns inside far procedures.
near
ptr
and far
ptr
operators to override the automatic assignment of a near or far call
. If NearLbl
is a near label and FarLbl
is a far label, then the following call
instructions generate a near and far call, respectively:
call NearLbl ;Generates a NEAR call. call FarLbl ;Generates a FAR call.
Suppose you need to make a far call to NearLbl
or a near call to FarLbl
. You can accomplish this using the following instructions:
call far ptr NearLbl ;Generates a FAR call. call near ptr FarLbl ;Generates a NEAR call.
Calling a near procedure using a far call, or calling a far procedure using a near call isn't something you'll normally do. If you call a near procedure using a far call instruction, the near return will leave the cs value on the stack. Generally, rather than:
call far ptr NearProc
you should probably use the clearer code:
push cs call NearProc
Calling a far procedure with a near call
is a very dangerous operation. If you attempt such a call, the current cs
value must be on the stack. Remember, a far ret
pops a segmented return address off the stack. A near call
instruction only pushes the offset, not the segment portion of the return address.
Starting with MASM v5.0, there are explicit instructions you can use to force a near or far ret
. If ret
appears within a procedure declared via proc
and end;
, MASM will automatically generate the appropriate near or far return instruction. To accomplish this, use the retn
and retf
instructions. These two instructions generate a near and far ret
, respectively.
OutsideProc proc near jmp EndofOutside InsideProc proc near mov ax, 0 ret InsideProc endp EndofOutside: call InsideProc mov bx, 0 ret OutsideProc endp
Unlike some high level languages, nesting procedures in 80x86 assembly language doesn't serve any useful purpose. If you nest a procedure (as with InsideProc
above), you'll have to code an explicit jmp
around the nested procedure. Placing the nested procedure after all the code in the outside procedure (but still between the outside proc
/endp
directives) doesn't accomplish anything. Therefore, there isn't a good reason to nest procedures in this manner.
Whenever you nest one procedure within another, it must be totally contained within the nesting procedure. That is, the proc
and endp
statements for the nested procedure must lie between the proc
and endp directives of the outside, nesting, procedure. The following is not legal:
OutsideProc proc near . . . InsideProc proc near . . . OutsideProc endp . . . InsideProc endp
The OutsideProc
and InsideProc
procedures overlap, they are not nested. If you attempt to create a set of procedures like this, MASM would report a "block nesting error". The figure below demonstrates this graphically:
The only form acceptable to MASMis
Besides fitting inside an enclosing procedure, proc/endp
groups must fit entirely within a segment. Therefore the following code is illegal:
cseg segment MyProc proc near ret cseg ends MyProc endp
The endp
directive must appear before the cseg ends
statement since MyProc
begins inside cseg
. Therefore, procedures within segments must always take the form shown below:
Not only can you nest procedures inside other procedures and segments, but you can nest segments inside other procedures and segments as well. If you're the type who likes to simulate Pascal or C procedures in assembly language, you can create variable declaration sections at the beginning of each procedure you create, just like Pascal:
cgroup group cseg1, cseg2 cseg1 segment para public 'code' cseg1 ends cseg2 segment para public 'code' cseg2 ends dseg segment para public 'data' dseg ends cseg1 segment para public 'code' assume cs:cgroup, ds:dseg MainPgm proc near ; Data declarations for main program: dseg segment para public 'data' I word ? J word ? dseg ends ; Procedures that are local to the main program: cseg2 segment para public 'code' ZeroWords proc near ; Variables local to ZeroBytes: dseg segment para public 'data' AXSave word ? BXSave word ? CXSave word ? dseg ends ; Code for the ZeroBytes procedure: mov AXSave, ax mov CXSave, cx mov BXSave, bx xor ax, ax ZeroLoop: mov [bx], ax inc bx inc bx loop ZeroLoop mov ax, AXSave mov bx, BXSave mov cx, CXSave ret ZeroWords endp Cseg2 ends ; The actual main program begins here: mov bx, offset Array mov cx, 128 call ZeroWords ret MainPgm endp cseg1 ends end
The system will load this code into memory as shown below:
ZeroWords
follows the main program because it belongs to a different segment (cseg2
) than MainPgm
(cseg1
). Remember, the assembler and linker combine segments with the same class name into a single segment before loading them into memory (see Chapter Eight for more details). You can use this feature of the assembler to "pseudo-Pascalize" your code in the fashion shown above. However, you'll probably not find your programs to be any more readable than using the straight forward non-nesting approach.
proc/endp
directives. All the rules and techniques that apply to procedures apply to functions. This text will take another look at functions later in this chapter in the section on function results. From here on, procedure will mean procedure or function.
mov cx, 10 Loop0: call PrintSpaces putcr loop Loop0 . . . PrintSpaces proc near mov al, ' ' mov cx, 40 PSLoop: putc loop PSLoop ret PrintSpaces endp
This section of code attempts to print ten lines of 40 spaces each. Unfortunately, there is a subtle bug that causes it to print 40 spaces per line in an infinite loop. The main program uses the loop
instruction to call PrintSpaces
10 times. PrintSpaces
uses cx
to count off the 40 spaces it prints. PrintSpaces
returns with cx
containing zero. The main program then prints a carriage return/line feed, decrements cx
, and then repeats because cx
isn't zero (it will always contain 0FFFFh at this point).
The problem here is that the PrintSpaces
subroutine doesn't preserve the cx
register. Preserving a register means you save it upon entry into the subroutine and restore it before leaving. Had the PrintSpaces
subroutine preserved the contents of the cx
register, the program above would have functioned properly.
Use the 80x86's push
and pop
instructions to preserve register values while you need to use them for something else. Consider the following code for PrintSpaces
:
PrintSpaces proc near push ax push cx mov al, ' ' mov cx, 40 PSLoop: putc loop PSLoop pop cx pop ax ret PrintSpaces endp
Note that PrintSpaces
saves and restores ax
and cx
(since this procedure modifies these registers). Also, note that this code pops the registers off the stack in the reverse order that it pushed them. The operation of the stack imposes this ordering.
Either the caller (the code containing the call
instruction) or the callee (the subroutine) can take responsibility for preserving the registers. In the example above, the callee preserved the registers. The following example shows what this code might look like if the caller preserves the registers:
mov cx, 10 Loop0: push ax push cx call PrintSpaces pop cx pop ax putcr loop Loop0 . . . PrintSpaces proc near mov al, ' ' mov cx, 40 PSLoop: putc loop PSLoop ret PrintSpaces endp
There are two advantages to callee preservation: space and maintainability. If the callee preserves all affected registers, then there is only one copy of the push and pop instructions, those the procedure contains. If the caller saves the values in the registers, the program needs a set of push and pop instructions around every call. Not only does this make your programs longer, it also makes them harder to maintain. Remembering which registers to push and pop on each procedure call is not something easily done.
On the other hand, a subroutine may unnecessarily preserve some registers if it preserves all the registers it modifies. In the examples above, the code needn't save ax
. Although PrintSpaces
changes the al
, this won't affect the program's operation. If the caller is preserving the registers, it doesn't have to save registers it doesn't care about:
mov cx, 10 Loop0: push cx call PrintSpaces pop cx putcr loop Loop0 putcr putcr call PrintSpaces mov al, '*' mov cx, 100 Loop1: putc push ax push cx call PrintSpaces pop cx pop ax putc putcr loop Loop1 . . . PrintSpaces proc near mov al, ' ' mov cx, 40 PSLoop: putc loop PSLoop ret PrintSpaces endp
This example provides three different cases. The first loop (Loop0
) only preserves the cx
register. Modifying the al
register won't affect the operation of this program. Immediately after the first loop, this code calls PrintSpaces
again. However, this code doesn't save ax
or cx
because it doesn't care if PrintSpaces
changes them. Since the final loop (Loop1
) uses ax
and cx
, it saves them both.
One big problem with having the caller preserve registers is that your program may change. You may modify the calling code or the procedure so that they use additional registers. Such changes, of course, may change the set of registers that you must preserve. Worse still, if the modification is in the subroutine itself, you will need to locate every call to the routine and verify that the subroutine does not change any registers the calling code uses.
Preserving registers isn't all there is to preserving the environment. You can also push and pop variables and other values that a subroutine might change. Since the 80x86 allows you to push and pop memory locations, you can easily preserve these values as well.
putcr
macro to accomplish this, but this call
instruction will accomplish the same thing.