【ARM Linux 系统稳定性分析入门及渐进 3 -- 栈溢出】


请阅读嵌入式及芯片开发学必备专栏


请阅读【ARM Linux 系统稳定性分析专栏导读】

上篇文章:ARM Linux 系统稳定性分析入门及渐进 2 – Kernel Lockup
下篇文章:ARM Linux 系统稳定性分析入门及渐进 4 – 栈分类

1.1 栈溢出

的空间必须由程序员静态的分配,但计算 堆heap栈stack的空间 大小却不是一件简单的事情,即便是对于最小的嵌入式系统。栈一般静态分配,并且后进先出,开发者静态的指定栈内存空间,一般栈向下生长,即从高地址到低地址,如果栈空间不足,发生下溢,则栈之下的内存空间被写入

导致栈溢出的常见的情况有以下几种:
(1) 局部数组过大
当系统栈设置比较小时,会导致栈溢出。当程序确实需要大数组时,可以设置为静态变量或全局变量。

(2) 递归调用层次太多
递归函数在运行时会执行压栈操作,当压栈次数太多时,也会导致堆栈溢出。

(3) 指针或者数组越界
比方说错误的指针、不加边界检查的数组访问等会造成这种情况。
这两种栈区溢出都会产生 死机 或者 代码跑飞 的风险。当栈区溢出时,栈指针指向非法区域,这个区域有可能是 全局变量区,有可能是别的 task 的栈区。

以函数中数据越界为例:
函数 func_a 调用 func_b,通过汇编我们知道在执行func_b的指令之前首先会为 func_b进行栈帧分配,一般都是进行 SP 指针减去一个立即数据

1.1.1 栈结构

每个进程都会有自己的栈空间,而进程中的各个函数也会维护自己本身的一个栈的区域,这个区域就是栈帧那么一个函数的栈帧的区域是如何来界定的呢?当然,首先会普及ARM的几个特殊寄存器功能

  • R11:frame pointer,FP寄存器
  • R12:IP寄存器,用于暂存SP
  • R13:stack pointer,SP寄存器
  • R14:link register,LR寄存器
  • R15:PC寄存器

而在 ARM上,函数的栈帧是由 SP寄存器FP寄存器 来界定的,参见图:
在这里插入图片描述
上图描述的是 main 函数调用 func1 函数的栈帧情况,从图可知,当 main 函数 调用 func1函数时,func1 函数会先将 PC、LR、SP、FP 四个寄存器压到栈上边,其中 SPFP 的值分别指向 main 函数栈帧的两个边界,LR 的值保存的是 func1 调用结束之后的返回值,PC 值表示的是当前执行到的指令地址,放置的是进入 func1 后的指令地址。紧接着就会在栈上分配一片区域,用于放置局部变量等。

如果 func1 中还调用了 func2 子函数,那么也会为 func2 创建一个栈帧,并且func2SPFP 会指向 func1 栈帧的两个边界。这样当函数返回的时候,参数进行出栈,也能找到 Caller 函数,这个也就是 backtrace 的原理。

1.1.2 汇编实例

反汇编分析某段代码,如下图所示:
在这里插入图片描述

  • 红色部分,表明进入到函数时先将几个特殊的寄存器压栈;
  • 黄色部分,sub sp, sp, #16,表明开辟一个4 x 32bit 大小的栈区域;
  • 蓝色部分,将传入的参数压栈,在 ARM ATPCS 中规定,寄存器R0-R3用来传参;
  • 绿色部分,调用子函数。

并不是所有函数调用都需要先 push {fp, ip, lr, pc},当子函数调用过程中,并不会去改变这些值的时候,就不需要压栈,说白了,压栈的目的就是为了在使用完的时候能恢复原来的状态。

1.1.3 数组越界栈回踩

从上面的 1.1.1 节内容我们知道,ARM 架构上一般栈都是向下增长的,如果在 函数 func1 中定义了一个大小为 N 的数组 int test[N],在 for 循环中根据数组下标 i 向数组 test 中写入数据 N0xfffff

int fun1(int a, int b, int c, int loop)
{
	...
	int test[N], i;
	for (int i = 0; i < loop; i++)
		*(test + i) = 0xffff;
	...

如果由于某种原因 loop 的值大于 N 的值,这样就会出现在 func1 栈空间向数组 &test[N] 之后的地址处写入数据, 从栈帧分配可以看到,func1的栈帧高地址处存放的是 FP, SP, LR, PC,如果 LR 的值被修改为 0xffff,那么在 func1 函数返回 main时将会出现致命错误,因为 func1 返回时,系统会从 LR 地址处取指令给 PC,而此时 func1栈帧中保存的 LR 的值已经被修改为 0xffff 了。

1.1.4 栈保护区

栈保护区是一块分配在栈之下的一块内存空间(假设栈stack是向下生长的),如图 2 所示,这样当栈 stack下溢时,就能在保护区留下痕迹 trace。通过软件的方法来检查保护区是否还完整(即填充保护区相同的数据,如0xff 的数据,然后检测保护区是否被写入)来确定栈下溢情况。有些公司在 task 创建后,stack 会被全部初始化为 0xEFEFEFEF

1.1.5 检测栈下溢

把栈空间填充成固定的值可以检测栈空间的下溢,如在程序开始前填充 0xCD。当程序终止时,栈内存可以从栈底端搜索模式直到 0xCD 没找到。这样就能得到从栈顶端的栈空间大小。

上篇文章:ARM Linux 系统稳定性分析入门及渐进 2 – Kernel Lockup
下篇文章:ARM Linux 系统稳定性分析入门及渐进 4 – 栈分类


在这里插入图片描述

推荐阅读:
https://www.jianshu.com/p/91c5dc0a8bb9

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

主公讲 ARM

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

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

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

打赏作者

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

抵扣说明:

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

余额充值