跳转和返回总流程
从第一个函数a开始
当他需要进子函数b时,首先需要跳转
使用bl 汇编跳转
bl用于子程序跳转,要返回地址,返回地址存于LR中。当发生bl跳转前,会在寄存器 R14 (即LR)中保存当前PC-4,即bl跳转指令的下一条指令的地址。所以在返回时只要 MOV pc,lr 。
接下来进子函数b
入栈 push {r3-r5,lr}
等从子函数b出来时
出栈 pop {r3-r5,pc}
压lr出pc
为什么进子函数压lr,出子函数出栈到pc呢
因为上面提到。进子函数时执行Bl跳转,此时会在寄存器 R14 (即LR)中保存当前PC-4,即bl跳转指令的下一条指令的地址。所以在返回时只要 MOV pc,lr 。
就是说bl指令一起做了lr值更新的动作。
Lr的值的变化
在主函数a时,lr的值是a的返回地址。且lr的值在进a时已入栈。
此时要进子函数b,执行bl,此时,lr的值被更新为pc-4,变为了子函数b的返回地址.
此时函数a的返回地址,得靠出栈才能找到了。
接着进子函数b,一开始就入栈 push {r3-r5,lr}
此时在子函数b,lr的值是子函数b的返回地址,栈里也有子函数b的返回地址lr
等执行完子函数b要返回a时,
出栈 pop {r3-r5,pc}
将进函数时的lr给到pc,一执行pc,就等于跳转回函数a,接着刚才执行a的位置继续往下执行了。
Pc-4和不-4
函数调用
bl 跳转时,会在寄存器 LR(R14)中保存 pc-4 值,即 bl 跳转指令的下一条指令地址,所以返回时只要 MOV pc,lr。
这里pc-4是因为3级流水线。
也可以是入栈 push lr 出栈 pop pc。
就是说,首先3级流水线
从图中可以看出,一条汇编指令的运行有三个步骤,取指、译码、执行,当第一条汇编指令取指完成后,紧接着就是第二条指令的取指,然后第三条…如此嵌套
所以第一条指令开始执行时,译码是+4,PC值取码已经加了8
所以必须记住这个前提,在arm中,每次该指令执行时,其实这时的PC值是PC=PC+8
举例:
当执行的指令是1,译码是指令2,取码是当前pc是指令3。
当发生函数跳转bl时,最终从子函数返回时,我们希望继续执行的是指令2.
那指令2和pc指令3是什么关系?
Code是从小地址向大地址执行的,所以如果指令1的地址是0000,指令2地址是0004,指令3地址是0008.
所以pc-4=指令2.
所以bl设计为进子函数前执行Bl跳转,此时会在寄存器 R14 (即LR)中保存当前PC-4,此时已经-4了。
所有从子函数返回时,直接用lr的值给pc就可以。
进出中断
函数的跳转,是可预期的,在跳转前,把pc-4给lr就是子函数的返回地址。
中断和异常,都是不可预期的,在跳转前,没有pc-4就给了lr,所以中断和异常要在从中断返回函数时,lr-4给pc,跳到pc的地址
中断会返回时硬件lr-4,异常需要用户自己lr-4.
Thumb 和 arm指令及奇偶
后续提到cmbacktrace时,会出现通过% 2 != 0 来筛选栈里的各个pc
原因提到:
//又由于Cortex-M使用 thumb 指令,因此pc必须是奇数
这是怎么解释呢?
CM3 中的指令至少是半字(2字节)对齐的,所以 PC 的 LSB 读回 的总是0
无论是直接写 PC 的值还是使用分支指令,都必须保证加载到 PC 的数值是奇数(即 LSB=1),用以表明这是在 Thumb 状态下执行。倘若写了 0,则视为企图转入 ARM 模式,CM3 将产生一个 fault 异常。