Subroutines & Procedures

All high level language programmers are familiar with the concept of subroutines and procedures. Here's an example from C:


#include

int fact(int);

main(int argc, char **argv)
{
int arg, ans;

if(argc < 2)
{
printf("Please re-enter with an integer on the CL\n");
exit(0);
}

arg = atoi(argv[1]); /* convert command line arg to integer */
ans = fact(arg); /* get it's factorial */
printf("%d! = %d\n", arg, ans); /* display the answer */
}

int fact(int num)
{
/* calculate factorial of num */
int ans = num;

if (num == 0) /* 0! is defined as 1 */
return(1);
else
while (num > 0) /* n! = n * (n - 1)! */
ans *= --num; &nbssp; /* same as ans = ans * (num - 1) */
return(ans); /* return answer */
}

In the above example the subroutine (or procedure) is fact(int). We could have put the fact code in the main routine, but by seperating it out we can now cut and paste the code into another program that uses factorial calculations; code re-use. Another way we could re-use the code is to put the fact subroutine (called functions in the C language) into a "library" of routines, and any program that needs to use fact can just call it up from that library. In MACRO-32, the way we used fact in the example, coul've been accomplished wih a subroutine. The library concept leads to procedures in MACRO-32.

Could've been accomplished by a subroutine, because the above example uses the same calling mechanism as MACRO-32 procedures. A subroutine is just what the name implies; a routine that's right below the main routine. In other words, a subroutine is by definition part of the same program as the calling routine; BASIC programmers are familiar with the GOSUB statement as a subroutine. A BASIC programmer calls a subroutine by making a GOSUB statment aimed at a label in his program; the label that marks the beginning of the subroutine. A RETURN statement returns the program to the line of code right after the GOSUB statment.

MACRO-32 has a mechanism similar to BASIC's GOSUB statement. Such a mechanism must:

  1. Store the address of the next line of code after the "GOSUB"
  2. GOTO the label that marks the beginning of the subroutine
  3. At the end of the subroutine GOTO the address we stored before the "GOSUB"

Steps 1. and 2. are accomplished with one of three statements:

  1. BSBB - Branch SuBroutine Byte ; can jump -128 to 127 bytes
  2. BSBW - Branch SuBroutine Word ; can jump -32768 to 32767 bytes
  3. JSB - Jump to SuBroutine ; no limit

These three statements all store the address of the statement after themselves on the stack. Actually, what they do is:

  1. Add their own size (incuding arguments) to the value in the PC, and place the result on the stack.
  2. Insert the address of the subroutine label in the PC.

At the end of the subroutine is a statement, RSB (Return from SuBroutine). RSB POPs that stack into the PC. Notice we don't mess with the stack, except for storage and retreival of return addresses. If we add anything else to the stack, or pop anything else off of it, we run the risk of destroying our return mechanism. Therefore we pass arguments to the subroutine through registers, or global variables (just like in BASIC GOSUBs). ...and like GOSUBs this is very neat and efficient.

Getting back to our C example with a factorial procedure: What if we want to stick this factorial function somewhere on the system where other programs can access it as well? Obviously the above mechanism won't work anymore. What we need is a mechanism that allows two programs to communicate with each other. The two programs in question are invoked as one program, and therefore share the same program stack. So we can have our main procedure

  1. push arguments onto the stack
  2. store the address of the next instruction like we did for subroutines.
  3. insert the address of the "called" procedure into the PC, so our next point in the program becomes the procedure.
  4. store the value in registers we will use in the "called" procedure.
  5. pop off the values on the stack.
  6. execute all the statements in the procedure including a RET. like RSB, but it performs the additional task of restoring registers we saved in step 3.

An example is:

NUM: .BLKL

.ENTRY BEGIN ^M<>

MOVL #10, NUM ; we want 10!

PUSHAL NUM ; number to get factorial value of

CALLS #1, FACT ; #1 is a literal, telling the procedure there is 1 val on the stack

POP NUM ; put answer back into NUM

MOVL #SS_NORMAL, 0 ; we always get a good return

RET

;

.ENTRY FACT ^M ; R0 and R1 are used by system

MOVL 4(AP), R2 ; don't pop since we will return on the stack

MOVL R2, R3 ; have to save the original value of num

$10: DECL R3 ; num! = num * (num -1)!

BEQL 20$ ; PSL value Z = 1, means R3 = 0

MULL R3, R2 ; R2 = R3 * R2

BRB $10 ; short jump

$20: MOVL R2, 4(AP) ; return answer

RET

.END BEGIN

  1. We push the arguments on the stack in the old fashioned way, with a PUSHAL statement.
  2. The first argument in the CALLS statement is the number of arguments pushed on the stack. So the assembler knows how many arguments to add the length of, and it has a table of the length of those arguments; this value is added to the value of the address of the next statement (along with the length of the CALLS itself) and stored in R13; the frame pointer..
  3. The address of the "called" procedure is the last argument of the CALLS statement; it's moved into PC.
  4. We will use the R2 and R3 registers so the ^M tells the assembler we want to store those values.
  5. We don't have to pop the stack; rarely done, anyway. Stacks are upside down in memory; i.e., you place values on top of the stack, but they go into increasing value memory addresses. The AP (Argument Pointer) is increased by 4 bytes with every longword we place on the stack So we can use register displacement addressing to find the arguments; address = displacement + value in register, which is written displacement(register). In this case it's 4(AP).

We move the answer back into the same place in the stack, because that's where the calling routine expects it; we could have done this differently. However, the way we did this required we popped the stack on returning. Note that the RET instruction moves R13 (also called FP) into the PC (which is R15); the address of the next instruction before making a call.

There's another way to call, CALLG, but I'll leave that for another VIOTW.


Return to VAX Instruction of the Week