GDB查看堆栈局部变量

“参数从右到左入栈”,“局部变量在栈上分配空间”,听的耳朵都起茧子了。最近做项目涉及C和汇编互相调用,写代码的时候才发现没真正弄明白。自己写了个最简单的函数,用gdb跟踪了调用过程,才多少懂了一点。

参考资料:

http://blog.csdn.net/liigo/archive/2006/12/23/1456938.aspx

http://blog.csdn.net/eno_rez/archive/2008/03/08/2158682.aspx

int add(int x, int y)

{

int a = 0;

a = x;

a += y;

return a;

}

int main(int argc, char *argv[])

{

int x, y, result;

x = 0x12;

y = 0x34;

result = add(x, y);

return 0;

}

编译:(Fedora6, gcc 4.1.2)

[test]$ gcc -g -Wall -o stack stack.c

反汇编:

这里的汇编的格式是AT&T汇编,它的格式和我们熟悉的汇编格式不太一样,尤其要注意源操作数和目的操作数的顺序是反过来的

[test]$ objdump -d stack > stack.dump

[test]$ cat stack.dump

......

08048354 :

8048354:       55                      push   %ebp  ;保存调用者的帧指针

8048355:       89 e5                   mov    %esp,%ebp  ;把当前的栈指针作为本函数的帧指针

8048357:       83 ec 10                sub    $0x10,%esp  ;调整栈指针,为局部变量保留空间

804835a:       c7 45 fc 00 00 00 00    movl   $0x0,0xfffffffc(%ebp)  ;把a置0。ebp-4的位置是第一个局部变量

8048361:       8b 45 08                mov    0x8(%ebp),%eax  ;把参数x保存到eax。ebp+8的位置是最后一个入栈的参数,也就是第一个参数

8048364:       89 45 fc                mov    %eax,0xfffffffc(%ebp)  ;把eax赋值给变量a

8048367:       8b 45 0c                mov    0xc(%ebp),%eax  ;把参数y保存到eax。ebp+C的位置是倒数第二个入栈的参数,也就是第二个参数

804836a:       01 45 fc                add    %eax,0xfffffffc(%ebp)  ;a+=y

804836d:       8b 45 fc                mov    0xfffffffc(%ebp),%eax  ;把a的值作为返回值,保存到eax

8048370:       c9                      leave

8048371:       c3                      ret

08048372 :

8048372:       8d 4c 24 04             lea    0x4(%esp),%ecx  ;????

8048376:       83 e4 f0                and    $0xfffffff0,%esp  ;把栈指针16字节对齐

8048379:       ff 71 fc                pushl  0xfffffffc(%ecx)  ;????

804837c:       55                      push   %ebp  ;保存调用者的帧指针

804837d:       89 e5                   mov    %esp,%ebp  ;把当前的栈指针作为本函数的帧指针

804837f:       51                      push   %ecx  ;????

8048380:       83 ec 18                sub    $0x18,%esp  ;调整栈指针,为局部变量保留空间

8048383:       c7 45 f0 12 00 00 00    movl   $0x12,0xfffffff0(%ebp)  ;x=0x12。ebp-16是局部变量x

804838a:       c7 45 f4 34 00 00 00    movl   $0x34,0xfffffff4(%ebp)  ;y=0x34。ebp-12是局部变量y

8048391:       8b 45 f4                mov    0xfffffff4(%ebp),%eax  ;y保存到eax

8048394:       89 44 24 04             mov    %eax,0x4(%esp)  ;y作为最右边的参数首先入栈

8048398:       8b 45 f0                mov    0xfffffff0(%ebp),%eax  ;x保存到eax

804839b:       89 04 24                mov    %eax,(%esp)  ;x第二个入栈

804839e:       e8 b1 ff ff ff          call   8048354   ;调用add

80483a3:       89 45 f8                mov    %eax,0xfffffff8(%ebp)  ;把保存在eax的add的返回值,赋值给位于ebp-8的第三个局部变量result。注意这条指令的地址,就是add的返回地址

80483a6:       b8 00 00 00 00          mov    $0x0,%eax  ;0作为main的返回值,保存到eax

