Review

  • A non-leaf function contains/uses a bl instruction that overwrites the lr register:

    NonLeafFunc:
               // lr has the return address
               ...
               ...
               bl   anotherFunc  // Will update the lr register
               ...
               ...
    
               mov   pc, lr      // lr has an incorrect return address

  • The non-leaf function must save its return address (in the lr register) in a stack

  • ARM instructions to push and pop on the System Stack:

       push {rN} // Pushes the value in reg rN on system stack
    
       pop  {rN} // Pops the value at top of the system stack into reg rN
    

We now apply these knowledge to implement the flow control of a non-leaf subroutine

Implementing a non-leaf method/function using the program stack
 

We start with the implementation of the leaf subroutine:

    leafMethod:
                                 // reg lr = return addr


		....
                ....
		.... (instructions in the method)     


     /* -------------------------------------------
        End of method, we need to return to caller
        ------------------------------------------- */   
     
		mov   pc, lr
   

We first add a function call to make it a non-leaf function....

Implementing a non-leaf method/function using the program stack
 

The bl someFunction instruction will overwrite the return address in the lr register

    nonLeafMethod:
                                 // reg lr = return addr  --- overwritten

		....
                bl    someFunction  // overwrites LR
                ....
		.... (instructions in the method)     


     /* -------------------------------------------
        End of method, we need to return to caller
        ------------------------------------------- */   
                // LR contains incorrect return address
		mov   pc, lr
   

Recall the solution:   the function must save the return address in LR on the runtime stack

Implementing a non-leaf method/function using the program stack
 

We save the return address in register LR on the stack as soon as possible:

    nonLeafMethod:
                push  {lr}   // Save return address on stack

                ....
                bl    someFunction  // overwrites LR
                ....
		.... (instructions in the method)     


     /* -------------------------------------------
        End of method, we need to return to caller
        ------------------------------------------- */   
                // LR contains incorrect return address
		mov   pc, lr   <---- use an incorrect return address
   

Observe that:   LR contains an incorrect return address when the function returns !

Implementing a non-leaf method/function using the program stack
 

We must first restore the return address from the stack with pop {lr}:

    nonLeafMethod:
                push  {lr}   // Save return address on stack

                ....
                bl    someFunction  // overwrites LR
                ....
		.... (instructions in the method)     


     /* -------------------------------------------
        End of method, we need to return to caller
        ------------------------------------------- */ 
                pop   {lr}   // Restore the correct return address 
		mov   pc, lr
   

This is the structure of a non-leaf function/method !!!

A more efficient way to return from a non-leaf function
 

Consider the structure of a non-leaf function:

    nonLeafMethod:
                push  {lr}   // Save return address on stack

                ....
                bl    someFunction  // overwrites LR
                ....
		.... (instructions in the method)     


     /* -------------------------------------------
        End of method, we need to return to caller
        ------------------------------------------- */ 
                pop   {lr}   // Restore the correct return address 
		mov   pc, lr
   

Question:   can you replace these 2 assembler instructions with one instruction ???

A more efficient way to return from a non-leaf function
 

We pop the return address directly into the program counter (PC):

    nonLeafMethod:
                push  {lr}   // Save return address on stack

                ....
                bl    someFunction  // overwrites LR
                ....
		.... (instructions in the method)     


     /* -------------------------------------------
        End of method, we need to return to caller
        ------------------------------------------- */ 
                pop   {pc}   // Return to caller function 
		mov   pc, lr
   

 

Example implementing non-leaf subroutine in assembler code
 

Example: (expressed in Java)

   public static void main(String[] args)
   {
      A( );
   }

   public static void A( )
   {
      ...
      B( );   // Calls B( )
      ...
   }

   public static void B( )
   {
      ...     // Does not call any method/function     
      ...
   }
   

Example implementing non-leaf subroutine in assembler code

How to implement the call sequence in ARM assembler:

        mov     r0, #1111
        mov     r1, #1111
        bl      A
        mov     r2, #2222
        mov     r3, #2222
        bl      A
        mov     r4, #3333
        mov     r5, #3333

Stop:

A:
        push    {lr}   // Save return address

        mov     r0, #7777
        mov     r1, #7777
        bl      B      // Overwrites lr, it's OK, we saved it !!!
        mov     r2, #8888
        mov     r3, #8888

        pop     {pc}   // A returns to main !!!


B:
        mov     r0, #9999
        mov     r1, #9999

        mov     pc, lr  // B can return to A
   

DEMO:   /home/cs255001/demo/asm/8-sub/bl+pop.s

The reason why we use a stack to store return addresses

Consider a deeper nesting of function calls:

main:  // main calls A, A calls B, B calls C, C calls D
        mov     r0, #1111
        bl      A
        mov     r0, #2222
        bl      A
        mov     r0, #3333
Stop:
	nop

A:
        push    {lr}   // Save return address
        mov     r0, #4444
        bl      B      // Overwrites lr, it's OK, we saved it !!!
        mov     r0, #5555
        pop     {pc}   // A returns to main !!!

B:
        push    {lr}   // Save return address
        mov     r0, #6666
        bl      C      // Overwrites lr, it's OK, we saved it !!!
        mov     r0, #7777
        pop     {pc}   // B returns to A !!!

C:
        push    {lr}   // Save return address
        mov     r0, #8888
        bl      D      // Overwrites lr, it's OK, we saved it !!!
        mov     r0, #9999
        pop     {pc}   // C returns to B !!!

D:
        mov     r0, #1234
        mov     pc, lr  // D returns to C
   

DEMO:   /home/cs255001/demo/asm/8-sub/bl+pop2.s

The reason why we use a stack to store return addresses

Situation: main( ) starting....

Content of the runtime stack:

The reason why we use a stack to store return addresses

Situation: main( ) called A( ) and A( ) has saved its return address:

Content of the runtime stack: contain the return address to main

The reason why we use a stack to store return addresses

Situation: A( ) called B( ) and B( ) has saved its return address:

Content of the runtime stack: contain the return address to A and to main

The reason why we use a stack to store return addresses

Situation: B( ) called C( ) and C( ) has saved its return address:

Content of the runtime stack: contain the return address to B, to A and to main

The reason why we use a stack to store return addresses

Situation: right before the function C( ) returns (to B( )):

Content of the runtime stack: contain the return address to B, to A and to main

The reason why we use a stack to store return addresses

Situation: right after the function C( ) has returned to B( ):

Content of the runtime stack: contain the return address to main and to A

The reason why we use a stack to store return addresses

Situation: right before the function B( ) returns (to A( )):

Content of the runtime stack: contain the return address to main and to A

The reason why we use a stack to store return addresses

Situation: right after the function B( ) has returned to A( ):

Content of the runtime stack: contain the return address to main

The reason why we use a stack to store return addresses

Situation: right before the function A( ) returns (to main( )):

Content of the runtime stack: contain the return address to main

The reason why we use a stack to store return addresses

Situation: right after the function A( ) has returned to main( ):

Content of the runtime stack: empty !!!

The reason why we use a stack to store return addresses
 

  • The insertion ordering and deletion ordering of the return addresses match perfectly with the call + return ordering of the functions

  • The stack's push and pop operations are perfectly suitable to maintain the return addresses for storage purpose in the implementation of the function call + return mechanism !!!