Side-efforts of "volatile register coding conventions"

The declaration of certain processor registers as "volatile", and others as "non-volatile", has important side effects which must be accounted for if you plan to call "C" subroutines generated by the SDS compiler or subroutines that I have provided. These routines follow the "volatile" conventions. You will also experience the effect if you follow the "C-standard convention" to subroutines in your own code.

Note that, because of different processor characteristics (and different manufacturer's ideas) the actual implementation of the standard will differ between processors and assemblers. However, the underlying "convention" concepts will remain present.


An example of the register convention in use

Consider the following subroutine to generate a simple sum inside a loop on the 68K processor. The subroutine is to return the calculated sum

SUM:	MOVE.L #0, D0		; (sum variable)
	MOVE.L #0, D1		; (loop control variable)
LOOP:	CMP.L #6, D1		; while (D1 < 6){
	BEQ ENDLOOP 
	ADD.L D1, D0 		;	D0 = D0 + D1;  (or in "C" D0 += D1)
	ADD.L #1, D1 		;	D1 = D1 + 1;   (or in "C" D1++)
	BRA LOOP 
ENDLOOP: 			; }
	RTS			; return

However the following, almost equivalent, code involving a subroutine call to "DoSomething()" would most likely not execute correctly

SUM:	MOVE.L #0, D0		; (sum variable)
	MOVE.L #0, D1		; (loop control variable)
LOOP:	CMP.L #6, D1		; while (D1 < 6){
	BEQ ENDLOOP 
	ADD.L D1, D0 		;	D0 = D0 + D1;  (or in "C" D0 += D1)
	ADD.L #1, D1 		;	D1 = D1 + 1;   (or in "C" D1++)
	JSR DoSomething		;	DoSomething();
	BRA LOOP 
ENDLOOP:			; } 
	RTS			; return

The result of this code, most likely, would be

In the second code segment, the subroutine "DoSomething()" must make use of some of the processor's registers in order to function. To maximize speed of operation and ease of development of subroutines, the calling convention allows the subroutine "DoSomething()" (built in "C" or from some second person's code library) to use registers D0 and D1 without having to save them. Thus the "sum" (D0) and "loop" (D1) variable values wanted inside the loop may not exist after the subroutine call,.


Proper coding for the "volatile register" convention

With the "volatile register" convention, certain registers need not be stored before use within the subroutine. This avoids saving these registers to the slow external memory stack. This greatly improves the operation of the processor.

However, the convention also means that anything needed after the subroutine call by the calling routine can't be stored in a "volatile register". Thus we need to write the code for the calling routine as

SUM:	MOVE.L #0, D2		; (sum variable)
	MOVE.L #0, D3		; (loop control variable)
LOOP:	CMP.L #6, D3		; while (D3 < 6){
	BEQ ENDLOOP 
	ADD.L D3, D2  		;		D2 = D2  + D3; (or in "C" D2  += D3)
	ADD.L #1, D3 		;	D3 = D3 + 1;   (or in "C" D3++)
	JSR DoSomething		;	DoSomething();
	BRA LOOP 
ENDLOOP: 			; }
	RTS			; return			

The calling convention "requires" that the values in registers D2 and D3 will restored by the subroutine "DoSomething()" if they are modified in that subroutine. Thus the "sum" and "loop" variables will remain correct after the subroutine call.

The subroutine "DoSomething()" would be coded in one of the following formats

The instruction, or sequence of instructions, that store the registers onto the stack at the start of a subroutine is often called the PROLOGUE of the subroutine. The recovery of registers from the stack at the end of the routine is called the EPILOGUE.

However coding conventions do not work unless they are followed. Our new code segment for "Sum()" does not follow the convention that it expects "DoSomething()" to follow!


Conventions don't work unless you follow them

The code segment for "Sum()" is as follows

SUM:	MOVE.L #0, D2	; (sum variable)
	MOVE.L #0, D3	; (loop control variable)
LOOP:	CMP.L #6, D3	; while (D3 < 6){
	BEQ ENDLOOP 
	ADD.L D3, D2 	;	D2 = D2 + D3; (or in "C" D2 += D3)
	ADD.L #1, D3 	;	D3 = D3 + 1; (or in "C" D3++)
	JSR DoSomething	;	DoSomething();
	BRA LOOP 
ENDLOOP: 		; }
	RTS		; return

is a subroutine itself. The earlier routine that called "Sum()" may be using the registers D2 and D3 and expects the values to remain in those registers after the call to "Sum()". Thus the subroutine "Sum()" must also follow the convention that registers D2 and D3 are recoverable.

Even a "main()" code segment in "C" is actually a subroutine of the code that initialized the processor for operation. Remember that the prototype for "main()" in "C" is int main(long argc, char *argv[]). "main" must be a subroutine to something, otherwise, how can you pass it parameters and to "what" would it return a value?


Approaches to storing registers on the stack during the PROLOGUE and EPILOGUE

The correct approach is to set up a "stack frame" at the start of a subroutine. You will see this approach also being taken on the PowerPC code. In this stack frame, any necessary registers (and local variables) are stored for later recovery as is shown in the following code segment.

There are two common approaches to doing this. The choice between the two is somewhat "style" rather than science. The different approaches have their own advantages and disadvantages.

I prefer the following approach

I prefer the next approach

ASUB:	SUB.L #20, SP		; Set up stack frame
	MOVEM.L D2-D3, 12(SP)	; Save registers D2 (12(SP)) and D3 (16(SP))
	(There is room for parameters and variables on the stack
		at locations 0(SP), 4(SP), 8(SP))

	Code and subroutine calls here

	MOVEM.L 12(SP), D2-D3	; Recover registers D2 and D3
	ADD.L #20, SP		; Clean up the stack
	RETURN

Since we will not need to pass parameters between subroutines in Laboratory 3, we shall leave further discussion of this topic until later.


Special instructions for stack manipulation operation

The saving and recovery of registers to the stack occur so frequently that there are typically specialized instructions on most processors to help you perform these operations quickly, efficiently and reliably. The following show what is available on the 68K CISC processor

The RISC processors, by definition, have a reduced instruction set and therefore not as many "special instructions". Stack operations are done with groups of the basic RISC instructions. Then these instructions can be re-organized by an intelligent compiler or programmer for speed considerations.

However, if you have simple coding requirements, are not generating time critical code, then why bother using the fancier stack operations until the complexity of your code requires it (Laboratory 4). After all the WIDFI process states -- if your code ain't broke -- why fix it!. Use the simple stack operations suggested above for saving registers.

ASUB:	MOVEM.L D2-D3, -(SP)	; Store registers -- PROLOGUE
	Code and subroutine calls here
	MOVEM.L (SP)+, D2-D3	; Recover registers -- EPILOGUE
	RTS


Last modified: July 22, 1996 01:25 PM by M. Smith.
Copyright -- M. R. Smith