背景:
由于一些任务上需要用到crash tool 来分析系统的崩溃问题,要对内存进行推栈分析
ARM 寄存器
AArch64架构提供了31个通用寄存器,每个寄存器都可以用作64位X寄存器(X0~X30)或32位W寄存器(W0~W30)。对于数据处理指令,X或W的选择决定了操作的size。使用X寄存器将导致64位的计算,使用W寄存器将导致32位的计算。当写入W寄存器时, 64位寄存器的高32位为零。
X0: 也称为零寄存器,用于存储函数的返回值、传递函数参数和临时存储变量。
X1-X7: 用于传递函数参数和临时存储变量。
X8: 也称为程序计数器(PC),用于存储当前正在执行的指令的地址。当处理器执行一条指令时,PC会自动递增以指向下一条指令。
X9-X15: Caller Saved寄存器,用于存储函数参数、局部变量和临时数据。
X16-X17: 也称为临时寄存器,用于存储临时数据,这些寄存器在函数调用期间不需要保留其值。
X18: 也称为平台相关寄存器,用于存储与特定平台相关的信息,如TLS(线程本地存储)指针。
X19-X28: Callee Saved寄存器,于存储函数参数、局部变量和临时数据。
X29: 也称为帧指针寄存器(Frame Pointer,FP),用于存储当前函数的堆栈帧指针。当函数调用发生时,x29寄存器的值被保存到堆栈中,以便在函数执行期间可以轻松地访问上一级函数的堆栈帧。这样,当函数返回时,可以通过恢复x29寄存器的值来恢复到正确的堆栈帧。
X30: 也称为链接寄存器(Link Register,LR),用于存储函数调用的返回地址。当函数执行完毕时,处理器将使用x30寄存器中存储的返回地址来恢复到调用点。这样,控制流程可以顺利返回到调用函数的位置继续执行。
X29寄存器和X30寄存器在函数调用和堆栈帧的管理中扮演着关键角色。X29寄存器用于存储当前函数的堆栈帧指针,以便在函数执行期间可以访问上一级函数的局部数据。X30寄存器则用于存储函数调用的返回地址,以便在函数执行完毕后可以返回到正确的调用点。理解和正确使用这两个寄存器对于编写正确和高效的函数代码至关重要。
X29 与 X30 的理解:
函数开始执行都需要保存fp指针到栈空间,这里的被保存fp指针是上一个函数的数据(一般是栈底数据),然后立即将本函数的栈底数据保存到fp指针, 函数调用结束,根据fp的数据恢复sp指针,从而释放结束函数的栈空间。
函数调用需要入栈的数据只有前一个函数的fp指针,然后跳转的时候会将跳转指令bl的下一条指令地址保存到 lr寄存器,方便函数调用结束后返回执行下一条指令。当然如果函数入参很多,寄存器不够用,这些参数同样需要入栈。
实例分析
纸上谈来终觉浅,觉知此事要躬行,在艰难的理解之后,决定以实际例子入手,结合图示来描述自己对于程序运行的一些理解。
C 代码
int m=0;
int funa(int a, int b)
{
int ret = 0 ;
ret = a+b;
return ret;
}
int funb(int c, int d)
{
int ret = c+d ;
ret = funa(c, ret);
return ret;
}
int main(void)
{
int i=1,j=2, r;
m=6;
r = funb(i,j);
return r;
}
我们可以借助在线汇编网址来将源代码转换,可以看清楚代码之间的对应关系
m:
.zero 4
funa:
sub sp, sp, #32
str w0, [sp, 12]
str w1, [sp, 8]
str wzr, [sp, 28]
ldr w1, [sp, 12]
ldr w0, [sp, 8]
add w0, w1, w0
str w0, [sp, 28]
ldr w0, [sp, 28]
add sp, sp, 32
ret
funb:
stp x29, x30, [sp, -48]!
mov x29, sp
str w0, [sp, 28]
str w1, [sp, 24]
ldr w1, [sp, 28]
ldr w0, [sp, 24]
add w0, w1, w0
str w0, [sp, 44]
ldr w1, [sp, 44]
ldr w0, [sp, 28]
bl funa
str w0, [sp, 44]
ldr w0, [sp, 44]
ldp x29, x30, [sp], 48
ret
main:
stp x29, x30, [sp, -32]!
mov x29, sp
mov w0, 1
str w0, [sp, 28]
mov w0, 2
str w0, [sp, 24]
adrp x0, m
add x0, x0, :lo12:m
mov w1, 6
str w1, [x0]
ldr w1, [sp, 24]
ldr w0, [sp, 28]
bl funb
str w0, [sp, 20]
ldr w0, [sp, 20]
ldp x29, x30, [sp], 32
ret
初看起来好混乱,当整个问题看起来复杂的时候,我们需要代码进行拆解,结合图示理解里面的思想。
首先来拆解fun a 的汇编指令
funa:
sub sp, sp, #32
str w0, [sp, 12]
str w1, [sp, 8]
str wzr, [sp, 28]
ldr w1, [sp, 12]
ldr w0, [sp, 8]
add w0, w1, w0
str w0, [sp, 28]
ldr w0, [sp, 28]
add sp, sp, 32
ret
int funa(int a, int b)
{
int ret = 0 ;
ret = a+b;
return ret;
}
workflow:
-
在程序运行之前,要给予函数局部变量和其他重要参数存储的内存空间,
sub sp,sp # 32 即 sp = sp -32 sp是栈指针,栈增长是高地址向低地址方向,更新sp
-
接下来我们应该存储相应的变量数据了
str w0, [sp, 12] 寄存器w0的值存储在sp + 12 的地址上 str w1, [sp, 8] 寄存器w0的值存储在sp + 8 的地址上
-
存在栈上,目前使用他进行其他计算,所以把这两个变量拿出来
ldr w1, [sp, 12] ldr (load register) 拿出数据 ldr w0, [sp, 8]
-
进行计算
add w0, w1, w0 # 即w0 = w1 + w0
-
将新变量的值 压入栈帧,然后将变量拿出。
str w0, [sp, 28] ldr w0, [sp, 28]
-
最后我们得到计算结果之后,需要释放栈空间 注:Linux:默认栈大小可能是8 MB,使用
ulimit -s
可以查看和更改这个值,且空间远小于堆。add sp, sp, 32 # 栈指针恢复 释放栈空间
-
ok 终于到最后一个指令,它里面涉及的东西稍微有点烦
ret # ret 指令会将控制权返回给调用该函数的代码。ret 指令从栈中弹出返回地址,并跳转到这个地址继续执行程序。 这里涉及到上面的链接寄存器LR(X30),以及栈帧寄存器FP(X29),保存当前函数栈起始位置,也就是让系统记得从哪开始申请空间的。
函数的栈结构为:
- 栈顶保存的是自己的FP(栈底)
- 栈顶+8 处保存的是LR寄存器的值,也就是自己return后要从哪里开始执行。
- 然后保存的是局部变量的值。
- 然后,保存的是从n,…,3,2,1 ,从低往高,的传参的值。
- 到上一个函数的FP为止,函数的栈结束
w0 w1 新w0 X30 X29 <-----
X29 X30的关系图
根据代码 stp x29, x30, [sp, -32]!,我们将X29 X30 的值进行存储,在main 函数中:
X29
保存main
函数的栈帧指针,指向main
函数栈帧的起始位置。X30
保存main
函数返回后的地址,通常用于在main
函数结束后回到程序的启动代码或操作系统执行清理和结束工作。
也就是最先开始存储的X29的地址是该函数的起始地址,便于释放内存、