ARM is unusual among the processors by having the program counter available as a “general purpose” register. Most other processors have the program counter hidden, and its value will only be disclosed as the return address when calling a function. If you want to modify it, a jumping instruction is used.

For example, on the x86, the program counter is called the instruction pointer, and is stored in eip, which is not an accessible register. After a function call, eip is pushed onto the stack, at which point it could be examined. Return is done through the ret instruction which pops the return address off the stack, and jumps there.

Another example: on the MIPS, the program counter is stored into register 31 after executing a JALR instruction, which is used for function calling. The value in there can be examined, and a return is a register jump JR to that register.

ARM’s unusual design allows many, many ways of returning from functions. But first, we must understand how function calls work on the ARM.

On ARM, the program counter is register 15, or r15, also called pc. The instruction to call a function is bl (for immediate offsets) or blx (for addresses in registers). These instructions stores the return address in r14, called the link register, or lr. To return, we must put this value back into pc.

Method 1

When writing non-leaf functions, i.e. functions that calls other functions, the value of lr must be preserved, since calling another function will overwrite it. The most common way is to store it on the stack. On the ARM, there are convenient instructions push and pop which pushes and pops multiple values onto the stack. We typically use this to preserve the registers we modify. For example, if we want to preserve r3, r4, and lr, we can write push {r3, r4, lr}.

A normal function will look like:

push        {r3, r4, lr}  ; Save registers.
; Function body.
pop         {r3, r4, pc}  ; Restore registers and return.

This is our first way of returning: using push to restore all the registers, except putting what was lr when we are doing push into pc. This will overwrite pc with the return address, achieving the return. Note that we could instead use r14 instead of lr and r15 instead of pc, but this is less clear on the intent.

Method 2

We can use an unconditional jump to register to return, which is useful in leaf functions where lr is never stored on the stack. This is simply:

bx          lr

This jumps to the address in lr, setting pc to lr, and completing the return.

Method 3

Similar in rationale to method 2, but as stated in the beginning, ARM lets you manipulate the program counter as you would any other register. So… we have:

mov         pc, lr

This copies lr into pc, also completing the return.

Method 4

To really get the point across, we can also use bitwise instructions to return. For example:

orr         r15, r14, r14

This performs a bitwise OR of r14 (lr) with itself, which results in the value of lr, and stores this in r15 (pc). This also copies lr into pc, completing the return.

Method n

Of course, there are many other ways of copying the value in one register into another, and to list that would be fairly silly. But as long as lr at the beginning of the function call is placed into pc, a return is completed.

But please, use the most sensible ways to return. This means you should prefer the first two, depending on whether the function is a leaf. As a distant third, use method 3 (mov pc, lr).