Linux Debugging(一): 使用反汇编理解C++程序函数调用栈

        拿到CoreDump后,如果看到的地址都是????,那么基本上可以确定,程序的栈被破坏掉了。GDB也是使用函数的调用栈去还原“事故现场”的。因此理解函数调用栈,是使用GDB进行现场调试或者事后调试的基础,如果不理解调用栈,基本上也从GDB得不到什么有用的信息。当然了,也有可能你非常“幸运”, 一个bt就把哪儿越界给标出来了。但是,大多数的时候你不够幸运,通过log,通过简单的code walkthrough,得不到哪儿出的问题;或者说只是推测,不能确诊。我们需要通过GDB来最终确定CoreDump产生的真正原因。

       本文还可以帮助你深入理解C++函数的局部变量。我们学习时知道局部变量是是存储到栈里的,内存管理对程序员是透明的。通过本文,你将明白这些结论是如何得出的。

       栈,是LIFO(Last In First Out)的数据结构。C++的函数调用就是通过栈来传递参数,保存函数返回后下一步的执行地址。接下来我们通过一个具体的例子来探究。

int func1(int a)
{
  int b = a + 1;
  return b;
}
int func0(int a)
{
  int b = func1(a);
  return b;
}

int main()
{
  int a = 1234;
  func0(a);
  return 0;
}

可以使用以下命令将上述code编程成汇编代码:

 g++ -g -S -O0 -m32 main.cpp -o-|c++filt >main.format.s

c++filt 是为了Demangle symbols。-m32是为了编译成x86-32的。因为对于x86-64来说,函数的参数是通过寄存器传递的。

main的汇编代码:

main:
        leal    4(%esp), %ecx
        andl    $-16, %esp
        pushl   -4(%ecx)

        pushl   %ebp           #1:push %ebp指令把ebp寄存器的值压栈,同时把esp的值减4
        movl    %esp, %ebp     #2  把esp的值传送给ebp寄存器。
                               #1 + #2 合起来是把原来ebp的值保存在栈上,然后又给ebp赋了新值。
                               #2+ ebp指向栈底,而esp指向栈顶,在函数执行过程中esp
                               #2++随着压栈和出栈操作随时变化,而ebp是不动的
        pushl   %ecx
        subl    $20, %esp      #3 现在esp地址-20/4 = 5, 及留出5个地址空间给main的局部变量
        movl    $1234, -8(%ebp)#4 局部变量1234 存入ebp - 8 的地址
        movl    -8(%ebp), %eax #5 将地址存入eax
        movl    %eax, (%esp)   #6 将1234存入esp指向的地址
        call    func0(int)     #7 调用func0,注意这是demangle后的函数名,实际是一个地址
        movl    $0, %eax
        addl    $20, %esp
        popl    %ecx
        popl    %ebp
        leal    -4(%ecx), %esp
        ret

对于call指令,这个指令有两个作用:

  1. func0函数调用完之后要返回到call的下一条指令继续执行,所以把call的下一条指令的地址压栈,同时把esp的值减4。
  2. 修改程序计数器eip,跳转到func0函数的开头执行。

至此,调用func0的栈就是下面这个样子:

下面看一下func0的汇编代码:

func0(int):
        pushl   %ebp
        movl    %esp, %ebp
        subl    $20, %esp
        movl    8(%ebp), %eax
        movl    %eax, (%esp)
        call    func1(int)
        movl    %eax, -4(%ebp)
        movl    -4(%ebp), %eax
        leave
        ret

需要注意的是esp也是留了5个地址空间给func0使用。并且ebp的下一个地址就是留给局部变量b的,调用栈如图:

通过调用栈可以看出,8(%ebp)其实就是传入的参数1234。

func1的代码:

func1(int):
        pushl   %ebp
        movl    %esp, %ebp
        subl    $16, %esp
        movl    8(%ebp), %eax #去传入的参数,即1234
        addl    $1, %eax # +1 运算
        movl    %eax, -4(%ebp)
        movl    -4(%ebp), %eax #将计算结果存入eax,这就是返回值
        leave
        ret

leave指令,这个指令是函数开头的push %ebpmov %esp,%ebp的逆操作:

  1. ebp的值赋给esp
  2. 现在esp所指向的栈顶保存着foo函数栈帧的ebp,把这个值恢复给ebp,同时esp增加4。注意,现在esp指向的是这次调用的返回地址,即上次调用的下一条执行指令。

最后是ret指令,它是call指令的逆操作:

  1. 现在esp所指向的栈顶保存着返回地址,把这个值恢复给eip,同时esp增加4,esp指向了当前frame的栈顶
  2. 修改了程序计数器eip,因此跳转到返回地址继续执行。

调用栈如下:

至此,func1返回后,控制权交还给func0,当前的栈就退化成func0的栈的情况,因为栈保存了一切信息,因此指令继续执行。直至func0执行

leave

ret

以同样的方式将控制权交回给main。

     到这里,你应该知道下面问题的答案了:

1. 局部变量的生命周期,

2. 局部变量是怎么样使用内存的;

3. 为什么传值不会改变原值(因为编译器已经帮你做好拷贝了)

4. 为什么会有栈溢出的错误

