TOC PREV NEXT INDEX

Webster Home Page



8.7 Parameters

Although there is a large class of procedures that are totally self-contained, most procedures require some input data and return some data to the caller. Parameters are values that you pass to and from a procedure. In straight assembly language, passing parameters can be a real chore. Fortunately, HLA provides a HLL-like syntax for procedure declarations and for procedure calls involving parameters. This chapter will present HLA's HLL parameter syntax. Later chapters on Intermediate Procedures and Advanced Procedures will deal with the low-level mechanisms for passing parameters in pure assembly code.

The first thing to consider when discussing parameters is how we pass them to a procedure. If you are familiar with Pascal or C/C++ you've probably seen two ways to pass parameters: pass by value and pass by reference. HLA certainly supports these two parameter passing mechanisms. However, HLA also supports pass by value/result, pass by result, pass by name, and pass by lazy evaluation. Of course, HLA is assembly language so it is possible to pass parameters in HLA using any scheme you can dream up (at least, any scheme that is possible at all on the CPU). However, HLA provides special HLL syntax for pass by value, reference, value/result, result, name, and lazy evaluation.

Because pass by value/result, result, name, and lazy evaluation are somewhat advanced, this chapter will not deal with those parameter passing mechanisms. If you're interested in learning more about these parameter passing schemes, see the chapters on Intermediate and Advanced Procedures.

Another concern you will face when dealing with parameters is where you pass them. There are lots of different places to pass parameters; the chapter on Intermediate Procedures will consider these places in greater detail. In this chapter, since we're using HLA's HLL syntax for declaring and calling procedures, we'll wind up passing procedure parameters on the stack. You don't really need to concern yourself with the details since HLA abstracts them away for you; however, do keep in mind that procedure calls and procedure parameters make use of the stack. Therefore, something you push on the stack immediately before a procedure call is not going to be immediately on the top of the stack upon entry into the procedure.

8.7.1 Pass by Value

A parameter passed by value is just that - the caller passes a value to the procedure. Pass by value parameters are input-only parameters. That is, you can pass them to a procedure but the procedure cannot return them. In HLA the idea of a pass by value parameter being an input only parameter makes a lot of sense. Given the HLA procedure call:

		CallProc(I);
 

If you pass I by value, then CallProc does not change the value of I, regardless of what happens to the parameter inside CallProc.

Since you must pass a copy of the data to the procedure, you should only use this method for passing small objects like bytes, words, and double words. Passing arrays and records by value is very inefficient (since you must create and pass a copy of the object to the procedure).

HLA, like Pascal and C/C++, passes parameters by value unless you specify otherwise. Here's what a typical function looks like with a single pass by value parameter:

    procedure PrintNSpaces( N:uns32 );
 
    begin PrintNSpaces;
 
    
 
        push( ecx );
 
        mov( N, ecx );
 
        repeat
 
        
 
            stdout.put( ' ' );  // Print 1 of N spaces.
 
            dec( ecx );         // Count off N spaces.
 
            
 
        until( ecx = 0 );
 
        pop( ecx );
 

 
    end PrintNSpaces;
 

 

The parameter N in PrintNSpaces is known as a formal parameter. Anywhere the name N appears in the body of the procedure the program references the value passed through N by the caller.

The calling sequence for PrintNSpaces can be any of the following:

				PrintNSpaces( constant );
 
				PrintNSpaces( reg32 );
 
				PrintNSpaces( uns32_variable );
 

 

Here are some concrete examples of calls to PrintNSpaces:

	PrintNSpaces( 40 );
 
	PrintNSpaces( EAX );
 
	PrintNSpaces( SpacesToPrint );
 

 

The parameter in the calls to PrintNSpaces is known as an actual parameter. In the examples above, 40, EAX, and SpacesToPrint are the actual parameters.

Note that pass by value parameters behave exactly like local variables you declare in the VAR section with the single exception that the procedure's caller initializes these local variables before it passes control to the procedure.

