随手记——栈空间不足导致的系统异常问题

Posted 穿越临界点

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了随手记——栈空间不足导致的系统异常问题相关的知识,希望对你有一定的参考价值。

在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的无效信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。

1 问题引入

栈空间不足的问题出现的概率其实不是很高。因为默认的栈空间都是MB级别的,如果调用深度不是很深或者局部变量不是很大是很难发生栈空间不足的,除了以下3种情况:

  • 使用了未知的第三方代码库。
  • 业务剧烈变更。
  • 新手。

出现栈空间不足的表象一般都是任务异常和段错误。按道理来讲,出现段错误很明显啊,在测试阶段直接定位解决了不就行了,也不算是什么疑难杂症。

如果真实的工程都和学校里的示例程序一样简单,当然这个推论是成立的。但是业务场景复杂之后,只有流程跑到这里才会暴露问题,而且这个问题还是一个致命问题。

为什么栈空间不足了还和流程相关呢?请看下文。。。

2 问题分析定位

当出现任务异常和段错误后,第一反应是指针解引用错误。往往都是一顿查指针的使用。然后,有时候排查完指针的使用也没有什么头绪时,就要考虑是否是栈空间不足导致的了(如果是第一次遇到,没有这个意识,确实会一头雾水)。

2.1 -fstack-protector-all只适用于栈泄露

有人推荐使用 -fstack-protector-all 选项定位是否栈空间不足了。但我亲测发现它 罩不住

考虑到有的TX对这个编译选项还不是很熟悉,借此机会简单介绍一下。

2.1.1 监控栈泄露

下面是一个C语言写的小例子。

# 编译时添加栈保护机制选项
gcc -fstack-protector-all -g stack.c

其中12和13行都有内存越界访问,但是,b数组的越界访问不属于栈泄露。因为在没有栈溢出保护机制下编译时,局部变量的定义顺序即入栈顺序。在添加了栈溢出保护机制后,局部变量的入栈顺序和类型相关,比如char类型先入栈,int类型后入栈;对于同一种类型(例如本例中的a和b数组),入栈顺序和定义顺序刚好相反。

所以,b数组虽然存在越界访问,但是不属于栈溢出。因此,-fstack-protector-all的能力很有限。

  /*C源文件*/
  1 #include <stdio.h>
  2
  3 #define MAX (1000)
  4
  5 void test(void)
  6 
  7         char a[MAX] = 0;
  8         char b[8] = 0;
  9
 10         printf("debug add_a = %p; add_b = %p \\n", a, b);
 11
 12         a[MAX-1+8] = 3; /*可以被栈溢出机制检测到*/
 13         b[16] = 4; /*不可以被栈溢出机制检测到*/
 14 
 15
 16 int main(void)
 17 
 18         test();
 19
 20         return 0;
 21 

栈泄露的监控结果如下:

debug add_a = 0x7ffcec109b00; add_b = 0x7ffcec109af0
*** stack smashing detected ***: ./a.out terminated
======= Backtrace: =========
/lib64/libc.so.6(__fortify_fail+0x37)[0x7f2b039429e7]
/lib64/libc.so.6(+0x1179a2)[0x7f2b039429a2]
./a.out[0x40060b]
./a.out[0x400629]
/lib64/libc.so.6(__libc_start_main+0xf5)[0x7f2b0384d3d5]
./a.out[0x4004c9]
======= Memory map: ========
00400000-00401000 r-xp 00000000 fd:02 6988561411                         /home/zhaoyuandong/mycode/0713_stack/1/a.out
00600000-00601000 r--p 00000000 fd:02 6988561411                         /home/zhaoyuandong/mycode/0713_stack/1/a.out
00601000-00602000 rw-p 00001000 fd:02 6988561411                         /home/zhaoyuandong/mycode/0713_stack/1/a.out
00ad7000-00af8000 rw-p 00000000 00:00 0                                  [heap]
7f2b03615000-7f2b0362a000 r-xp 00000000 fd:00 41943555                   /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7f2b0362a000-7f2b03829000 ---p 00015000 fd:00 41943555                   /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7f2b03829000-7f2b0382a000 r--p 00014000 fd:00 41943555                   /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7f2b0382a000-7f2b0382b000 rw-p 00015000 fd:00 41943555                   /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7f2b0382b000-7f2b039ed000 r-xp 00000000 fd:00 41960524                   /usr/lib64/libc-2.17.so
7f2b039ed000-7f2b03bed000 ---p 001c2000 fd:00 41960524                   /usr/lib64/libc-2.17.so
7f2b03bed000-7f2b03bf1000 r--p 001c2000 fd:00 41960524                   /usr/lib64/libc-2.17.so
7f2b03bf1000-7f2b03bf3000 rw-p 001c6000 fd:00 41960524                   /usr/lib64/libc-2.17.so
7f2b03bf3000-7f2b03bf8000 rw-p 00000000 00:00 0
7f2b03bf8000-7f2b03c1a000 r-xp 00000000 fd:00 41960517                   /usr/lib64/ld-2.17.so
7f2b03df7000-7f2b03dfa000 rw-p 00000000 00:00 0
7f2b03e16000-7f2b03e19000 rw-p 00000000 00:00 0
7f2b03e19000-7f2b03e1a000 r--p 00021000 fd:00 41960517                   /usr/lib64/ld-2.17.so
7f2b03e1a000-7f2b03e1b000 rw-p 00022000 fd:00 41960517                   /usr/lib64/ld-2.17.so
7f2b03e1b000-7f2b03e1c000 rw-p 00000000 00:00 0
7ffcec0ea000-7ffcec10b000 rw-p 00000000 00:00 0                          [stack]
7ffcec1f2000-7ffcec1f4000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
Aborted (core dumped)

