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.
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,.
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
DoSomething: code using registers D0 and/or D1 No memory operations necessary RTS
DoSomething: MOVE.L D2, -(SP) ; Save "old" value of register D2 that ; "might" be in use by earlier routine code using registers D0 and D1 and D2 MOVE.L (SP)+, D2 ; Recover "old" value of register D2 RTS
DoSomething: MOVEM.L D2-D4, -(SP) ; Save "old" values of registers that ; "might" be in use by earlier routine code using registers D0, D1, D2, D3 and D4 MOVEM.L (SP)+, D2-D4 ; Recover "old" values of registers RTS
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!
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?
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
ASUB: MOVEM.L D2-D3, -(SP) ; Store registers -- PROLOGUE
Code and subroutine calls here
MOVEM.L (SP)+, D2-D3 ; Recover registers -- EPILOGUE RTS
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.
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