HLA uses positional parameter notation just like most high level languages. Therefore, if you need to pass more than one parameter, HLA will associate the actual parameters with the formal parameters by their position in the parameter list. The following PrintNChars procedure demonstrates a simple procedure that has two parameters.

    procedure PrintNChars( N:uns32; c:char );
 
    begin PrintNChars;
 
    
 
        push( ecx );
 
        mov( N, ecx );
 
        repeat
 
        
 
            stdout.put( c );    // Print 1 of N characters.
 
            dec( ecx );         // Count off N characters.
 
            
 
        until( ecx = 0 );
 
        pop( ecx );
 

 
    end PrintNChars;
 

 

The following is an invocation of the PrintNChars procedure that will print 20 asterisk characters:

PrintNChars( 20, `*' );
 

 

Note that HLA uses semicolons to separate the formal parameters in the procedure declaration and it uses commas to separate the actual parameters in the procedure invocation (Pascal programmers should be comfortable with this notation). Also note that each HLA formal parameter declaration takes the following form:

parameter_identifier : type_identifier
 

 

In particular, note that the parameter type has to be an identifier. None of the following are legal parameter declarations because the data type is not a single identifier:

	PtrVar: pointer to uns32
 
	ArrayVar: uns32[10]
 
	recordVar: record i:int32; u:uns32; endrecord
 
	DynArray: array.dArray( uns32, 2 )
 

 

However, don't get the impression that you cannot pass pointer, array, record, or dynamic array variables as parameters. The trick is to declare a data type for each of these types in the TYPE section. Then you can use a single identifier as the type in the parameter declaration. The following code fragment demonstrates how to do this with the four data types above:

type
 
	uPtr:			pointer to uns32;
 
	uArray10:			uns32[10];
 
	recType:			record i:int32; u:uns32; endrecord
 
	dType:			array.dArray( uns32, 2 );
 

 
	procedure FancyParms
 
	( 
 
		PtrVar: uPtr; 
 
		ArrayVar:uArray10; 
 
		recordVar:recType; 
 
		DynArray: dtype 
 
	);
 
	begin FancyParms;
 
		.
 
		.
 
		.
 
	end FancyParms;
 

By default, HLA assumes that you intend to pass a parameter by value. HLA also lets you explicitly state that a parameter is a value parameter by prefacing the formal parameter declaration with the VAL keyword. The following is a version of the PrintNSpaces procedure that explicitly states that N is a pass by value parameter:

    procedure PrintNSpaces( val N:uns32 );
 
    begin PrintNSpaces;
 
    
 
        push( ecx );
 
        mov( N, ecx );
 
        repeat
 
        
 
            stdout.put( ' ' );  // Print 1 of N spaces.
 
            dec( ecx );         // Count off N spaces.
 
            
 
        until( ecx = 0 );
 
        pop( ecx );
 

 
    end PrintNSpaces;
 

 

Explicitly stating that a parameter is a pass by value parameter is a good idea if you have multiple parameters in the same procedure declaration that use different passing mechanisms.

When you pass a parameter by value and call the procedure using the HLA high level language syntax, HLA will automatically generate code that will make a copy of the actual parameter's value and copy this data into the local storage for that parameter (i.e., the formal parameter). For small objects pass by value is probably the most efficient way to pass a parameter. For large objects, however, HLA must generate code that copies each and every byte of the actual parameter into the formal parameter. For large arrays and records this can be a very expensive operation1. Unless you have specific semantic concerns that require you to pass an array or record by value, you should use pass by reference or some other parameter passing mechanism for arrays and records.

When passing parameters to a procedure, HLA checks the type of each actual parameter and compares this type to the corresponding formal parameter. If the types do not agree, HLA then checks to see if either the actual or formal parameter is a byte, word, or dword object and the other parameter is one, two, or four bytes in length (respectively). If the actual parameter does not satisfy either of these conditions, HLA reports a parameter type mismatch error. If, for some reason, you need to pass a parameter to a procedure using a different type than the procedure calls for, you can always use the HLA type coercion operator to override the type of the actual parameter.

8.7.2 Pass by Reference

To pass a parameter by reference, you must pass the address of a variable rather than its value. In other words, you must pass a pointer to the data. The procedure must dereference this pointer to access the data. Passing parameters by reference is useful when you must modify the actual parameter or when you pass large data structures between procedures.

To declare a pass by reference parameter you must preface the formal parameter declaration with the VAR keyword. The following code fragment demonstrates this:

	procedure UsePassByReference( var PBRvar: int32 );
 
	begin UsePassByReference;
 
		.
 
		.
 
		.
 
	end UsePassByReference;
 

 

Calling a procedure with a pass by reference parameter uses the same syntax as pass by value except that the parameter has to be a memory location; it cannot be a constant or a register. Furthermore, the type of the memory location must exactly match the type of the formal parameter. The following are legal calls to the procedure above (assuming i32 is an int32 variable):

UsePassByReference( i32 );
 
UsePassByReference( (type int32 [ebx] ) );
 

 

The following are all illegal UsePassbyReference invocations (assumption: charVar is of type char):

UsePassByReference( 40 );										// Constants are illegal.
 
UsePassByReference( EAX );										// Bare registers are illegal.
 
UsePassByReference( charVar );										// Actual parameter type must match
 
										//  the formal parameter type.
 

Unlike the high level languages Pascal and C++, HLA does not completely hide the fact that you are passing a pointer rather than a value. In a procedure invocation, HLA will automatically compute the address of a variable and pass that address to the procedure. Within the procedure itself, however, you cannot treat the variable like a value parameter (as you could in most HLLs). Instead, you treat the parameter as a dword variable containing a pointer to the specified data. You must explicitly dereference this pointer when accessing the parameter's value. The following example provides a simple demonstration of this:


 
program PassByRefDemo;
 
#include( "stdlib.hhf" );
 

 
var 
 
    i:  int32;
 
    j:  int32;
 
    
 
    procedure pbr( var a:int32; var b:int32 );
 
    const
 
        aa: text := "(type int32 [ebx])";
 
        bb: text := "(type int32 [ebx])";
 
        
 
    begin pbr;
 
    
 
        push( eax );
 
        push( ebx );        // Need to use EBX to dereference a and b.
 
        
 
        // a = -1;
 
        
 
        mov( a, ebx );      // Get ptr to the "a" variable.
 
        mov( -1, aa );      // Store -1 into the "a" parameter.
 
        
 
        // b = -2;
 
        
 
        mov( b, ebx );      // Get ptr to the "b" variable.
 
        mov( -2, bb );      // Store -2 into the "b" parameter.
 
        
 
        // Print the sum of a+b.
 
        // Note that ebx currently contains a pointer to "b".
 
        
 
        mov( bb, eax );
 
        mov( a, ebx );      // Get ptr to "a" variable.
 
        add( aa, eax );
 
        stdout.put( "a+b=", (type int32 eax), nl );
 
            
 
    end pbr;
 
    
 
begin PassByRefDemo;
 

 
    // Give i and j some initial values so
 
    // we can see that pass by reference will
 
    // overwrite these values.
 
    
 
    mov( 50, i );
 
    mov( 25, j );
 
    
 
    // Call pbr passing i and j by reference
 
    
 
    pbr( i, j );
 
    
 
    // Display the results returned by pbr.
 
     
 
    stdout.put
 
    ( 
 
        "i=  ", i, nl, 
 
        "j=  ", j, nl 
 
    );
 

 
end PassByRefDemo;
 

 
Program 8.8	 Accessing Pass by Reference Parameters
 

Passing parameters by reference can produce some peculiar results in some rare circumstances. Consider the pbr procedure in Program 8.8. Were you to modify the call in the main program to be "pbr(i,i)" rather than "pbr(i,j);" the program would produce the following non-intuitive output:

a+b=-4
 
i=  -2;
 
j=  25;
 

 

The reason this code displays "a+b=-4" rather than the expected "a+b=-3" is because the "pbr(i,i);" call passes the same actual parameter for a and b. As a result, the a and b reference parameters both contain a pointer to the same memory location- that of the variable i. In this case, a and b are aliases of one another. Therefore, when the code stores -2 at the location pointed at by b, it overwrites the -1 stored earlier at the location pointed at by a. When the program fetches the value pointed at by a and b to compute their sum, both a and b point at the same value, which is -2. Summing -2 + -2 produces the -4 result that the program displays. This non-intuitive behavior is possible anytime you encounter aliases in a program. Passing the same variable as two different parameters probably isn't very common. But you could also create an alias if a procedure references a global variable and you pass that same global variable by reference to the procedure (this is a good example of yet one more reason why you should avoid referencing global variables in a procedure).

Pass by reference is usually less efficient than pass by value. You must dereference all pass by reference parameters on each access; this is slower than simply using a value since it typically requires at least two instructions. However, when passing a large data structure, pass by reference is faster because you do not have to copy a large data structure before calling the procedure. Of course, you'd probably need to access elements of that large data structure (e.g., an array) using a pointer, so very little efficiency is lost when you pass large arrays by reference.

8.8 Functions and Function Results

Functions are procedures that return a result. In assembly language, there are very few syntactical differences between a procedure and a function which is why HLA doesn't provide a specific declaration for a function. Nevertheless, although there is very little syntactical difference between assembly procedures and functions, there are considerable semantic differences. That is, although you can declare them the same way in HLA, you use them differently.

Procedures are a sequence of machine instructions that fulfill some activity. The end result of the execution of a procedure is the accomplishment of that activity. Functions, on the other hand, execute a sequence of machine instructions specifically to compute some value to return to the caller. Of course, a function can perform some activity as well and procedures can undoubtedly compute some values, but the main difference is that the purpose of a function is to return some computed result; procedures don't have this requirement.

A good example of a procedure is the stdout.puti32 procedure. This procedure requires a single int32 parameter. The purpose of this procedure is to print the decimal conversion of this integer value to the standard output device. Note that stdout.puti32 doesn't return any kind of value that is usable by the calling program.

A good example of a function is the cs.member function. This function expects two parameters: the first is a character value and the second is a character set value. This function returns true (1) in EAX if the character is a member of the specified character set. It returns false if the character parameter is not a member of the character set.

Logically, the fact that cs.member returns a usable value to the calling code (in EAX) while stdout.puti32 does not is a good example of the main difference between a function and a procedure. So, in general, a procedure becomes a function by virtue of the fact that you explicitly decide to return a value somewhere upon procedure return. No special syntax is needed to declare and use a function. You still write the code as a procedure.

8.8.1 Returning Function Results

The 80x86's registers are the most popular place to return function results. The cs.member routine in the HLA Standard Library is a good example of a function that returns a value in one of the CPU's registers. It returns true (1) or false (0) in the EAX register. By convention, programmers try to return eight, sixteen, and thirty-two bit (non-real) results in the AL, AX, and EAX registers, respectively2. For example, this is where most high level languages return these types of results.

Of course, there is nothing particularly sacred about the AL/AX/EAX register. You could return function results in any register if it is more convenient to do so. However, if you don't have a good reason for not using AL/AX/EAX, then you should follow the convention. Doing so will help others understand your code better since they will generally assume that your functions return small results in the AL/AX/EAX register set.

If you need to return a function result that is larger than 32 bits, you obviously must return it somewhere besides in EAX (which can hold values 32 bits or less). For values slightly larger than 32 bits (e.g., 64 bits or maybe even as many as 128 bits) you can split the result into pieces and return those parts in two or more registers. For example, it is very common to see programs returning 64-bit values in the EDX:EAX register pair (e.g., the HLA Standard Library stdin.geti64 function returns a 64-bit integer in the EDX:EAX register pair).

If you need to return a really large object as a function result, say an array of 1,000 elements, you obviously are not going to be able to return the function result in the registers. There are two common ways to deal with really large function return results: either pass the return value as a reference parameter or allocate storage on the heap (using malloc) for the object and return a pointer to it in a 32-bit register. Of course, if you return a pointer to storage you've allocated on the heap, the calling program must free this storage when it is done with it.

8.8.2 Instruction Composition in HLA

Several HLA Standard Library functions allow you to call them as operands of other instructions. For example, consider the following code fragment:

if( cs.member( al, {`a'..'z'}) ) then
 
	.
 
	.
 
	.
 
endif;
 

 

As your high level language experience (and HLA experience) should suggest, this code calls the cs.member function to check to see if the character in AL is a lower case alphabetic character. If the cs.member function returns true then this code fragment executes the then section of the IF statement; however, if cs.member returns false, this code fragment skips the IF..THEN body. There is nothing spectacular here except for the fact that HLA doesn't support function calls as boolean expressions in the IF statement (look back at Chapter Two in Volume One to see the complete set of allowable expressions). How then, does this program compile and run producing the intuitive results?

The very next section will describe how you can tell HLA that you want to use a function call in a boolean expression. However, to understand how this works, you need to first learn about instruction composition in HLA.

Instruction composition lets you use one instruction as the operand of another. For example, consider the MOV instruction. It has two operands, a source operand and a destination operand. Instruction composition lets you substitute a valid 80x86 machine instruction for either (or both) operands. The following is a simple example:

							mov( mov( 0, eax ), ebx );
 

 

Of course the immediate question is "what does this mean?" To understand what is going on, you must first realize that most instructions "return" a value to the compiler while they are being compiled. For most instructions, the value they "return" is their destination operand. Therefore, "mov( 0, eax);" returns the string "eax" to the compiler during compilation since EAX is the destination operand. Most of the time, specifically when an instruction appears on a line by itself, the compiler ignores the string result the instruction returns. However, HLA uses this string result whenever you supply an instruction in place of some operand; specifically, HLA uses that string in place of the instruction as the operand. Therefore, the MOV instruction above is equivalent to the following two instruction sequence:

	mov( 0, eax );     // HLA compiles interior instructions first.
 
	mov( eax, ebx );
 

 

When processing composed instructions (that is, instruction sequences that have other instructions as operands), HLA always works in an " left-to-right then depth-first (inside-out)" manner. To make sense of this, consider the following instructions:

	add( sub( mov( i, eax ), mov( j, ebx )), mov( k, ecx ));
 

 

To interpret what is happening here, begin with the source operand. It consists of the following:

	sub( mov( i, eax ), mov( j, ebx ))
 

 

The source operand for this instruction is "mov( i, eax )" and this instruction does not have any composition, so HLA emits this instruction and returns its destination operand (EAX) for use as the source to the SUB instruction. This effectively gives us the following:

	sub( eax, mov( j, ebx ))
 

 

Now HLA compiles the instruction that appears as the destination operand ("mov( j, ebx )") and returns its destination operand (EBX) to substitute for this MOV in the SUB instruction. This yields the following:

	sub( eax, ebx )
 

 

This is a complete instruction, without composition, that HLA can compile. So it compiles this instruction and returns its destination operand (EBX) as the string result to substitute for the SUB in the original ADD instruction. So the original ADD instruction now becomes:

	add( ebx, mov(i, ecx ));
 

 

HLA next compiles the MOV instruction appearing in the destination operand. It returns its destination operand as a string that HLA substitutes for the MOV, finally yielding the simple instruction:

	add( ebx, ecx );
 

 

The compilation of the original ADD instruction, therefore, yields the following instruction sequence:

	mov( i, eax );
 
	mov( j, ebx );
 
	sub( eax, ebx );
 
	mov( k, ecx );
 
	add( ebx, ecx );
 

 

Whew! It's rather difficult to look at the original instruction and easily see that this sequence is the result. As you can easily see in this example, overzealous use of instruction composition can produce nearly unreadable programs. You should be very careful about using instruction composition in your programs. With only a few exceptions, writing a composed instruction sequence makes your program harder to read.

Note that the excessive use of instruction composition may make errors in your program difficult to decipher. Consider the following HLA statement:

		add( mov( eax, i ), mov( ebx, j ) );
 

 

This instruction composition yields the 80x86 instruction sequence:

		mov( eax, i );
 
		mov( ebx, j );
 
		add( i, j );
 

 

Of course, the compiler will complain that you're attempting to add one memory location to another. However, the instruction composition effectively masks this fact and makes it difficult to comprehend the cause of the error message. Moral of the story: avoid using instruction composition unless it really makes your program easier to read. The few examples in this section demonstrate how not to use instruction composition.

There are two main areas where using instruction composition can help make your programs more readable. The first is in HLA's high level language control structures. The other is in procedure parameters. Although instruction composition is useful in these two cases (and probably a few others as well), this doesn't give you a license to use extremely convoluted instructions like the ADD instruction in the previous example. Instead, most of the time you will use a single instruction or a function call in place of a single operand in a high level language boolean expression or in a procedure/function parameter.

While we're on the subject, exactly what does a procedure call return as the string that HLA substitutes for the call in an instruction composition? For that matter, what do statements like IF..ENDIF return? How about instructions that don't have a destination operand? Well, function return results are the subject of the very next section so you'll read about that in a few moments. As for all the other statements and instructions, you should check out the HLA reference manual. It lists each instruction and its "RETURNS" value. The "RETURNS" value is the string that HLA will substitute for the instruction when it appears as the operand to another instruction. Note that many HLA statements and instructions return the empty string as their "RETURNS" value (by default, so do procedure calls). If an instruction returns the empty string as its composition value, then HLA will report an error if you attempt to use it as the operand of another instruction. For example, the IF..ENDIF statement returns the empty string as its "RETURNS" value, so you may not bury an IF..ENDIF inside another instruction.

8.8.3 The HLA RETURNS Option in Procedures

HLA procedure declarations allow a special option that specifies the string to use when a procedure invocation appears as the operand of another instruction: the RETURNS option. The syntax for a procedure declaration with the RETURNS option is as follows:

	procedure ProcName ( optional parameters );  RETURNS( string_constant );
 
		<< Local declarations >>
 
	begin ProcName;
 
		<< procedure statements >>
 
	end ProcName;
 

 

If the RETURNS option is not present, HLA associates the empty string with the RETURNS value for the procedure. This effectively makes it illegal to use that procedure invocation as the operand to another instruction.

The RETURNS option requires a single string parameter surrounded by parentheses. This must be a string constant3. HLA will substitute this string constant for the procedure call if it ever appears as the operand of another instruction. Typically this string constant is a register name; however, any text that would be legal as an instruction operand is okay here. For example, you could specify memory address or constants. For purposes of clarity, you should always specify the location of a function's return value in the RETURNS parameter.

As an example, consider the following boolean function that returns true or false in the EAX register if the single character parameter is an alphabetic character4:

procedure IsAlphabeticChar( c:char ); RETURNS( "EAX" );
 
begin IsAlphabeticChar;
 

 
	// Note that cs.member returns true/false in EAX
 

 
	cs.member( c, {`a'..'z', `A'..'Z'} );
 

 
end IsAlphabeticChar;
 

 

Once you tack the RETURNS option on the end of this procedure declaration you can legally use a call to IsAlphabeticChar as an operand to other HLA statements and instructions:

	mov( IsAlphabeticChar( al ), EBX );
 
		.
 
		.
 
		.
 
	if( IsAlphabeticChar( ch ) ) then
 
		.
 
		.
 
		.
 
	endif;
 

 

The last example above demonstrates that, via the RETURNS option, you can embed calls to your own functions in the boolean expression field of various HLA statements. Note that the code above is equivalent to

	IsAlphabeticChar( ch );
 
	if( EAX ) then
 
		.
 
		.
 
		.
 
	endif;
 

 

Not all HLA high level language statements expand composed instructions before the statement. For example, consider the following WHILE statement:

	while( IsAlphabeticChar( ch ) ) do
 
		.
 
		.
 
		.
 
	endwhile;
 

 

This code does not expand to the following:

	IsAlphabeticChar( ch );
 
	while( EAX ) do
 
		.
 
		.
 
		.
 
	endwhile;
 

 

Instead, the call to IsAlphabeticChar expands inside the WHILE's boolean expression so that the program calls this function on each iteration of the loop.

You should exercise caution when entering the RETURNS parameter. HLA does not check the syntax of the string parameter when it is compiling the procedure declaration (other than to verify that it is a string constant). Instead, HLA checks the syntax when it replaces the function call with the RETURNS string. So if you had specified "EAZ" instead of "EAX" as the RETURNS parameter for IsAlphabeticChar in the previous examples, HLA would not have reported an error until you actually used IsAlphabeticChar as an operand. Then of course, HLA complains about the illegal operand and it's not at all clear what the problem is by looking at the IsAlphabeticChar invocation. So take special care not to introduce typographical errors in the RETURNS string; figuring out such errors later can be very difficult.

1Note to C/C++ programmers: HLA does not automatically pass arrays by reference. If you specify an array type as a formal parameter, HLA will emit code that makes a copy of each and every byte of that array when you call the associated procedure.

2In the next chapter you'll see where most programmers return real results.

3Do note, however, that it doesn't have to be a string literal constant. A CONST string identifier or even a constant string expression is legal here.

4Before you run off and actually use this function in your own programs, note that the HLA Standard Library provides the char.isAlpha function that provides this test. See the HLA documentation for more details.


Web Site Hits Since
Jan 1, 2000

TOC PREV NEXT INDEX