2.1.2 无法监控栈空间不足

我们再构造一种栈空间不足的场景,编译时同样加上 -fstack-protector-all 选项,看看能否生效。

Tips:我使用的x86平台创建一个进程默认栈空间大小是8MB。至于如何查看和修改请看问题解决一节。

  1 #include <stdio.h>
  2
  3 #define MAX (8192 * 1024) // 定义长度为默认的栈空间大小
  4 void test(void)
  5 
  6         char a[MAX];
  7
  8         a[MAX] = 3; // ok
  9
 10         printf("a[-1] = %d.\\n", a[MAX-8]); // not ok
 11 
 12
 13 int main(void)
 14 
 15         test();
 16
 17         return 0;
 18 

运行结果如下所示。

Segmentation fault (core dumped)

我们发现,-fstack-protector-all是无法有效监控栈空间不足的问题的。

那么我们如何跟踪定位这个问题呢?答案是使用GDB。


2.2 GDB跟踪调试

使用GDB可以精准的定位到是哪一行代码出现了问题(编译时加上-g选项),对GDB不太熟悉的TX可以看这篇:随手记——GDB调试入门看这一篇就够了

(gdb) r
Starting program: /home/zhaoyuandong/mycode/0713_stack/1/a.out

Program received signal SIGSEGV, Segmentation fault.
0x000000000040053f in test () at main2.c:10
10              printf("a[-1] = %d.\\n", a[MAX-8]); // not ok

比较细心的TX可能会文了,即使用GDB跟到了这里,那又怎么确定是指针使用错误还是栈空间不足导致的呢?

好问题。有两个比较简单的方式可以验证:

  • 一个是缩减变量长度测试。
  • 一个是将局部变量定义成全局变量;或者直接在局部变量前添加static将其定义到静态区后测试。

如果上述两个方式生效了,就基本说明是栈空间不足导致的。

当然,还可以实际查看一下当前线程的栈空间大小和地址(maps信息)以便进一步石锤。


3 问题解决

3.1 更改变量大小或位置

解决栈空间不足的问题优选删除或减小过大的局部变量。

如果该函数属于调用比较频繁的可以考虑使用局部static变量。

如果调用不是非常频繁,对性能也没有太高要求的话,可以考虑使用malloc动态申请内存。

3.2 扩展栈空间

如果优化效果不明显,或者由于业务扩张导致的栈空间严重不足的话就只能选择扩充栈空间了。

3.2.1 通过shell指令

# 查询当前默认的栈空间大小
[xxx@bogon 1]$ ulimit -s
8192 # 我的Linux系统上默认的是8192 MB

# 设置默认的栈空间大小
[xxx@bogon 1]$ ulimit -s 8193

3.2.2 通过posix接口

配置堆栈空间大小一般在创建线程之前进行。使用下述接口获取和配置:

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);

如果你也遇到栈空间不足的问题,在代码里找到上述接口,把栈空间大小改合适就可以了。


4 复盘

在问题引入时,我们提到了这个问题的引入大体分为3种情况:使用第三方代码;业务变动;新手编程。

最后一种情况抛开不讲(涉及到公司的培训制度)。另外两种情况出现时是否有应对策略?

其实是有的,但是大部分国内的公司并没有做。最简单直接的方式就是在上述两种情况产生时,一定要使用栈空间监测工具进行测试,提前发现 病灶 ,提前治疗。

对于栈空间监测工具的制作也并不复杂,我们下回接着说。


恭喜你又坚持看完了一篇博客,又进步了一点点!如果感觉还不错就点个赞再走吧,你的点赞和关注将是我持续输出的哒哒哒动力~~

以上是关于随手记——栈空间不足导致的系统异常问题的主要内容,如果未能解决你的问题,请参考以下文章

随手记——栈空间使用率实时监测工具

随手记——栈空间使用率实时监测工具

inline

inline详解

inline函数

C++inline函数