2021 元旦好,祝大家新的一年顺顺顺。

 

为什么会写篇栈变化的文章?做系统分析的话你肯定遇到过一些 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. 程序在内存分布区域

 

 

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 的栈空间

 

 

总结

这么多图有没有看花?相信到这里你已经了解了栈背后的来龙去脉,下一篇我们一起根据实际的 stack 错误案例剖析错误的可能性。