80483ab:       83 c4 18                add    $0x18,%esp  ;恢复栈指针,也就是讨论stdcall和cdecl的时候总要提到的“调用者清栈”

80483ae:       59                      pop    %ecx  ;

80483af:       5d                      pop    %ebp  ;

80483b0:       8d 61 fc                lea    0xfffffffc(%ecx),%esp  ;

80483b3:       c3                      ret

80483b4:       90                      nop

......

有一点值得注意的是main在调用add之前把参数压栈的过程。

它用的不是push指令,而是另一种方法。

在main入口调整栈指针的时候,也就是位于8048380的这条指令 sub $0x18,%esp

不但象通常函数都要做的那样给局部变量预留了空间,还顺便把调用add的两个参数的空间也预留出来了。

然后把参数压栈的时候,用的是mov指令。

我不太明白这种方法有什么好处。

另外一个不明白的就是main入口的四条指令8048372、8048376、8048379、804837f,还有与之对应的main返回之前的指令。

貌似main对esp要求16字节对齐,所以先把原来的esp压栈,然后强行把esp的低4位清0。等到返回之前再从栈里恢复原来的esp

准备工作都做好了,现在开始gdb

对gdb不太熟悉的同学要注意一点,stepi命令执行之后显示出来的源代码行或者指令地址,都是即将执行的指令,而不是刚刚执行完的指令。

我在每个stepi后面都加了注释,就是刚执行过的指令。

[test]$ gdb -q stack

(gdb) break main

Breakpoint 1 at 0x8048383: file stack.c, line 11.

gdb并没有把断点设置在main的第一条指令,而是设置在了调整栈指针为局部变量保留空间之后

(gdb) run

Starting program: /home/brookmill/test/stack

Breakpoint 1, main () at stack.c:11

11              x = 0x12;

(gdb) stepi    // 注释: movl   $0x12,0xfffffff0(%ebp)

12              y = 0x34;

(gdb) stepi    // 注释: movl   $0x34,0xfffffff4(%ebp)

13              result = add(x, y);

(gdb) info registers esp

esp            0xbf8df8ac       0xbf8df8ac

(gdb) info registers ebp

ebp            0xbf8df8c8       0xbf8df8c8

(gdb) x/12 0xbf8df8a0

0xbf8df8a0:     0x002daff4      0x002d9220      0xbf8df8d8      0x080483e9

0xbf8df8b0:     0x001ca8d5      0xbf8df96c      0x00000012      0x00000034

0xbf8df8c0:     0x001903d0      0xbf8df8e0      0xbf8df938      0x001b4dec

这就是传说中的栈。在main准备调用add之前,先看看这里有些什么东东

0xbf8df8c8(ebp)保存的是上一层函数的帧指针:0xbf8df938,距离这里有112字节

0xbf8df8cc(ebp+4)保存的是main的返回地址0x001b4dec

0xbf8df8b8(ebp-16)是局部变量x,已经赋值0x12;

0xbf8df8bc(ebp-12)是局部变量y,已经赋值0x34;

0xbf8df8c0(ebp-8)是局部变量result。值得注意的是,因为我们没有给result赋值,这里是一个不确定的值。局部变量如果不显式的初始化,初始值不一定是0。

现在开始调用add

(gdb) stepi    // 注释: mov    0xfffffff4(%ebp),%eax

0x08048394      13              result = add(x, y);

(gdb) stepi    // 注释: mov    %eax,0x4(%esp)

0x08048398      13              result = add(x, y);

(gdb) x/12 0xbf8df8a0

0xbf8df8a0:     0x002daff4      0x002d9220      0xbf8df8d8      0x080483e9

0xbf8df8b0:     0x00000034      0xbf8df96c      0x00000012      0x00000034

0xbf8df8c0:     0x001903d0      0xbf8df8e0      0xbf8df938      0x001b4dec

y首先被压栈,在0xbf8df8b0

(gdb) stepi    // 注释: mov    0xfffffff0(%ebp),%eax

0x0804839b      13              result = add(x, y);

(gdb) stepi    // 注释: mov    %eax,(%esp)

0x0804839e      13              result = add(x, y);

(gdb) x/12 0xbf8df8a0

