Because procedures use the stack to hold the return address, you must exercise caution when pushing and popping data within a procedure. Consider the following simple (and defective) procedure:
procedure MessedUp; @noframe; @nodisplay; begin MessedUp; push( eax ); ret(); end MessedUp;
At the point the program encounters the ret
instruction, the 80x86 stack takes the form shown in Figure 5-1.
The ret
instruction isn't aware that the value on the top of stack is not a valid address. It simply pops whatever value is on the top of the stack and jumps to that location. In this example, the top of stack contains the saved EAX value. Because it is very unlikely that EAX contains the proper return address (indeed, there is about a one in four billion chance it is correct), this program will probably crash or exhibit some other undefined behavior. Therefore, you must take care when pushing data onto the stack within a procedure that you properly pop that data prior to returning from the procedure.
If you do not specify the @noframe
option when writing a procedure, HLA automatically generates code at the beginning of the procedure that pushes some data onto the stack. Therefore, unless you understand exactly what is going on and you've taken care of this data HLA pushes on the stack, you should never execute the bare ret
instruction inside a procedure that does not have the @noframe
option. Doing so will attempt to return to the location specified by this data (which is not a return address) rather than properly returning to the caller. In procedures that do not have the @noframe
option, use the exit
or exitif
statement to return from the procedure.
Popping extra data off the stack prior to executing the ret
statement can also create havoc in your programs. Consider the following defective procedure:
procedure messedUpToo; @noframe; @nodisplay; begin messedUpToo; pop( eax ); ret(); end messedUpToo;
Upon reaching the ret
instruction in this procedure, the 80x86 stack looks something like that shown in Figure 5-2.
Once again, the ret
instruction blindly pops whatever data happens to be on the top of the stack and attempts to return to that address. Unlike the previous example, where it was very unlikely that the top of stack contained a valid return address (because it contained the value in EAX), there is a small possibility that the top of stack in this example actually does contain a return address. However, this will not be the proper return address for the messedUpToo
procedure; instead, it will be the return address for the procedure that called messedUpToo
. To understand the effect of this code, consider the program in Example 5-11.
Example 5-11. Effect of popping too much data off the stack
program extraPop; #include( "stdlib.hhf" ); // Note that the following procedure pops // excess data off the stack (in this case, // it pops messedUpToo's return address). procedure messedUpToo; @noframe; @nodisplay; begin messedUpToo; stdout.put( "Entered messedUpToo" nl ); pop( eax ); ret(); end messedUpToo; procedure callsMU2; @noframe; @nodisplay; begin callsMU2; stdout.put( "calling messedUpToo" nl ); messedUpToo(); // Because messedUpToo pops extra data // off the stack, the following code // never executes (because the data popped // off the stack is the return address that // points at the following code). stdout.put( "Returned from messedUpToo" nl ); ret(); end callsMU2; begin extraPop; stdout.put( "Calling callsMU2" nl ); callsMU2(); stdout.put( "Returned from callsMU2" nl ); end extraPop;
Because a valid return address is sitting on the top of the stack, you might think that this program will actually work (properly). However, note that when returning from the messedUpToo
procedure, this code returns directly to the main program rather than to the proper return address in the callsMU2
procedure. Therefore, all code in the callsMU2
procedure that follows the call to messedUpToo
does not execute. When reading the source code, it may be very difficult to figure out why those statements are not executing because they immediately follow the call to the messedUpToo
procedure. It isn't clear, unless you look very closely, that the program is popping an extra return address off the stack and therefore doesn't return to callsMU2
but rather returns directly to whoever calls callsMU2
. Of course, in this example it's fairly easy to see what is going on (because this example is a demonstration of this problem). In real programs, however, determining that a procedure has accidentally popped too much data off the stack can be much more difficult. Therefore, you should always be careful about pushing and popping data in a procedure. You should always verify that there is a one-to-one relationship between the pushes in your procedures and the corresponding pops.