Although many procedures 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 high-level-language-like syntax for procedure declarations and for procedure calls involving parameters. This section presents HLA's high-level parameter syntax. Later sections in this chapter 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 high-level 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 book will not deal with those parameter-passing mechanisms. If you're interested in learning more about these parameter-passing schemes, see the HLA reference manual or check out the electronic versions of this text at http://webster.cs.ucr.edu/ or http://www.artofasm.com/.
Another concern you will face when dealing with parameters is where you pass them. There are many different places to pass parameters; in this section we'll pass procedure parameters on the stack. You don't really need to concern yourself with the details because HLA abstracts them away for you; however, do keep in mind that procedure calls and procedure parameters make use of the stack. Therefore, whatever you push on the stack immediately before a procedure call is not going to be on the top of the stack upon entry into the procedure.
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 values through them. 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
.
Because you must pass a copy of the data to the procedure, you should use this method only for passing small objects like bytes, words, and double words. Passing large arrays and records by value is very inefficient (because 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. The following is 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 as most high-level languages do. 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 (that is, 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 operation.[75] Unless you have specific semantic concerns that require you to pass a large 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 the formal parameter is a byte, word, or double-word object and the other parameter is 1, 2, or 4 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.
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 (assuming 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 high-level languages). Instead, you treat the parameter as a double-word variable containing a pointer to the specified data. You must explicitly dereference this pointer when accessing the parameter's value. The example appearing in Example 5-8 provides a simple demonstration of this.
Example 5-8. Accessing pass-by-reference parameters
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;
Passing parameters by reference can produce some peculiar results in some rare circumstances. Consider the pbr
procedure in Example 5-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 nonintuitive 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 nonintuitive behavior is possible anytime you encounter aliases in a program. Passing the same variable as two different reference 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 because 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 the large data structure before calling the procedure. Of course, you'd probably need to access elements of that large data structure (for example, an array) using a pointer, so very little efficiency is lost when you pass large arrays by reference.
[75] Note 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.