C-Language & Subroutines
Examples using Intel 8086 Assembly Language
The C-language is a powerful high-level language used on a wide variety of computers. Because C is portable and addresses low-level operations, it is commonly used in DSP environments.
As a futher advantage, C compilers generally create assembly language code to be assembled on the way to making executable binary code. The assembly language code can be studied to appreciate what the C compiler is really doing (and when it is not doing what was expected). It would be possible to tweak the assembly code and to continue by running an assembler manually. Generally, C-language compilers feature a command line flag to keep the assembly language version, or to stop compilation once it is generated.
In the following example the Intel 8086 assembly language corresponding to some simple C code will be examined. Note that convention has 8086 operations showing source and destination operands with destination first (for example: ADD destination, source). Also note the 8086 stack grows downwards towards 0000 (the stack pointer is decremented for a PUSH). Addresses in the 8086 are byte oriented, and each integer type variable occupies 2 bytes. The principles illustrated here are identical when code is examined on different hardware platforms. Same example using TMS320C30
Note that in the C-language, the main( ) program and a subroutine( ) are basically structured in the same way. In fact, the main( ) program really is a subroutine (called from the operating system?). The discussion below can be extended to the main( ) program itself.
CALLING SUBROUTINES
Consider a simple program which calls a small subroutine. (Assume this does something useful; the purpose here is to examine the mechanics of subroutine calls.) The assembly language code generated by the following C-language segments will be studied.
MAIN PROGRAM
main()
{ int a,b,c;
c = adder(a,b)
}
|
SUBROUTINE
adder(int a,b)
{ int z;
z = a + b;
return z;
}
|
To see what is really happening, the essence of this operation can now be examined in 8086 assembly language. (Here, SP="Stack Pointer", BP="Base Pointer", AX="16-bit accumulator")
See the complete compiler output file.
MAIN PROGRAM
... PUSH b PUSH a CALL adder ADD SP,4 (result is in AX) ... |
SUBROUTINE
adder: PUSH BP
MOV BP,SP
SUB SP,2
MOV AX, [BP+4]
ADD AX, [BP+6]
MOV [BP-2], AX
MOV SP,BP
POP BP
RET
|
Note that the subroutine code in BLUE is the only part which is specific to the addition accomplished by this routine. The surrounding instructions set up and tear down the subroutine, and will be found in virtually all subroutines.
DETAILED ANALYSIS
Several questions can be answered by examining this code:
How do the values (a,b) get passed to the subroutine?
They are pushed on the stack, the rightmost first (b before a) where the subroutine can access them when it is called. Note that after two PUSHes (4 bytes), the stack must be restored after the CALL and ADD SP,4 accomplishes this. Observe that only copies of (a,b) were placed on the stack. No manipulation with the subroutine will affect the original values of (a,b) within the main program.
How does the result (a+b) get passed back to the main program?
Integer variables are often returned using an internal CPU register. In this case, the 8086 register AX holds the result. Longer variables would be passed back using a pointer to the value.
Where do local variables (z) get stored?
Space for local variables is created on the stack when a subroutine is called. The statement SUB SP,2 effectively creates space for 2 bytes to store the integer z (by skipping over space which would be overwritten if any more stack activity occurred). This assignment vanishes when the subroutine is done and the value of SP is restored as MOV SP,BP. In this way, local storage is created only when needed, and recursive subroutines are made possible.
How are variables accessed within the subroutine?
All these variables (a,b) and (z) are present on the stack. A copy of the stack pointer is placed in the 8086 Base Pointer (BP) and BP is indexed to access the variables. Before this happens, BP itself is saved on the stack.
The stack-related setup activity is as follows:
At this point, the actual function of the subroutine (in this case z=a+b) can occur. The input parameters (a,b) and the local variable (z) are simply addressed through the BP register and a suitable (constant) offset.
The state of the stack is:
| ... | --- |
| [SP-2] | new top of stack |
| [BP-2] | space for z |
| [BP] | old BP |
| [BP+2] | return address |
| [BP+4] | value of a |
| [BP+6] | value of b |
| ... | --- |
Once the necessary math is done, the value (a+b) will be stored in (z) at address [BP-2]. A copy remains in AX to be passed back to the calling program. It is time to exit the subroutine:
The stack-related tear down activity is as follows:
LOCAL VARIABLE STORAGE
Note that the space created for z (2 bytes) could easily be any size, limited only be the available stack space. This allocation is created dynamically and vanishes when the subroutine exits. This is essentially how all local variables are created, including those within main( ). In this example, the values for (a,b,c) would probably be found in memory immediately above the stack locations shown above.
CONCLUSIONS
Examination of the corresponding assembly language can be a powerful tool in understanding the C language. The operation of creating local variables and of passing values back and forth between subroutines has been examined in detail.
By understanding the above mechanisms, it is relatively easy to create assembly language subroutines which can be called from within a C program.