首先ARM有许多寄存器(这里说的ARM 32位,指的是arm cortex A系列),如下图:
![32位ARM寄存器和栈使用浅析-LMLPHP 32位ARM寄存器和栈使用浅析-LMLPHP](https://c1.lmlphp.com/user/master/2020/10/08/son_1/c4c4f4dfe094b6b5274da50ffb344164.jpg)
从图中可以看出(图选自于《cortex_A_series_PG.pdf》),32位arm的R0 - R7为低端寄存器(Thumb16模式下,只能使用R0-R7,R13,R14,R15这几个寄存器),这在所有ARM的工作模式下是共享的(ARM有7种工作模式:Usr, Sys, FIQ, IRQ, ABT, SVC, UND,其中MON和HYP我们暂时不计入考虑,大部分操作系统工作于SVC模式下,应用工作在Usr模式下面)。下面我们来看看各个寄存器的作用。
R0寄存器: 通常用于函数传参(参数1)或者普通寄存器或者函数返回值。
R1寄存器: 通常用于函数传参(参数2)或者普通寄存器。
R2寄存器: 通常用于函数传参(参数3)或者普通寄存器。
R3寄存器: 通常用于函数传参(参数4)或者普通寄存器。
R7寄存器: 系统调用时,存放系统调用号,有时也用于作为FP使用。FP又叫frame pointer即栈基指针,主要在函数中保存当前函数的栈起始位置,用于堆栈回溯。
R13寄存器:R13又名SP,即栈指针寄存器,主要用于指向当前程序栈顶,配合指令pop/push等。栈主要用于存放局部变量,保存函数间调用的关键寄存器,如LR。SP可以是向上增长也可以是向下增长,通常我们ARM采用的向下增长模式,下图为一个函数调用时,栈的排布情况。
![32位ARM寄存器和栈使用浅析-LMLPHP 32位ARM寄存器和栈使用浅析-LMLPHP](https://c1.lmlphp.com/user/master/2020/10/08/son_1/afbfc4f91f17eeb1f91e11ecf74efe44.png)
R15寄存器:R15又名PC,即程序寄存器,主要用于存放CPU取指的地址,记住是取指地址,不是当前运行地址。目前,ARM是三级流水线,因此,当CPU在执行S指令的时候,PC指向的是S+2指令。但是当手动向PC赋值,则是让CPU跳转到赋入的值 所代表的地址去运行。
注:通常PC指针指向的地址都是4字节对齐,即地址的[1:0]位总是为0,这也是我们说的ARM模式。现在很多CPU都支持混合编码即同时支持ARM指令和Thumb指令,因此为了区分Thumb指令,ARM将[0]位设置成1,即地址最低位如果是1,表示当前指令是Thumb指令,否则为ARM指令。
Thumb(16)指令占用的空间通常比ARM指令少,但是ARM指令运行的效率通常要比Thumb更高,Thumb模式到ARM模式可以通过带X的跳转进行切换,如BLX, BX跳转指令(Thumb分为Thumb16和Thumb32)。
上面说了寄存器的情况,下面说说在不同模式下,程序返回时,CPU应该运行的地址:
点击(此处)折叠或打开
- P1 P2 P3
- | | |
- v v v
- CPU执行 CPU译码 CPU取指(PC)
1、函数调用 返回
点击(此处)折叠或打开
- MOV R0, #0
- BL test
- MOV R1,R0
2、系统调用和未定义指令异常返回
点击(此处)折叠或打开
- S1
- S2
- S3
- S4
3、FIQ和IRQ异常返回
点击(此处)折叠或打开
- S1
- S2
- S3
- S4
4、取指异常返回
点击(此处)折叠或打开
- S1
- S2
- S3
- S4
5、数据访问异常返回
点击(此处)折叠或打开
- S1
- S2
- S3
- S4
上述说完了LR和PC关系和取值,下面继续说关于FP的堆栈回溯功能, 如图:
点击(此处)折叠或打开
- static int b(void)
- {
- return 0;
- }
- static int a(void)
- {
- b();
- return 0;
- }
- static int t(void)
- {
- a();
- return 0;
- }
1、通过读取寄存器R13和R7我们可以得到R13 = 0x0800(栈顶), R7 = 0x1000。
2、通过栈的布局情况我们了解到,FP + 4为上一个函数的返回地址LR。在b崩溃的时候,LR的值是*0X1004 = 0x300,即这个0x300就是a函数调用b函数的时候的下一条指令地址。
3、通过栈的布局情况我们了解到,fp则存放的是上一个函数fp的地址。因此,我们可以拿到a函数的fp地址:*0x1000 = 0x2000。
4、重复2 3 步骤,我们可以获取到t函数调用a函数的时候的下一条指令地址:0x500。
因此,崩溃时候的调用栈是:
点击(此处)折叠或打开
- b函数
- 0x300 (a函数)
- 0x500(t函数)
不管函数调用多少层,都可以用同样方法,一直找到最上层调用者。堆栈回溯的前提是:编译的时候 不能禁用FP功能(gcc编译的时候不要添加-fomit-frame-pointer参数,否则堆栈回溯会有问题)。
我们从上面已经知道了寄存器的常用方法,下面我们通过一段hello程序来进一步说明,程序源码如下:
点击(此处)折叠或打开
- #include <stdio.h>
- static int main_test(int number)
- {
- int i = 1000;
- for (;i > 0; i--)
- number += i % 10;
- return number;
- }
- int main (int argc, char *argv[])
- {
- printf ("Hello World\n");
- return 0;
- }
点击(此处)折叠或打开
- Disassembly of section .plt:
- 82c0: e28fc600 add ip, pc, #0, 12
- 82c4: e28cca08 add ip, ip, #8, 20 ; 0x8000
- 82c8: e5bcf324 ldr pc, [ip, #804]! ; 0x324
- 82cc: e28fc600 add ip, pc, #0, 12
- 82d0: e28cca08 add ip, ip, #8, 20 ; 0x8000
- 82d4: e5bcf31c ldr pc, [ip, #796]! ; 0x31c
- 82d8: 4778 bx pc
- 82da: 46c0 nop ; (mov r8, r8)
- 82dc: e28fc600 add ip, pc, #0, 12
- 82e0: e28cca08 add ip, ip, #8, 20 ; 0x8000
- 82e4: e5bcf310 ldr pc, [ip, #784]! ; 0x310
- 82e8: e28fc600 add ip, pc, #0, 12
- 82ec: e28cca08 add ip, ip, #8, 20 ; 0x8000
- 82f0: e5bcf308 ldr pc, [ip, #776]! ; 0x308
- 000083cc <main_test>:
- 83cc: e52db004 push {fp} ; (str fp, [sp, #-4]!)
- 83d0: e28db000 add fp, sp, #0
- 83d4: e24dd014 sub sp, sp, #20
- .............
- 8434: e3530000 cmp r3, #0
- 8438: caffffea bgt 83e8 <main_test+0x1c>
- 843c: e51b3010 ldr r3, [fp, #-16]
- 8440: e1a00003 mov r0, r3 # 返回值赋值给r0
- 8444: e28bd000 add sp, fp, #0
- 8448: e8bd0800 ldmfd {fp}
- 844c: e12fff1e bx lr # 返回main函数
- 00008450 <main>:
- 8450: e92d4800 push {fp, lr} # 将lr和fp压栈
- 8454: e28db004 add fp, sp, #4 # 设置新的fp地址(这里的fp位置可能和上面描述有所不同,但原理一样)
- 8458: e24dd008 sub sp, sp, #8 # 开辟局部变量地址空间
- 845c: e50b0008 str r0, [fp, #-8] # 给局部变量赋值 处置,这里fp -8 是argc
- 8460: e50b100c str r1, [fp, #-12] # 给局部变量赋值 处置,这里fp -12 是argv
- 8464: e30804d4 movw r0, #34004 ; 0x84d4 # 初始化函数的第一个传参,r0
- 8468: e3400000 movt r0, #0
- 846c: ebffff93 bl 82c0 <_init+0x20> # 这里利用0x82c0 间接跳转到main_test函数
- 8470: e3a03000 mov r3, #0
- 8474: e1a00003 mov r0, r3 # 将返回值 赋值给r0,相当于return 0;
- 8478: e24bd004 sub sp, fp, #4 # 还原栈
- 847c: e8bd8800 pop {fp, pc} # 弹出 上一个函数的fp和lr返回地址到pc