Take a look at the program in Example 5-2. This section of code attempts to print 20 lines of 40 spaces and an asterisk. Unfortunately, there is a subtle bug that creates an infinite loop. The main program uses the repeat..until
loop to call PrintSpaces
20 times. PrintSpaces
uses ECX to count off the 40 spaces it prints. PrintSpaces
returns with ECX containing 0. The main program then prints an asterisk and a newline, decrements ECX, and then repeats because ECX isn't 0 (it will always contain $FFFF_FFFF at this point).
The problem here is that the PrintSpaces
subroutine doesn't preserve the ECX 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 ECX register, the program in Example 5-2 would have functioned properly.
Example 5-2. Program with an unintended infinite loop
program nonWorkingProgram; #include( "stdlib.hhf" ); procedure PrintSpaces; begin PrintSpaces; mov( 40, ecx ); repeat mov( ' ', al ); stdout.putc( al ); // Print 1 of 40 spaces. dec( ecx ); // Count off 40 spaces. until( ecx = 0 ); end PrintSpaces; begin nonWorkingProgram; mov( 20, ecx ); repeat PrintSpaces(); stdout.put( '*', nl ); dec( ecx ); until( ecx = 0 ); end nonWorkingProgram;
You can 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
:
procedure PrintSpaces; begin PrintSpaces; push( eax ); push( ecx ); mov( 40, ecx ); repeat mov( ' ', al ); stdout.putc( al ); // Print 1 of 40 spaces. dec( ecx ); // Count off 40 spaces. until( ecx = 0 ); pop( ecx ); pop( eax ); end PrintSpaces;
Note that PrintSpaces
saves and restores EAX and ECX (because 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 last-in, first-out 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 example in Example 5-3 shows what this code might look like if the caller preserves the registers:
Example 5-3. Demonstration of caller register preservation
program callerPreservation; #include( "stdlib.hhf" ); procedure PrintSpaces; begin PrintSpaces; mov( 40, ecx ); repeat mov( ' ', al ); stdout.putc( al ); // Print 1 of 40 spaces. dec( ecx ); // Count off 40 spaces. until( ecx = 0 ); end PrintSpaces; begin callerPreservation; mov( 20, ecx ); repeat push( eax ); push( ecx ); PrintSpaces(); pop( ecx ); pop( eax ); stdout.put( '*', nl ); dec( ecx ); until( ecx = 0 ); end callerPreservation;
There are two advantages to callee preservation: space and maintainability. If the callee (the procedure) 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 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 EAX. Although PrintSpaces
changes 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 (see the program in Example 5-4).
Example 5-4. Demonstrating that caller preservation need not save all registers
program callerPreservation2; #include( "stdlib.hhf" ); procedure PrintSpaces; begin PrintSpaces; mov( 40, ecx ); repeat mov( ' ', al ); stdout.putc( al ); // Print 1 of 40 spaces. dec( ecx ); // Count off 40 spaces. until( ecx = 0 ); end PrintSpaces; begin callerPreservation2; mov( 10, ecx ); repeat push( ecx ); PrintSpaces(); pop( ecx ); stdout.put( '*', nl ); dec( ecx ); until( ecx = 0 ); mov( 5, ebx ); while( ebx > 0 ) do PrintSpaces(); stdout.put( ebx, nl ); dec( ebx ); endwhile; mov( 110, ecx ); for( mov( 0, eax ); eax < 7; inc( eax )) do PrintSpaces(); stdout.put( eax, " ", ecx, nl ); dec( ecx ); endfor; end callerPreservation2;
This example in Example 5-4 provides three different cases. The first loop (repeat..until
) preserves only the ECX register. Modifying the AL register won't affect the operation of this loop. Immediately after the first loop, this code calls PrintSpaces
again in the while
loop. However, this code doesn't save EAX or ECX because it doesn't care if PrintSpaces
changes them.
One big problem with having the caller preserve registers is that your program may change over time. You may modify the calling code or the procedure to 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. Because the 80x86 allows you to push and pop memory locations, you can easily preserve these values as well.