说明:
本文章旨在总结备份、方便以后查询,由于是个人总结,如有不对,欢迎指正;另外,内容大部分来自网络、书籍、和各类手册,如若侵权请告知,马上删帖致歉。
QQ 群 号:513683159 【相互学习】
内容来源:
《Debug Hack 中文版》#9
走进C语言:堆、栈与堆区、栈区,你知道有什么区别吗?
浅谈堆、栈、堆区、栈区的概念和区别
在64位linux下编译32位程序
函数调用栈-计算斐波拉契数列
函数调用过程中的栈帧结构及其变化
深入理解计算机系统–bomblab
实验环境:
ubuntu16.04,64位
什么是stack?
stack即栈/栈堆/堆叠,一般可分为两种:①数据结构(栈)②与内存分配有关(栈区)。
栈是一种数据项按序排列的数据结构(特殊的线性表),只能在一端对数据项进行插入(压栈 / pushl
)或删除(出栈 / popl
),故:具有 LIFO(Last In First Out,后进先出) 的特点。
允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom)。
栈区是内存中的一段区域,位于内存的最顶端地址(对栈区而言称为栈底),从内存顶部开始并向下增。操作方式与数据结构中的栈类似,的数据结构。
栈对数据的两种操作:
①PUSH(压栈):向栈中存储数据的操作。
②POP(弹栈):从栈中取出数据。
栈的作用(一般由编译器自动分配,函数执行完后自动释放):
①保存动态分配的自动变量。
②函数调用时,传递函数参数、保存返回地址和返回值
什么是stack frame?
stack frame即栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。
栈是从高地址向低地址延伸的。每个函数的每次调用,都有独立的一个栈帧,这个栈帧中维持着所需要的各种信息(函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等)。
寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)
【注意】:EBP指向当前位于系统栈最上边一个栈帧的底部,而不是系统栈的底部。严格说来,“栈帧底部”和“栈底”是不同的概念;ESP所指的栈帧顶部和系统栈的顶部是同一个位置。
程序功能:对main函数中传入数字参数(终值),实现计算从0至终值的递增的累加的总和。
#include
#include //调用isdigital函数所需头文件
#include
#define MAX (1UL << 20) //UL后缀表示无符号长整型,若不写系统将默认为int(有符号整型)
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(n < max_addend)
sum += sum_till_MAX(n);
return sum;
}
int main(int argc, char** argv)
{
u64 sum = 0;
if((argc == 2) && isdigit(*(argv[1]))) //判断参数个数是否为2,参数是否为数字
max_addend = strtoul(argv[1],NULL,0); //将参数数字字符串转为数字
if(max_addend > MAX || max_addend == 0) //增加健壮性
{
fprintf(stderr,"Invalid number is specified\n");//报错信息
return 1;
}
sum = sum_till_MAX(0); //调用函数计算
printf("sum(0..%u) = %llu\n",max_addend,sum); //输出结果
return 0;
}
1️⃣isdigital():菜鸟教程-isdigital()
函数原型:int isdigit(int c);
功能:用于检查其参数是否为十进制数字字符。
头文件:#include
返回值:若参数c为阿拉伯数字0~9,则返回非0值,否则返回0
2️⃣strtoul():菜鸟教程-strtoul()/C语言strtoul函数简介
函数原型:unsigned long int strtoul(const char *str, char **endptr, int base)
功能:把参数 str 所指向的字符串根据给定的 base 转换为一个无符号长整数(类型为 unsigned long int 型),base 必须介于 2 和 36(包含)之间,或者是特殊值 0
参数说明:
①str – 要转换为无符号长整数的字符串
②endptr – 对类型为 char* 的对象的引用,其值由函数设置为 str 中数值后的下一个字符
③base – 基数,必须介于 2 和 36(包含)之间,或者是特殊值 0。
头文件:#include
返回值:该函数返回转换后的长整数,如果没有执行有效的转换,则返回一个零值。
3️⃣fprintf():菜鸟教程-fprintf()
函数原型:int fprintf(FILE *stream, const char *format, ...)
功能:发送格式化输出到流 stream 中。
参数说明:
①stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
②format – 这是 C 字符串,包含了要被写入到流 stream 中的文本。【具体内容看上面链接】
① 若为64位环境编译出32位程序则需下载32位库:sudo apt-get install libc6-dev-i386 gcc-multilib g++-multilib
② 编译程序:gcc sum.c -o sum -g -m32
,产生sum文件,-g
包含调试信息,-m32
编译为32位程序
③ 执行程序:./sum 100
,可得:
sum(0..100) = 5050
现在进行调试查看函数调用与栈的关系
④ 启动gdb:gdb sum
,得到以下则表示成功,若不相同则是编译过程忘了加-g
选项。
...
Reading symbols from sum...done.
⑤反汇编main函数:disas /m main
,会将源程序与汇编代码对应打印出【一页装不下,回车可继续翻页】
disas介绍:
disas全称:disassemble ,即反汇编。
功能:对指定内存部分内容进行反汇编。
disas用法:/m、/s、/r (详细可查看help disas
)
...
36 sum = sum_till_MAX(0);
0x080485c8 <+160>: sub $0xc,%esp
0x080485cb <+163>: push $0x0 //调用函数时首先将函数参数压入栈中
0x080485cd <+165>: call 0x80484eb <sum_till_MAX> //call指令自动把返回地址(0x80484eb)压入栈中
0x080485d2 <+170>: add $0x10,%esp
0x080485d5 <+173>: mov %eax,-0x10(%ebp)
0x080485d8 <+176>: mov %edx,-0xc(%ebp)
...
⑥反汇编sum_till_MAX函数:disas sum_till_MAX
:只会打印出汇编程序
0x080484eb <+0>: push %ebp //1️⃣在栈上保存上层帧的帧指针
0x080484ec <+1>: mov %esp,%ebp //2️⃣将新的栈帧赋给帧指针
0x080484ee <+3>: sub $0x18,%esp //3️⃣栈上分配用于保存自动变量的空间
0x080484f1 <+6>: addl $0x1,0x8(%ebp) //4️⃣该处开始为sum_till_MAX函数的处理过程
0x080484f5 <+10>: mov 0x8(%ebp),%eax
0x080484f8 <+13>: mov %eax,-0x10(%ebp)
0x080484fb <+16>: movl $0x0,-0xc(%ebp)
0x08048502 <+23>: mov 0x804a028,%eax
0x08048507 <+28>: cmp %eax,0x8(%ebp)
0x0804850a <+31>: jae 0x8048520 <sum_till_MAX+53>
0x0804850c <+33>: sub $0xc,%esp
0x0804850f <+36>: pushl 0x8(%ebp)
0x08048512 <+39>: call 0x80484eb <sum_till_MAX>
0x08048517 <+44>: add $0x10,%esp
0x0804851a <+47>: add %eax,-0x10(%ebp)
0x0804851d <+50>: adc %edx,-0xc(%ebp)
0x08048520 <+53>: mov -0x10(%ebp),%eax
0x08048523 <+56>: mov -0xc(%ebp),%edx
0x08048526 <+59>: leave //5️⃣leave为删除栈帧指令,与1️⃣、2️⃣是完全相反的处理,释放栈
0x08048527 <+60>: ret //6️⃣子程序返回指令,将栈中保存的返回地址POP到程序计数寄存器,将控制权返回调用者
1️⃣ push S:压栈指令,将S压入栈中。 %ebp: 基指针寄存器(帧指针)
push %ebp
,将上层帧的帧指针存入栈中。
2️⃣ mov S,D:传送 指令:将S传送D中。%esp:堆栈指针寄存器(栈指针)
mov %esp,%ebp
,将新的栈帧赋给帧指针。
3️⃣ sub S,D:减法 指令:D减S存入D中。
sub $0x18,%esp
,%esp-0x18
后存入%esp
中,因为栈是位于内存高地址,所以减去数值表示栈指针向上移动。
4️⃣ addl S,D:加法 指令:D加S存入D中。
addl $0x1,0x8(%ebp)
:0x8(%ebp)
表示指向帧指针+8字节的地址(n
),即:n++
⑦ 设置程序运行参数:set args 100
⑧ 查看程序运行参数:show args
Argument list to give program being debugged when it is started is "100".
⑨查看代码:l 12
(l = list),也可l 1,40
查看全部,或l -
上翻,l
或l +
下翻
① 设置断点:b 12
(b = break)
Breakpoint 1 at 0x80484f1: file sum.c, line 12.
②查看断点:info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x080484f1 in sum_till_MAX at sum.c:12
③运行程序:r
(r = run r
后也可直接跟参数)
Starting program: /home/xsndz/Desktop/sum 100
Breakpoint 1, sum_till_MAX (n=0) at sum.c:15
15 n++;
④继续运行程序两次c 2
(c = continue 后可跟次数若无则表示一次)
Will ignore next crossing of breakpoint 1. Continuing.
Breakpoint 1, sum_till_MAX (n=2) at sum.c:15
15 n++;
⑤打印函数调用堆栈(栈信息):bt
(bt = backtrace)
#0 sum_till_MAX (n=2) at sum.c:15
#1 0x08048517 in sum_till_MAX (n=2) at sum.c:19
#2 0x08048517 in sum_till_MAX (n=1) at sum.c:19
#3 0x080485d2 in main (argc=2, argv=0xffffd014) at sum.c:36
⑥查看寄存器信息:i r eip ebp
(i = info r = registers)
eip 0x80484f1 0x80484f1 <sum_till_MAX+6>
ebp 0xffffced8 0xffffced8
PS:
1.当前执行位置(PC[程序计数器]):x86上为eip寄存器
2.FP(帧指针):x86上为ebp寄存器
⑦查看内存地址中的值x /40w $sp
(x =examine),从栈顶sp开始显示适当大小
0xffffcec0: 134520852 -134317918 0 429496729
0xffffced0: -134520832 0 -12536 134513943
0xffffcee0: 2 -1 0 -11737
0xffffcef0: -134230016 134513248 2 0
0xffffcf00: -134520832 -134520832 -12488 134513943
0xffffcf10: 2 0 10 0
0xffffcf20: -134517728 -134291472 1 0
0xffffcf30: -11737 -134227688 -12440 134514130
0xffffcf40: 1 0 0 134514005
0xffffcf50: 2 -12268 0 0
不是很懂,可能该处有点问题?
⑧再继续执行两次c 2
⑨再查看栈信息bt
#0 sum_till_MAX (n=4) at sum.c:15
#1 0x08048517 in sum_till_MAX (n=4) at sum.c:19
#2 0x08048517 in sum_till_MAX (n=3) at sum.c:19
#3 0x08048517 in sum_till_MAX (n=2) at sum.c:19
#4 0x08048517 in sum_till_MAX (n=1) at sum.c:19
#5 0x080485d2 in main (argc=2, argv=0xffffd014) at sum.c:36
同上面bt
的查看的栈信息对比可知,每次新压入栈的函数调用总为#0
①查看现在选择的帧frame
,可知该栈帧数为#0,之前的栈帧中的函数调用会在新的函数调用的下面,结合上图的栈空间模型想象一下。
#0 sum_till_MAX (n=4) at sum.c:15
15 n++;
②单步运行到sum变量处:s 2
(s = step)
18 if(n < max_addend)
②查看该帧的变量sum:p sum
(p = print)
$1 = 5
③选择上一帧:frame 1
,也可以用up
(上一帧),down
(下一帧)
#1 0x08048517 in sum_till_MAX (n=4) at sum.c:19
19 sum += sum_till_MAX(n);
④查看该帧的变量sum:p sum
$2 = 4
⑤查看更多详细的栈帧信息:i frame 1
Stack frame at 0xffffceb0:
eip = 0x8048517 in sum_till_MAX (sum.c:19); saved eip = 0x8048517
called by frame at 0xffffcee0, caller of frame at 0xffffce80
source language c.
Arglist at 0xffffcea8, args: n=4
Locals at 0xffffcea8, Previous frame's sp is 0xffffceb0
Saved registers:
ebp at 0xffffcea8, eip at 0xffffceac
该示例若不加参数运行则会引发段错误,./sum
Segmentation fault (core dumped)
①用gdb启动程序gdb sum
②运行程序:r
Starting program: /home/xsndz/Desktop/sum
Program received signal SIGSEGV, Segmentation fault.
0x0804850f in sum_till_MAX (n=174674) at sum.c:19
19 sum += sum_till_MAX(n);
可看出问题是处在第19行处,n=174674
,先查看一下程序计数器(PC)的内容。
程序计数器的作用:存放下一条指令所在单元的地址。
③查看程序计数器(PC)内存:x /i $pc
(x = examine i = instruction)
=> 0x804850f <sum_till_MAX+36>: pushl 0x8(%ebp)
这正是将sum_till_MAX()的参数n PUSH到栈顶的命令
④查看栈指针(SP)的位置:p $sp
$1 = (void *) 0xff7fdff4
⑤查看该进程的内存映射,即要查看GDB atttch了进程的内存映像,显示被调试的进程相对应/proc/i proc mapping
process 6927
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x8048000 0x8049000 0x1000 0x0 /home/xsndz/Desktop/sum
0x8049000 0x804a000 0x1000 0x0 /home/xsndz/Desktop/sum
0x804a000 0x804b000 0x1000 0x1000 /home/xsndz/Desktop/sum
0xf7e05000 0xf7e06000 0x1000 0x0
0xf7e06000 0xf7fb3000 0x1ad000 0x0 /lib32/libc-2.23.so
0xf7fb3000 0xf7fb4000 0x1000 0x1ad000 /lib32/libc-2.23.so
0xf7fb4000 0xf7fb6000 0x2000 0x1ad000 /lib32/libc-2.23.so
0xf7fb6000 0xf7fb7000 0x1000 0x1af000 /lib32/libc-2.23.so
0xf7fb7000 0xf7fba000 0x3000 0x0
0xf7fd3000 0xf7fd4000 0x1000 0x0
0xf7fd4000 0xf7fd7000 0x3000 0x0 [vvar]
0xf7fd7000 0xf7fd9000 0x2000 0x0 [vdso]
0xf7fd9000 0xf7ffc000 0x23000 0x0 /lib32/ld-2.23.so
0xf7ffc000 0xf7ffd000 0x1000 0x22000 /lib32/ld-2.23.so
0xf7ffd000 0xf7ffe000 0x1000 0x23000 /lib32/ld-2.23.so
0xff7fe000 0xffffe000 0x800000 0x0 [stack]
最后一行[stack],表示栈空间,可知栈顶为地址为0xff7fe000
,栈底为0xffffe000
,而刚刚查看栈指针的地址为0xff7fdff4
,超出栈空间,即发生栈溢出。【栈是位于内存高地址,向下扩展】
使用该命令GDB会打开/proc/info fils
,info target
⑥退出调试:q
(q = quit)
⑦查看栈空间大小:ulimit -s
【-a可查看全部限制,关于ulimit指令:修改shell的资源限制,可通过help ulimit
查看对应作用。】
8192
可知该环境进程允许栈的大小为8MB。
⑧扩大栈空间:ulimit -s 81920
【将栈空间扩大为原来的10倍】
⑨再次执行程序:./sum
sum(0..1048576) = 549756338176
很明显,之前n算到174674时发生栈溢出,故将栈空间扩大到能继续计算的大小即可。【一开始设置n = 1 << 20 = 1048576】
验证计算结果是否正确,使用等差公式就和计算:(1 + 1048576)*(1048576/2)= 549756338176
PS:
每个进程都有允许的栈大小,但实际上每个线程的栈大小也有限制。
多线程编程时,各个线程使用的栈的总和不能超过进程许可的栈大小,同时还要注意各线程栈大小的限制。