Search code examples
assemblyarmcpu-architecturecortex-minstruction-set

Why register list of PUSH must not include PC?


PUSH {R0, PC} ; A1477E: This register combination results in UNPREDICTABLE behaviour

Why does it cause unpredictable behaviour? Why is LR allowed in register list but not PC? I think implementing recursive functions will be much easier using LR in stack, but when we can still write recursive functions using PC why is it not included in valid list of registers?


Solution

  • Try these instructions

    PUSH {R0, R7}
    PUSH {R0, R8}
    

    and disassemble, do you get this:

    0:  e92d0081    push    {r0, r7}
    4:  e92d0101    push    {r0, r8}
    

    or this:

    0:  b481        push    {r0, r7}
    2:  e92d 0101   stmdb   sp!, {r0, r8}
    

    or an error that the register list is in valid (r8 is not valid)?

    If you get the first set, then you are building for ARM not thumb and the warning/error message makes more sense (for r15). If you are building for thumb it should say invalid (for r15 and/or r8).

    Early ARM docs (for ARM) stated

    If R15 is specified in the value stored is IMPLEMENTATION DEFINED

    Now they say:

    The SP and PC can be in the list in ARM instructions, but not in Thumb instructions. However:

    • ARM deprecates the use of ARM instructions that include the PC in the list

    Reading more (which you should have read yourself by now)

    str pc, [sp, #-4]!
    
    0:  e52df004    push    {pc}        ; (str pc, [sp, #-4]!)
    

    Also is shown in the armv7-a as deprecated and the older as implementation defined with some additional comments from back then:

    Reading the program counter

    When an instruction reads R15 without breaking any of the restrictions on its use, the value read is the address of the instruction plus 8 bytes. As ARM instructions are always word-aligned, bits[1:0] of the resulting value are always zero. (In T variants of the architecture, this behavior changes during Thumb state execution - see Chapter A6 The Thumb Instruction Set for details.) This way of reading the PC is primarily used for quick, position-independent addressing of nearby instructions and data, including position-independent branching within a program.

    An exception to the above rule occurs when an STR or STM instruction stores R15. Such instructions can store either the address of the instruction plus 8 bytes, like other instructions that read R15, or the instruction's own address plus 12 bytes. Whether the offset of 8 or the offset of 12 is used is IMPLEMENTATION DEFINED . An implementation must use the same offset for all STR and STM instructions that store R15. It cannot use 8 for some of them and 12 for others.

    Because of this exception, it is usually best to avoid the use of STR and STM instructions that store R15. If this is difficult, use a suitable instruction sequence in the program to ascertain which offset the implementation uses.

    You should have noticed in the ARMv7-M that it also uses a 16 bit register mask, but the r15 and r13 locations are marked as (0).

    Now if you go in and enter the machine code 0xe92d8101 (thumb2) you will see at least the gnu disassembler show

    0: e92d 8101 stmdb sp!, {r0, r8, pc}

    But I would have to dig out a cortex-m3 or few to see how they behave.

    Keil may simply have taken the ARM warning that existed before and applied it to the thumb encoding (or recycled a warning rather than creating a new one), there is a location in the mask for the program counter, you just should not use it and/or should not make assumptions about its use. instead push some stuff, use one of the things you pushed to get a copy of the pc at that point then push that, that will be predictable and whatever value you think you are serving by pushing the pc, you can have.