0xbf8df8a0:     0x002daff4      0x002d9220      0xbf8df8d8      0x00000012

0xbf8df8b0:     0x00000034      0xbf8df96c      0x00000012      0x00000034

0xbf8df8c0:     0x001903d0      0xbf8df8e0      0xbf8df938      0x001b4dec

x第二个进栈,在0xbf8df8ac

(gdb) stepi    // 注释: call   8048354

add (x=18, y=52) at stack.c:2

2       {

刚刚执行了call指令,现在我们进入了add函数

(gdb) info registers esp

esp            0xbf8df8a8       0xbf8df8a8

(gdb) info registers ebp

ebp            0xbf8df8c8       0xbf8df8c8

(gdb) x/12 0xbf8df8a0

0xbf8df8a0:     0x002daff4      0x002d9220      0x080483a3      0x00000012

0xbf8df8b0:     0x00000034      0xbf8df96c      0x00000012      0x00000034

0xbf8df8c0:     0x001903d0      0xbf8df8e0      0xbf8df938      0x001b4dec

现在esp指向0xbf8df8a8,这里保存的是add函数的返回地址,它是由call指令压栈的。

(gdb) stepi    // 注释: push   %ebp

0x08048355      2       {

(gdb) stepi    // 注释: mov    %esp,%ebp

0x08048357      2       {

(gdb) stepi    // 注释: sub    $0x10,%esp

3               int a = 0;

(gdb) info registers esp

esp            0xbf8df894       0xbf8df894

(gdb) info registers ebp

ebp            0xbf8df8a4       0xbf8df8a4

(gdb) x/16 0xbf8df890

0xbf8df890:     0x00000000      0x08049574      0xbf8df8a8      0x08048245

0xbf8df8a0:     0x002daff4      0xbf8df8c8      0x080483a3      0x00000012

0xbf8df8b0:     0x00000034      0xbf8df96c      0x00000012      0x00000034

0xbf8df8c0:     0x001903d0      0xbf8df8e0      0xbf8df938      0x001b4dec

刚刚执行完的3条指令是函数入口的定式。

现在我们可以看到,main的栈还是原样,向下增长之后就是add的栈。

0xbf8df8a4(ebp)保存的是上层函数main的帧指针

0xbf8df8a8(ebp+4)保存的是返回地址

0xbf8df8ac(ebp+8)保存的是最后一个入栈的参数x

0xbf8df8b0(ebp+C)保存的是倒数第二个入栈的参数y

0xbf8df8a0(ebp-4)保存的是局部变量a,现在是一个不确定值

接下来add函数就真正开始干活了

(gdb) stepi    // 注释: movl   $0x0,0xfffffffc(%ebp)

4               a = x;

(gdb) x/16 0xbf8df890

0xbf8df890:     0x00000000      0x08049574      0xbf8df8a8      0x08048245

0xbf8df8a0:     0x00000000      0xbf8df8c8      0x080483a3      0x00000012

0xbf8df8b0:     0x00000034      0xbf8df96c      0x00000012      0x00000034

0xbf8df8c0:     0x001903d0      0xbf8df8e0      0xbf8df938      0x001b4dec

可以看到a被置0了

(gdb) stepi    // 注释: mov    0x8(%ebp),%eax

0x08048364      4               a = x;

(gdb) stepi    // 注释: mov    %eax,0xfffffffc(%ebp)

5               a += y;

(gdb) x/16 0xbf8df890

0xbf8df890:     0x00000000      0x08049574      0xbf8df8a8      0x08048245

0xbf8df8a0:     0x00000012      0xbf8df8c8      0x080483a3      0x00000012

0xbf8df8b0:     0x00000034      0xbf8df96c      0x00000012      0x00000034

0xbf8df8c0:     0x001903d0      0xbf8df8e0      0xbf8df938      0x001b4dec

参数x(ebp+8)的值通过eax赋值给了局部变量a(ebp-4)

(gdb) stepi    // 注释: mov    0xc(%ebp),%eax

0x0804836a      5               a += y;

(gdb) stepi    // 注释: add    %eax,0xfffffffc(%ebp)

6               return a;

(gdb) x/16 0xbf8df890

0xbf8df890:     0x00000000      0x08049574      0xbf8df8a8      0x08048245

0xbf8df8a0:     0x00000046      0xbf8df8c8      0x080483a3      0x00000012

0xbf8df8b0:     0x00000034      0xbf8df96c      0x00000012      0x00000034

0xbf8df8c0:     0x001903d0      0xbf8df8e0      0xbf8df938      0x001b4dec

参数y(ebp+C)的值通过eax加到了局部变量a(ebp-4)

现在要从add返回了。返回之前把局部变量a(ebp-4)保存到eax用作返回值

(gdb) stepi    // 注释: mov    0xfffffffc(%ebp),%eax

7       }

(gdb) stepi    // 注释: leave

0x08048371 in add (x=1686688, y=134513616) at stack.c:7

7       }

(gdb) stepi    // 注释: ret

0x080483a3 in main () at stack.c:13

13              result = add(x, y);

现在我们回到了main,栈现在是这样的

(gdb) info registers esp

esp            0xbf8df8ac       0xbf8df8ac

(gdb) info registers ebp

ebp            0xbf8df8c8       0xbf8df8c8

(gdb) x/16 0xbf8df890

0xbf8df890:     0x00000000      0x08049574      0xbf8df8a8      0x08048245

0xbf8df8a0:     0x00000046      0xbf8df8c8      0x080483a3      0x00000012

0xbf8df8b0:     0x00000034      0xbf8df96c      0x00000012      0x00000034

0xbf8df8c0:     0x001903d0      0xbf8df8e0      0xbf8df938      0x001b4dec

可以看到,esp和ebp都已经恢复到了调用add之前的值。

但是,调用add的两个参数还在栈里(0xbf8df8ac、0xbf8df8b0,都在esp以上)。

也就是说,被调用的函数add没有把它们从栈上清出去,需要调用方main来清理。这就是著名的“调用者清栈”,cdecl调用方式的特点之一。

(gdb) stepi    // 注释: mov    %eax,0xfffffff8(%ebp)

14              return 0;

(gdb) x/16 0xbf8df890

0xbf8df890:     0x00000000      0x08049574      0xbf8df8a8      0x08048245

0xbf8df8a0:     0x00000046      0xbf8df8c8      0x080483a3      0x00000012

0xbf8df8b0:     0x00000034      0xbf8df96c      0x00000012      0x00000034

0xbf8df8c0:     0x00000046      0xbf8df8e0      0xbf8df938      0x001b4dec

从eax得到函数add的返回值,赋值给了局部变量result(ebp-8)

(gdb) stepi    // 注释: mov    $0x0,%eax ;把eax置0作为main的返回值

15      }

