栈:是程序存放数据内存区域之一,特点是LIFO(后进先出)。
PUSH:入栈
POP:出战
使用场景:
1.保存动态分配的自动变量使用栈
2.函数调用时,用栈传递函数参数,半寸返回地址,返回值
代码sum.c
#include
#include
#include
#define MAX (1UL << 20)
typedef unsigned long long u64;
typedef unsigned int u32;
u32 max_addend=MAX;
u64 sum_till_MAX(u32 n)
{
u64 sum;
n++;
sum=n;
if(nMAX||max_addend==0){
fprintf(stderr,"Invalid number is specified\n");
return 1;
}
sum=sum_till_MAX(0);
printf("sum(0..%lu)=%llu\n",max_addend,sum);
return 0;
}
该程序计算0到命令行参数传递过来的数字之间所有的正数和。
图a是函数调用前的栈,图b是函数调用后的栈,图c是再次函数调用后的栈。
栈上保存了函数参数,调用返回地址,上层栈帧指针和函数内部使用的自动变量。
有时,还会用栈保存寄存器,每个函数独有,称作栈帧。
此时,需要设置表示栈帧起始地址的帧指针(FP)。
栈指针(SP)永远指向栈顶!
编译程序
#gcc -g -Wall -o sum sum.c
启动gdb调试sum
#gdb sum
(gdb) disas main
从图中可见call指令自动把返回地址0x8048494压入栈中
再看sum_till_MAX函数
在栈上保存上层帧的帧指针0x8048494,
然后将新的栈帧赋给帧指针%esp,
然后在栈上分配用于保存自动变量的空间,至此完成栈帧。
0x8(%ebp)指向帧指针+8字节的地址,
-0x10(%ebp)为指针-10字节的地址
leave指令删除栈帧,释放当前的栈,
ret指令为函数返回,将栈中保存的返回地址POP到程序计数寄存器,控制权返回给调用者。
这是第二次进入sum_till_MAX函数时使用backtrace
获取当前执行位置,可以通过程序计数器PC获得,在x86上是eip寄存器,FP是ebp寄存器
下面查看栈的内容,从表示栈顶的SP开始。
(gdb) x/40xw $sp
从栈上的返回地址信息可以看到和之前backtrace结果相同的调用跟踪信息0x080484c1和0x0804858d
用frame命令查看现在选择的帧,
frame 1选择帧1
up选择下一层的帧
用info命令的frame选项可以查看更详细的栈帧信息
栈的大小限制
注意,如果上述sum程序不带参数会引发段错误。
#./sum
发生栈溢出。
下面用gdb跟踪,查看程序计数器PC即可看到程序执行位置
#gdb sum
(gdb) r
(gdb) x/i $pc
这正是将sum_till_MAX的参数PUSH到栈顶的命令
查看栈指针SP的位置
(gdb) p $sp
查看该进程的内存映射
(gdb) i proc mapping
最后一行的[stack]表示栈空间,顶端是0xbf401000,之前看到的栈指针是0xbf3ffff0,超出了栈的范围发生溢出。
分析内核转储的时候无法使用上述命令,可以使用
(gdb) info files或者(gdb) info target得到相同信息
linux栈大小
显示本机系统支持的栈大小
# ulimit -s
10240
修改栈大小扩大10倍
#ulimit -Ss 102400
这样就不会栈溢出了
Linux下默认的栈空间大小是10M,可以通过ulimit -s进行查看,不同的Linux发行版本可能不太一样。下面介绍栈空间大小修改方法。
临时修改方法:
ulimit -s <新的栈空间大小>
永久修改方法:
1. 可以修改配置文件/etc/security/limits.conf
2. 可以将ulimit -s命令放到/etc/profile中,在任何用户启动的时候调用
参考《Debug.Hacks中文版 深入调试的技术和工具-调试时必须的栈知识》