![19e46a4c337f334fb3e4d421eddbef63.png](https://img-blog.csdnimg.cn/img_convert/19e46a4c337f334fb3e4d421eddbef63.png)
为什么会写篇栈变化的文章?做系统分析的话你肯定遇到过一些crash, oops等棘手问题,一般大家都会用 gdb, objdump 或者 addr2line等工具分析 pc 位置来定位出错的地方。但是这些分析工具背后的本质原理就不见得理解深刻了,而且有的时候面对一系列 backtrace 或者 stack 日志处于懵逼的状态。
今天和大家一起看下面对 crash 日志的时候,如何利用 stack 来分析其变化的来龙去脉。
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。它不能用于任何其它用途。
演示代码
假如现在你已经掌握了 arm 指令的用法,即便没有掌握也没关系,“书到用时回头翻”。这里以一段简单的 c 语言为例:
#include
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 -D main
00010400 :
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 :
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
10468: e1a02000 mov r2, r0
1046c: e59f3010 ldr r3, [pc, #16] ; 10484
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
图解栈的变化过程
如何能让读者接受吸收的更快,我一直觉得按照学习效率来讲的话顺序应该是视频,图文,文字。反正我是比较喜欢视频类的教学。这里给大家画下栈变化的过程是什么样子的。这里的图是结合上面的代码来画的,希望有助于读者的理解。
1.程序在内存分布区域
![163f889c72f0912b5d99a2a63674b126.png](https://img-blog.csdnimg.cn/img_convert/163f889c72f0912b5d99a2a63674b126.png)
2.全局变量m赋值
![90d886551d2fd4b3953761c8b9f3e172.png](https://img-blog.csdnimg.cn/img_convert/90d886551d2fd4b3953761c8b9f3e172.png)
3.保存进入main之前的栈底, fp-sp之间是当前函数栈
![7a21442e4438793cceca2a0b3b499e67.png](https://img-blog.csdnimg.cn/img_convert/7a21442e4438793cceca2a0b3b499e67.png)
4.函数main的栈已经准备好了
![3c01c4e402f9d679d165963ae40d1f1d.png](https://img-blog.csdnimg.cn/img_convert/3c01c4e402f9d679d165963ae40d1f1d.png)
5.i入栈
![733e5859a51a0ecf0105ef0e0d084835.png](https://img-blog.csdnimg.cn/img_convert/733e5859a51a0ecf0105ef0e0d084835.png)
6.j入栈
![872e480da20346f0868725228d4a7dd0.png](https://img-blog.csdnimg.cn/img_convert/872e480da20346f0868725228d4a7dd0.png)
7.准备函数fun的调用, 形参反向入栈 先形参b入栈
![e56c7b574e7cb10a1f45ce06ebdd18b0.png](https://img-blog.csdnimg.cn/img_convert/e56c7b574e7cb10a1f45ce06ebdd18b0.png)
8.形参a入栈
![6300a259a723864a24649600cebfac69.png](https://img-blog.csdnimg.cn/img_convert/6300a259a723864a24649600cebfac69.png)
9.留空一个地址作为fun返回值, 待后面返回时填入
![2c2ac5e99ef96f0f249b92af4c7d82e0.png](https://img-blog.csdnimg.cn/img_convert/2c2ac5e99ef96f0f249b92af4c7d82e0.png)
10.fun返回地址入栈, 通常是main函数当前pc指针的下一个
![76cc92025f5a32ca69e70310392994b8.png](https://img-blog.csdnimg.cn/img_convert/76cc92025f5a32ca69e70310392994b8.png)
11.main函数的栈底地址入栈
![3602a3b24731a1de5769e4f23a35dd67.png](https://img-blog.csdnimg.cn/img_convert/3602a3b24731a1de5769e4f23a35dd67.png)
12.pc指针跳转fun代码
![67adf1524846304c898b9c3ff332b8f9.png](https://img-blog.csdnimg.cn/img_convert/67adf1524846304c898b9c3ff332b8f9.png)
13.c入栈
![46cc7aea7536b31f7f1cdaeab4802eba.png](https://img-blog.csdnimg.cn/img_convert/46cc7aea7536b31f7f1cdaeab4802eba.png)
14.可以看到函数fun的数据 形参a,b 在上一层函数的栈中. 一部分在自己的栈上. 此步取值到加法器中进行加法运算,再赋值给c
![40cb9ed19a6addd73413577ef9513803.png](https://img-blog.csdnimg.cn/img_convert/40cb9ed19a6addd73413577ef9513803.png)
15.c赋给返回值,填入上面的留空位置
![63702fb4af37056d9e011c36222d3503.png](https://img-blog.csdnimg.cn/img_convert/63702fb4af37056d9e011c36222d3503.png)
16.栈底恢复上一层
![c6284e3594beaf36134a74d3838938b9.png](https://img-blog.csdnimg.cn/img_convert/c6284e3594beaf36134a74d3838938b9.png)
17.lr赋值给pc, 实现了跳转
![7f4cb34fb3209c9555cb1d056ef0d08c.png](https://img-blog.csdnimg.cn/img_convert/7f4cb34fb3209c9555cb1d056ef0d08c.png)
18.返回值赋值给全局变量m
![289643aed3e8cb0b21e69c5824dbcf55.png](https://img-blog.csdnimg.cn/img_convert/289643aed3e8cb0b21e69c5824dbcf55.png)
19.前面函数调用的形参已经无用,回滚sp
![cab9430b24f1f1f26c7582126ebd1af8.png](https://img-blog.csdnimg.cn/img_convert/cab9430b24f1f1f26c7582126ebd1af8.png)
20.函数返回,清理main的栈空间
![56850a68f78ec434f1c3e2b3b098c8d6.png](https://img-blog.csdnimg.cn/img_convert/56850a68f78ec434f1c3e2b3b098c8d6.png)
总结
这么多图有没有看花?相信到这里你已经了解了栈背后的来龙去脉,下一篇我们一起根据实际的 stack 错误案例剖析错误的可能性。
添加极客助手微信,加入技术交流群
![d636f88c00d653cc5b7a372ed626f872.png](https://img-blog.csdnimg.cn/img_convert/d636f88c00d653cc5b7a372ed626f872.png)