入门系列:gdb学习——函数调用栈

说明
  本文章旨在总结备份、方便以后查询,由于是个人总结,如有不对,欢迎指正;另外,内容大部分来自网络、书籍、和各类手册,如若侵权请告知,马上删帖致歉。
  QQ 群 号:513683159 【相互学习】
内容来源
  《Debug Hack 中文版》#9
  走进C语言:堆、栈与堆区、栈区,你知道有什么区别吗?
  浅谈堆、栈、堆区、栈区的概念和区别
  在64位linux下编译32位程序
  函数调用栈-计算斐波拉契数列
  函数调用过程中的栈帧结构及其变化
  深入理解计算机系统–bomblab
实验环境
  ubuntu16.04,64位

栈(stack)的相关知识

什么是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

  现在进行调试查看函数调用与栈的关系
  ④ 启动gdbgdb 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++

简易图示

入门系列:gdb学习——函数调用栈_第1张图片

  ⑦ 设置程序运行参数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 -上翻,ll +下翻
  ① 设置断点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/ /map的信息: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//maps。因此分析core dump时无法使用,可使用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:
  每个进程都有允许的栈大小,但实际上每个线程的栈大小也有限制。
  多线程编程时,各个线程使用的栈的总和不能超过进程许可的栈大小,同时还要注意各线程栈大小的限制。

你可能感兴趣的:(基础知识,gdb)