[crash分析1]C语言在ARM中函数调用时,栈是如何变化的?

[crash分析1]C语言在ARM中函数调用时,栈是如何变化的?


为什么会写篇栈变化的文章?做系统分析的话你肯定遇到过一些crash, oops等棘手问题,一般大家都会用 gdb, objdump 或者 addr2line等工具分析 pc 位置来定位出错的地方。但是这些分析工具背后的本质原理就不见得理解深刻了,而且有的时候面对一系列 backtrace 或者 stack 日志处于懵逼的状态。

今天和大家一起看下面对 crash 日志的时候,如何利用 stack 来分析其变化的来龙去脉。

1.ARM体系结构通用寄存器

  1. r0-r3 用作传入函数参数,传出函数返回值。在子程序调用之间,可以将 r0-r3 用于任何用途。被调用函数在返回之前不必恢复 r0-r3。—如果调用函数需要再次使用 r0-r3 的内容,则它必须保留这些内容。
  2. r4-r11 被用来存放函数的局部变量。如果被调用函数使用了这些寄存器,它在返回之前必须恢复这些寄存器的值。r11 是栈帧指针 fp。
  3. r12 是内部调用暂时寄存器 ip。它在过程链接胶合代码(例如,交互操作胶合代码)中用于此角色。在过程调用之间,可以将它用于任何用途。被调用函数在返回之前不必恢复 r12。
  4. 寄存器 r13 是栈指针 sp。它不能用于任何其它用途。sp 中存放的值在退出被调用函数时必须与进入时的值相同。
  5. 寄存器 r14 是链接寄存器 lr。如果您保存了返回地址,则可以在调用之间将 r14 用于其它用途,程序返回时要恢复
  6. 寄存器 r15 是程序计数器 pc。它不能用于任何其它用途。

在这里插入图片描述
在这里插入图片描述

2. 演示

假如现在你已经掌握了 arm 指令的用法,即便没有掌握也没关系,“书到用时回头翻”。这里以一段简单的 c 语言为例:


#include <stdio.h>

int m = 8;
int fun(int a,int b)
{
    int c = 0;
    c = a + b;
    return c;
}
int main()
{
    int i = 4;
    int j = 5;
    m = fun(i, j);
    return 0;
}

编译一下,然后反汇编:
$ arm-linux-gnueabi-gcc main.c -o main
$ arm-linux-gnueabi-objdump -D main


00010400 <fun>:
   10400:       e52db004        push    {fp}            ; (str fp, [sp, #-4]!)
   10404:       e28db000        add     fp, sp, #0
   10408:       e24dd014        sub     sp, sp, #20
   1040c:       e50b0010        str     r0, [fp, #-16]
   10410:       e50b1014        str     r1, [fp, #-20]  ; 0xffffffec
   10414:       e3a03000        mov     r3, #0
   10418:       e50b3008        str     r3, [fp, #-8]
   1041c:       e51b2010        ldr     r2, [fp, #-16]
   10420:       e51b3014        ldr     r3, [fp, #-20]  ; 0xffffffec
   10424:       e0823003        add     r3, r2, r3
   10428:       e50b3008        str     r3, [fp, #-8]
   1042c:       e51b3008        ldr     r3, [fp, #-8]
   10430:       e1a00003        mov     r0, r3
   10434:       e24bd000        sub     sp, fp, #0
   10438:       e49db004        pop     {fp}            ; (ldr fp, [sp], #4)
   1043c:       e12fff1e        bx      lr

00010440 <main>:
   10440:       e92d4800        push    {fp, lr}
   10444:       e28db004        add     fp, sp, #4
   10448:       e24dd008        sub     sp, sp, #8
   1044c:       e3a03004        mov     r3, #4
   10450:       e50b300c        str     r3, [fp, #-12]
   10454:       e3a03005        mov     r3, #5
   10458:       e50b3008        str     r3, [fp, #-8]
   1045c:       e51b1008        ldr     r1, [fp, #-8]
   10460:       e51b000c        ldr     r0, [fp, #-12]
   10464:       ebffffe5        bl      10400 <fun>
   10468:       e1a02000        mov     r2, r0
   1046c:       e59f3010        ldr     r3, [pc, #16]   ; 10484 <main+0x44>
   10470:       e5832000        str     r2, [r3]
   10474:       e3a03000        mov     r3, #0
   10478:       e1a00003        mov     r0, r3
   1047c:       e24bd004        sub     sp, fp, #4
   10480:       e8bd8800        pop     {fp, pc}
   10484:       00021024        andeq   r1, r2, r4, lsr #32

3.图解栈的变化过程

如何能让读者接受吸收的更快,我一直觉得按照学习效率来讲的话顺序应该是视频,图文,文字。反正我是比较喜欢视频类的教学。这里给大家画下栈变化的过程是什么样子的。这里的图是结合上面的代码来画的,希望有助于读者的理解。

1.程序在内存分布区域

在这里插入图片描述
2.全局变量m赋值

在这里插入图片描述
3.保存进入main之前的栈底, fp-sp之间是当前函数栈

在这里插入图片描述
4.函数main的栈已经准备好了
在这里插入图片描述
5.i入栈

在这里插入图片描述
6.j入栈

在这里插入图片描述
7.准备函数fun的调用, 形参反向入栈 先形参b入栈

在这里插入图片描述
8.形参a入栈
在这里插入图片描述
9.留空一个地址作为fun返回值, 待后面返回时填入
在这里插入图片描述
10.fun返回地址入栈, 通常是main函数当前pc指针的下一个
在这里插入图片描述
11.main函数的栈底地址入栈
在这里插入图片描述
12.pc指针跳转fun代码
在这里插入图片描述
13.c入栈
在这里插入图片描述
14.可以看到函数fun的数据 形参a,b 在上一层函数的栈中. 一部分在自己的栈上. 此步取值到加法器中进行加法运算,再赋值给c
在这里插入图片描述
15.c赋给返回值,填入上面的留空位置
在这里插入图片描述
16.栈底恢复上一层
在这里插入图片描述
17.lr赋值给pc, 实现了跳转

在这里插入图片描述

18.返回值赋值给全局变量m
在这里插入图片描述
19.前面函数调用的形参已经无用,回滚sp
在这里插入图片描述
20.函数返回,清理main的栈空间
在这里插入图片描述

4. 总结

可以看到,上述所说的流程没有遵循ATPCS规范,根据反汇编的代码看到也没有遵循ATPCS规则的寄存器使用规则。

1.在这里函数的传参一律使用栈传参,因此不涉及寄存器的保存。
2.callee的返回地址没有使用LR寄存器,而是使用caller的栈保存子函数返回地址。
3.FP寄存器在这里叫栈底寄存器,用来标识当前函数的栈底。每次进入一个子函数时,子函数一开始FP和SP相同,SP随着该子函数的栈变化而变化。

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老衲不依

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值