5. 为什么有的写坏栈的程序可以运行,而有的却会crash(如果栈被破坏的是数据,那么数据是脏的,不应该继续运行;如果破坏的是上一层调用的bp,或者返回地址,那么程序会crash,or unexpected behaviour...)

    小节一下:

   1. 在32位的机器上,C++的函数调用的参数是存到栈上的。当然gcc可以在函数声明中添加_attribute__((regparm(3)))使用eax, edx,ecx传递开头三个参数。

   2. 通过bp可以访问到调用的参数值。

   3. 函数的返回地址(函数返回后的执行指令)也是存到栈上的,有目的的修改它可以使程序跳转到它不应该的地方。。。

   4. 如果程序破坏了上一层的bp的地址,或者程序的返回地址,那么程序就很有可能crash

   5. 拿到一个CoreDump,应该首先先看有可能出问题的线程的的frame的栈是否完整。

   6. 64位的机器上,参数是通过寄存器传递的,当然寄存器不够用就会通过栈来传递

支持原创,转载请注明出处:anzhsoft  http://blog.csdn.net/anzhsoft/article/details/18730605

时间: 2024-09-20 00:14:21

Linux Debugging(一): 使用反汇编理解C++程序函数调用栈的相关文章

Linux Debugging(三): C++函数调用的参数传递方法总结(通过gdb+反汇编)

         上一篇文章<Linux Debugging:使用反汇编理解C++程序函数调用栈>没想到能得到那么多人的喜爱,因为那篇文章是以32位的C++普通函数(非类成员函数)为例子写的,因此只是一个特殊的例子.本文将函数调用时的参数传递方法进行一下总结.总结将为C++普通函数.类成员函数:32位和64位进行总结.         建议还是读一下Linux Debugging:使用反汇编理解C++程序函数调用栈,这样本文的结论将非常容易理解,将非常好的为CoreDump分析开一个好头.而且

Linux Debugging(二): 熟悉AT&amp;amp;T汇编语言

    没想到<Linux Debugging:使用反汇编理解C++程序函数调用栈>发表了收到了大家的欢迎.但是有网友留言说不熟悉汇编,因此本书列了汇编的基础语法.这些对于我们平时的调试应该是够用了. 1 AT&T与Intel汇编语法对比     本科时候大家学的基本上都是Intel的8086汇编语言,微软采用的就是这种格式的汇编.GCC采用的是AT&T的汇编格式, 也叫GAS格式(Gnu ASembler GNU汇编器). 1.寄存器命名不同 AT&T Intel 说

Linux Debugging(五): coredump 分析入门

        作为工作几年的老程序猿,肯定会遇到coredump,log severity设置的比较高,导致可用的log无法分析问题所在. 更悲剧的是,这个问题不好复现!所以现在你手头唯一的线索就是这个程序的尸体:coredump.你不得不通过它,来寻找问题根源.       通过上几篇文章,我们知道了函数参数是如何传递的,和函数调用时栈是如何变化的:当然了还有AT&T的汇编基础,这些,已经可以使我们具备了一定的调试基础.其实,很多调试还是需要经验+感觉的.当然说这句话可能会被打.但是你不得不

如何在64位版本Linux上开发运行32位应用程序

最近换了Linux系统,由i686换成了x86-64,导致在进行开发的时候出用不了原来SDK中32位的开发工具.于是,博主找到如下文章,博主亲测实用: 如何在64位版本Linux上开发运行32位应用程序  内容如下: 很多程序员(特别是别的公司的)跟我抱怨说他们32位软件无法在我们的64位Linux系统上正常运行,而在他们32位机上正常,其实这个很好解决,一般 是64位系统安装后没有默认安装glibc的32位版本,通过简单的执行以下命令即可实现在64位Linux系统上开发运行32位应用程序,而不

脚本-linux AWK报越界错误,但是程序能正常运行,为啥?

问题描述 linux AWK报越界错误,但是程序能正常运行,为啥? 在shell脚本中: arr=(awk 'NR==2{for(i=1;i<=$infoo1;i++)printf $i" "}' $1) // 我用awk读取一个文本的第二行,并将每个域的元素依次赋值给数组arr[],$1是 shell从命令行传入的参数,即我读取的文本的路径,这个文本很简单,第二行内容如下: p.jpg-1 p.jpg-2 p.jpg-3 p.jpg-4 p.jpg-5 p.jpg-6 p.jp

Linux 下编译c++ opengl的小程序

问题描述 Linux 下编译c++ opengl的小程序 完整的代码如下 #include <GL/glut.h> #include <cmath> #include <utility> #include <vector> using std::vector; using std::pair; vector<pair<int, int> > my_points(4); vector<pair<int, int> &g

linux作为防火墙,编一个小程序,实现我选择一个ip , 就显示这个ip的上网历史记录! 大家说说吧!linux作为防火墙

问题描述 linux作为防火墙,编一个小程序,实现我选择一个ip,就显示这个ip的上网历史记录!大家说说吧!要是非要安装软件的话,最好是linux自带的或是开源的.

基础才是重中之重~理解内存中的栈和堆

.NET中使用stack(栈)和heap(堆)两种结构在内存中存储数据,今天咱们就来说说这两个结构 Value Types,值类型      在C#中,值类型继承自System.ValueType的,它们分别是       Bool,   byte ,  char, decimal, double, enu, float, int, long, sbyte, short, struct, uint, ulong, ushort Reference Types 引用类型     引用类型包括所有的

printf-新编得程序函数调用总出错,求大神帮忙。。

问题描述 新编得程序函数调用总出错,求大神帮忙.. #include int my_str_len(void); int print_char_array_values(void); int char_array_reverse(void); int main(void){ char arr[30]; int i; char e,n,d; while(3){ puts("Please provide a character string,don't type more than 30 eleme