(gdb) stepi    // 注释: add    $0x18,%esp ; 调用者清栈

0x080483ae      15      }

(gdb) continue

Continuing.

Program exited normally.

(gdb) quit

[test]$

GDB查看堆栈局部变量的更多相关文章

  1. Linux 如何使用gdb 查看core堆栈信息

    转载:http://blog.csdn.net/mergerly/article/details/41994207 core dump 一般是在segmentation fault(段错误)的情况下产 ...

  2. gdb查看线程堆栈信息

    查看堆栈:gdb -quiet -batch -ex='thread apply all bt' -p pid查看运行位置:gdb -quiet -batch -ex='thread apply al ...

  3. Linux程序宕掉后如何通过gdb查看出错信息

    我们在编写服务端程序的时候,由于多线程并且环境复杂,程序可能在不确定条件的情况下宕掉,还不好重新,这是我们如何获取程序的出错信息,一种方法通过打日志,有时候一些错误日志也不能体现出来,这时就用到我们的 ...

  4. x/nfu-用gdb查看内存

    用gdb查看内存 2007-12-08 12:43 用gdb查看内存 格式: x /nfu <addr> 说明x 是 examine 的缩写 n表示要显示的内存单元的个数 f表示显示方式, ...

  5. GDB查看内存(x 命令)

    gdb查看内存命令 首先使用gdb [YourFileName].c进入gdb界面 使用examine命令,字母缩写为x查看内存地址的值.x命令语法 x/[number][format] <ad ...

  6. 一起talk GDB吧(第五回:GDB查看信息)

    各位看官们.大家好,上一回中我们说的是GDB的调用栈调试功能,而且说了怎样使用GDB进行查看调用 栈.这一回中,我们继续介绍GDB的调试功能:查看信息.当然了.我们也会介绍怎样使用GDB查看程序 执行 ...

  7. GDB查看内存命令(x命令) 用gdb查看指定地址的内存内容

    GDB查看内存命令(x命令) - super119 - 博客园 https://www.cnblogs.com/super119/archive/2011/11/18/2254382.html 可以使 ...

  8. 编程工具系列之一------使用GDB的堆栈跟踪功能

    在调试程序的过程中,查看程序的函数调用堆栈是一项最基本的任务,几乎所有的图形调试器都支持这项特性. GDB调试器当然也支持这一特性,但是功能更加灵活和丰富. GDB将当前函数的栈帧编号为0,为外层函数 ...

  9. 【Linux】GDB查看栈信息(转)

    在调试程序的过程中,查看程序的函数调用堆栈是一项最基本的任务,几乎所有的图形调试器都支持这项特性. GDB调试器当然也支持这一特性,但是功能更加灵活和丰富. GDB将当前函数的栈帧编号为0,为外层函数 ...

随机推荐

  1. Win10正式企业版激活方法

    Win10正式企业版激活方法 在正式开始激活Win10正式企业版系统之前,我们需要先查看一下当前Win10正式企业版系统的激活状态: 右击桌面左下角的“Windows”按钮,从弹出的右键菜单中选择“控 ...

  2. (转)java术语(PO/POJO/VO/BO/DAO/DTO)

    转自:http://blog.csdn.net/gaoyunpeng/article/details/2093211 PO(persistant object) 持久对象在o/r 映射的时候出现的概念 ...

  3. 查看安装的react-native和react版本

    转:http://blog.csdn.net/miss_ok/article/details/52777115 npm info React-native(目前是0.34.1) 知道最新版本后,通过以 ...

  4. 程序猿必备的10款超有趣的SVG绘制动画赏析

    SVG作为时下比较新颖的技术标准,已经建立了很多基于SVG的前端项目.由于SVG在绘制路径上非常灵活,我们将很多网页上的元素使用SVG来绘制而成,有各种人物.小图标.小动画等等.今天我们收集了10个非 ...

  5. 基于jQuery发展历程时间轴特效代码

    分享一款基于jQuery发展历程时间轴特效代码,带左右箭头,数字时间轴选项卡切换特效下载.效果图如下: 在线预览   源码下载 实现的代码. html代码: <div id="time ...

  6. SecureCRT自动上传文件python脚本

    本人在ubuntu下使用SecureCRT,上传文件习惯用rz命令.每次上传都弹对话框选择文件按确定后才上传,感觉很费力.最后自己摸索整理出一个脚本. 使用方法是,在[Script]菜单点[Run.. ...

  7. android9.0适配HTTPS:not permitted by network security policy'

    app功能接口正常,其他手机运行OK,但是在Android9.0的手机上报错 CLEARTEXT communication to 192.168.1.xx not permitted by netw ...

  8. 解决使用微软模拟器VS Emulator for Android在VS2017 Xamarin开发中不能调试程序的问题。

    在使用VS2017 XAMARIN调试Android应用程序时,屏幕闪一下,进入不了调试(使用谷歌的模拟器可以调试,但是太慢), 我们现在来解决一下这个问题. 第一步:打开Hyper-V管理器 第二步 ...

  9. 【静默】在RHEL 6.5上静默安装Oracle 18c

    [静默]在RHEL 6.5上静默安装Oracle 18c Oracle 18c.18c其实就是12.2.0.2,19c就是12.2.0.3.db_home.zip 安装包大概4.25G,解压后有8.9 ...

  10. 自动化测试工具Ranorex的录制功能使用

    由于帆软的 Report 包含gui和web端 设计器 web预览 做自动化测试不适合使用 Katalon 发现了Ranorex Ranorex 是一款在Windows操作系统的上运行的GUI自动测试 ...