注:在linux下开发经常使用的辅助小工具:

readelf 、hexdump、od、objdump、nm、telnet、nc 等,详细能够man一下。

我们用以下的C代码来研究函数调用的过程。

 C++ Code 
1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16
int bar(int c, int d)

{

    int e = c + d;

    return e;

}



int foo(int a, int b)

{

    return bar(a, b);

}



int main(void)

{

    foo(, );

    ;

}

假设在编译时加上-g选项,那么用objdump反汇编时能够把C代码和汇编代码穿插起来显示,这样C代码和汇编代码的相应关系看得更清楚。反汇编的结果非常长,下面仅仅列出我们关心的部分。

simba@ubuntu:~/Documents/code/asm$ objdump -dS a.out

 ASM Code 
1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50
int bar(int c, int d)



 80483dc:                             push   %ebp

 80483dd:        e5                   mov    %esp,%ebp

 80483df:        ec                 sub    $0x10,%esp

        int e = c + d; 

 80483e2:       8b  0c                mov    0xc(%ebp),%eax

 80483e5:       8b                  mov    0x8(%ebp),%edx

 80483e8:        d0                   add    %edx,%eax

 80483ea:         fc                mov    %eax,-0x4(%ebp)

        return e;

 80483ed:       8b  fc                mov    -0x4(%ebp),%eax

}

 80483f0:       c9                      leave  

 80483f1:       c3                      ret    



080483f2 <foo>:



int foo(int a, int b)



 80483f2:                             push   %ebp

 80483f3:        e5                   mov    %esp,%ebp

 80483f5:        ec                 sub    $0x8,%esp

        return bar(a, b);

 80483f8:       8b  0c                mov    0xc(%ebp),%eax

 80483fb:                       mov    %eax,0x4(%esp)

 80483ff:       8b                  mov    0x8(%ebp),%eax

 :                         mov    %eax,(%esp)

 :       e8 d2 ff ff ff          call   80483dc <bar>

}

 804840a:       c9                      leave  

 804840b:       c3                      ret    



0804840c <main>:



int main(void)



 804840c:                             push   %ebp

 804840d:        e5                   mov    %esp,%ebp

 804840f:        ec                 sub    $0x8,%esp

        foo(, ); 

 :       c7          movl   $0x3,0x4(%esp)

 :        

 804841a:       c7          movl   $0x2,(%esp)

 :       e8 cc ff ff ff          call   80483f2 <foo>

        return ;

 :       b8              mov    $0x0,%eax

}

 804842b:       c9                      leave  

 804842c:       c3                      ret 

要查看编译后的汇编代码,事实上另一种办法是gcc -S main.c,这样仅仅生成汇编代码main.s,而不生成二进制的目标文件。

整个程序的运行过程是main调用foo,foo调用bar,我们用gdb跟踪程序的运行,直到bar函数中的int e = c + d;语句运行完成准备返回时,这时在gdb中打印函数栈帧,由于此时栈已经生长到最大。

simba@ubuntu:~/Documents/code/asm$ gdb a.out

GNU gdb (GDB) 7.5-ubuntu

Copyright (C) 2012 Free Software Foundation, Inc.

License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software: you are free to change and redistribute it.

There is NO WARRANTY, to the extent permitted by law.  Type "show copying"

and "show warranty" for details.

This GDB was configured as "i686-linux-gnu".

For bug reporting instructions, please see:

<http://www.gnu.org/software/gdb/bugs/>...

Reading symbols from /home/simba/Documents/code/asm/a.out...done.

(gdb) start

Temporary breakpoint 1 at 0x8048412: file foo_bar.c, line 22.

Starting program: /home/simba/Documents/code/asm/a.out 





Temporary breakpoint 1, main () at foo_bar.c:22

22              foo(2, 3); 

(gdb) s

foo (a=2, b=3) at foo_bar.c:17

17              return bar(a, b);

(gdb) s

bar (c=2, d=3) at foo_bar.c:11

11              int e = c + d; 

(gdb) disas

Dump of assembler code for function bar:

   0x080483dc <+0>:     push   %ebp

   0x080483dd <+1>:     mov    %esp,%ebp

   0x080483df <+3>:     sub    $0x10,%esp

=> 0x080483e2 <+6>:     mov    0xc(%ebp),%eax

   0x080483e5 <+9>:     mov    0x8(%ebp),%edx

   0x080483e8 <+12>:    add    %edx,%eax

   0x080483ea <+14>:    mov    %eax,-0x4(%ebp)

   0x080483ed <+17>:    mov    -0x4(%ebp),%eax

   0x080483f0 <+20>:    leave  

   0x080483f1 <+21>:    ret    

End of assembler dump.

(gdb) si

0x080483e5      11              int e = c + d; 

(gdb) 

0x080483e8      11              int e = c + d; 

(gdb) 

0x080483ea      11              int e = c + d; 

(gdb) 

12              return e;

(gdb) 

13      }

(gdb) bt

#0  bar (c=2, d=3) at foo_bar.c:13

#1  0x0804840a in foo (a=2, b=3) at foo_bar.c:17

#2  0x08048426 in main () at foo_bar.c:22

(gdb) info registers

eax            0x5      5

ecx            0xbffff744       -1073744060

edx            0x2      2

ebx            0xb7fc6000       -1208197120

esp            0xbffff678       0xbffff678

ebp            0xbffff688       0xbffff688

esi            0x0      0

edi            0x0      0

eip            0x80483f0        0x80483f0 <bar+20>

eflags         0x206    [ PF IF ]

cs             0x73     115

ss             0x7b     123

ds             0x7b     123

es             0x7b     123

fs             0x0      0

gs             0x33     51

(gdb) x/20x $esp

0xbffff678:     0x0804a000      0x08048482      0x00000001      0x00000005

0xbffff688:     0xbffff698      0x0804840a      0x00000002      0x00000003

0xbffff698:     0xbffff6a8      0x08048426      0x00000002      0x00000003

0xbffff6a8:     0x00000000      0xb7e394d3      0x00000001      0xbffff744

0xbffff6b8:     0xbffff74c      0xb7fdc858      0x00000000      0xbffff71c

在运行程序时,操作系统为进程分配一块栈空间来保存函数栈帧,esp寄存器总是指向栈顶,在x86平台上这个栈是从高地址向低地址增长的,我们知道每次调用一个函数都要分配一个栈帧来保存參数和局部变量,如今我们具体分析这些数据在栈空间的布局,依据gdb的输出结果图演示样例如以下:

图中每一个小方格表示4个字节的内存单元,比如b: 3这个小方格占的内存地址是0xbffff6a4~0xbffff6a8,我把地址写在每一个小方格的下边界线上,是为了强调该地址是内存单元的起始地址。我们从main函数的这里開始看起:

 ASM Code 
1

2

3

4

5
foo(, ); 

 :       c7          movl   $0x3,0x4(%esp)

 :        

 804841a:       c7          movl   $0x2,(%esp)

 :       e8 cc ff ff ff          call   80483f2 <foo>

要调用函数foo先要把參数准备好,第二个參数保存在esp+4指向的内存位置,第一个參数保存在esp指向的内存位置,可见參数是从右向左依次压栈的。然后运行call指令,这个指令有两个作用:

1. foo函数调用完之后要返回到call的下一条指令继续运行,所以把call的下一条指令的地址0x8048426压栈,同一时候把esp的值减4,esp的值如今是0xbffff69c(能够在main函数開始运行时info r 一下,此时esp为0xbffff6a0)。

2. 改动程序计数器eip,跳转到foo函数的开头运行。

如今看foo函数的汇编代码:

 ASM Code 
1

2

3

4

5

6

7

8

9

10

11

12
int foo(int a, int b)



 80483f2:                             push   %ebp

 80483f3:        e5                   mov    %esp,%ebp

 80483f5:        ec                 sub    $0x8,%esp

        return bar(a, b);

 80483f8:       8b  0c                mov    0xc(%ebp),%eax

 80483fb:                       mov    %eax,0x4(%esp)

 80483ff:       8b                  mov    0x8(%ebp),%eax

 :                         mov    %eax,(%esp)

 :       e8 d2 ff ff ff          call   80483dc <bar>

}

push %ebp指令把ebp寄存器的值压栈,同一时候把esp的值减4。esp的值如今是0xbffff698,下一条指令把这个值传送给ebp寄存器。这两条指令合起来是把原来ebp的值保存在栈上,然后又给ebp赋了新值。在每一个函数的栈帧中,ebp指向栈底,而esp指向栈顶,在函数运行过程中esp随着压栈和出栈操作随时变化,而ebp是不动的,函数的參数和局部变量都是通过ebp的值加上一个偏移量来訪问,比如foo函数的參数a和b分别通过ebp+8和ebp+12来訪问。所以以下的指令把參数a和b再次压栈,为调用bar函数做准备,然后把返回地址压栈,调用bar函数:

如今看bar函数的指令:

 ASM Code 
1

2

3

4

5

6

7

8

9

10

11

12
 

int bar(int c, int d)



 80483dc:                             push   %ebp

 80483dd:        e5                   mov    %esp,%ebp

 80483df:        ec                 sub    $0x10,%esp

        int e = c + d; 

 80483e2:       8b  0c                mov    0xc(%ebp),%eax

 80483e5:       8b                  mov    0x8(%ebp),%edx

 80483e8:        d0                   add    %edx,%eax

 80483ea:         fc                mov    %eax,-0x4(%ebp)

  

这次又把foo函数的ebp压栈保存,然后给ebp赋了新值,指向bar函数栈帧的栈底,通过ebp+8和ebp+12分别能够訪问參数c和d。bar函数另一个局部变量e,能够通过ebp-4来訪问。所以后面几条指令的意思是把參数c和d取出来存在寄存器中做加法,计算结果保存在eax寄存器中,再把eax寄存器存回局部变量e的内存单元。

在gdb中能够用bt命令和frame命令查看每层栈帧上的參数和局部变量,如今能够解释它的工作原理了:假设我当前在bar函数中,我能够通过ebp找到bar函数的參数和局部变量,也能够找到foo函数的ebp保存在栈上的值,有了foo函数的ebp,又能够找到它的參数和局部变量,也能够找到main函数的ebp保存在栈上的值,因此各层函数栈帧通过保存在栈上的ebp的值串起来了。

如今看bar函数的返回指令:

 ASM Code 
1

2

3

4

5

6
      return e;

 80483ed:       8b  fc                mov    -0x4(%ebp),%eax

}

 80483f0:       c9                      leave  

 80483f1:       c3                      ret    

bar函数有一个int型的返回值,这个返回值是通过eax寄存器传递的,所以首先把e的值读到eax寄存器中。

然后运行leave指令,这个指令是函数开头的push %ebp和mov %esp,%ebp的逆操作:

1. 把ebp的值赋给esp,如今esp的值是0xbffff688。

2. 如今esp所指向的栈顶保存着foo函数栈帧的ebp,把这个值恢复给ebp,同一时候esp添加4,esp的值变成0xbffff68c。

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

1. 如今esp所指向的栈顶保存着返回地址,把这个值恢复给eip(pop),同一时候esp添加4,esp的值变成0xbffff690。

2. 改动了程序计数器eip,因此跳转到返回地址0x804840a继续运行。

地址0x804840a处是foo函数的返回指令:

 ASM Code 
1

2

3
 804840a:       c9                      leave  

 804840b:       c3                      ret    

反复相同的过程,又返回到了main函数。

依据上面的分析,ebp终于会又一次获取值0x00000000, 而从main函数返回到0xb7e39473地址去运行,终于esp值为0xbffff6b0。

当main函数最后一条指令运行完是info r 一下能够发现:

esp            0xbffff6b0       0xbffff6b0

ebp            0x0      0x0

实际上回过头发现main函数最開始也有初始化的3条汇编指令,先把ebp压栈,此时esp减4为0x6ffffba8,再将esp赋值给ebp,最后将esp减去8,所以在我们调试第一条执行的指令(movl   $0x3,0x4(%esp) )时,esp已经是0x6ffff6a0,与前面对比发现是吻合的。那么main函数回到哪里去执行呢?实际上main函数也是被其它系统函数所调用的,比方进一步si
下去会发现 是 被 libc-start.c 所调用,终于还会调用exit.c。为了从main函数入口就開始调试,能够设置一个断点例如以下:

(gdb) disas main

Dump of assembler code for function main:

   0x0804840c <+0>:     push   %ebp

   0x0804840d <+1>:     mov    %esp,%ebp

   0x0804840f <+3>:     sub    $0x8,%esp

   0x08048412 <+6>:     movl   $0x3,0x4(%esp)

   0x0804841a <+14>:    movl   $0x2,(%esp)

   0x08048421 <+21>:    call   0x80483f2 <foo>

   0x08048426 <+26>:    mov    $0x0,%eax

   0x0804842b <+31>:    leave  

   0x0804842c <+32>:    ret    

End of assembler dump.

(gdb) b *0x0804840c

Breakpoint 1 at 0x804840c: file foo_bar.c, line 21.

(gdb) r

Starting program: /home/simba/Documents/code/asm/a.out 





Breakpoint 1, main () at foo_bar.c:21

21      { 

(gdb) i reg

eax            0x1      1

ecx            0xbffff744       -1073744060

edx            0xbffff6d4       -1073744172

ebx            0xb7fc6000       -1208197120

esp            0xbffff6ac       0xbffff6ac

ebp            0x0      0x0

esi            0x0      0

edi            0x0      0

eip            0x804840c        0x804840c <main>

eflags         0x246    [ PF ZF IF ]

cs             0x73     115

ss             0x7b     123

ds             0x7b     123

es             0x7b     123

fs             0x0      0

gs             0x33     51

(gdb) x/x $esp

0xbffff6ac:     0xb7e394d3

(gdb) x/10i 0xb7e394d3-10

   0xb7e394c9 <__libc_start_main+233>:  inc    %esp

   0xb7e394ca <__libc_start_main+234>:  and    $0x74,%al

   0xb7e394cc <__libc_start_main+236>:  mov    %eax,(%esp)

   0xb7e394cf <__libc_start_main+239>:  call   *0x70(%esp)

   0xb7e394d3 <__libc_start_main+243>:  mov    %eax,(%esp)

   0xb7e394d6 <__libc_start_main+246>:  call   0xb7e52fb0 <__GI_exit>

   0xb7e394db <__libc_start_main+251>:  xor    %ecx,%ecx

   0xb7e394dd <__libc_start_main+253>:  jmp    0xb7e39414 <__libc_start_main+52>

   0xb7e394e2 <__libc_start_main+258>:  mov    0x3928(%ebx),%eax

   0xb7e394e8 <__libc_start_main+264>:  ror    $0x9,%eax

(gdb) x/x $esp+4+0x70

0xbffff720:     0x0804840c

能够看到main函数最開始时,esp为0xbffff6ac,ebp为0,eip为0x804840c,esp所指的0xb7e394d3就是main函数运行完的返回地址,怎样证明呢?

能够看到0xb7e394cf 处的指令 call *0x70(%esp) ,即将下一条地址压栈,打印一下 esp+4+0x70 指向的地址为0x804840c,也就是main函数的入口地

址。此外能够看到调用call 时esp 应该为0xbffff6b0,与main 函数运行完成时的esp 值一致。

知道了main函数的返回地址,我们也就明确了所谓的shellcode的大概实现原理,利用栈空间变量的缓冲区溢出将返回地址覆盖掉,将esp所指返回地址pop到eip时,就会改变程序的流程,不再是正确地退出,而是被我们所控制了,通常是跳转到一段shellcode(机器指令)的起始地址,这样就启动了一个shell。

注意函数调用和返回过程中的这些规则:

1. 參数压栈传递,而且是从右向左依次压栈。

2. ebp总是指向当前栈帧的栈底。

3. 返回值通过eax寄存器传递。

这些规则并非体系结构所强加的,ebp寄存器并非必须这么用,函数的參数和返回值也不是必须这么传,仅仅是操作系统和编译器选择了以这种方式实现C代码中的函数调用,这称为Calling Convention,Calling Convention是操作系统二进制接口规范(ABI,Application Binary Interface)的一部分。

參考:《linux c 编程一站式学习》

从汇编角度来理解linux下多层函数调用堆栈执行状态的更多相关文章

  1. 从汇编角度来理解linux下多层函数调用堆栈运行状态

    我们用下面的C代码来研究函数调用的过程.  C++ Code  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16   int bar(int c, int d) {     ...

  2. 从需求的角度去理解Linux系列:总线、设备和驱动

    笔者成为博客专家后整理以前原创的嵌入式Linux系列博文,现推出以让更多的读者受益. <从需求的角度去理解linux系列:总线.设备和驱动>是一篇有关如何学习嵌入式Linux系统的方法论文 ...

  3. 深入理解LINUX下动态库链接器/加载器ld-linux.so.2

    [ld-linux-x86-64.so.2] 最近在Linux 环境下开发,搞了好几天 Compiler 和 linker,觉得有必要来写一篇关于Linux环境下 ld.so的文章了,google上搜 ...

  4. 从操作系统层面理解Linux下的网络IO模型

    I/O( INPUT OUTPUT),包括文件I/O.网络I/O. 计算机世界里的速度鄙视: 内存读数据:纳秒级别. 千兆网卡读数据:微妙级别.1微秒=1000纳秒,网卡比内存慢了千倍. 磁盘读数据: ...

  5. 理解linux下的load

    我们在做Linux负载计算的时候,我们需要了解负载的几个概念 1)Linux负载是什么 2)Linux负载怎么计算 3)如何区分目前负载是“好”还是“坏” 4)什么时候应该注意哪些不正常的值   1) ...

  6. Linux下追踪函数调用,打印栈帧

    事情的起因是这样的,之前同事的代码有一个内存池出现了没有回收的情况.也就是是Pop出来的对象没有Push回去,情况很难复现,所以在Pop里的打印日志,跟踪是谁调用了它,我想在GDB调试里可以追踪调用的 ...

  7. 理解linux下源码、yum和rpm安装方法的特点

    1.yum可看作在线安装,只需yum install 软件名,系统就自动根据yum源配置文件中的镜像位置去下载安装包,并可以自动分析所需的软件依赖关系,自动安装所需的依赖软件包.简单方便,不易出错,不 ...

  8. Linux下获取java堆栈文件并进行分析

    当服务器内存飙升或者cpu负载飙升的时候,可以使用如下步骤排查问题: 1.终端输入top命令,键盘大写的情况下按P(cpu负载率从高到低排序)或者M(内存使用率从高到低排序),可以查看导致cpu或者内 ...

  9. 在Linux中打印函数调用堆栈【原创】

    本人学习笔记,代码参考如下网址 参考http://www.cnblogs.com/dma1982/archive/2012/02/08/2342215.html zhangbh@prolin-srv: ...

随机推荐

  1. BZOJ 1231: [Usaco2008 Nov]mixup2 混乱的奶牛( dp )

    状压dp dp( x , S ) 表示最后一个是 x , 当前选的奶牛集合为 S , 则状态转移方程 : dp( x , S ) =  Σ dp( i , S - { i } )  ( i ∈ S , ...

  2. Ecmall系统自带的分页功能使用

    在控制器如果没有定义相关模型,直接使用sql语句的话,直接使用如下语句. 即: public $db; $this->db = &db(); //然后开始使用分页类 $sql='sele ...

  3. 深入浅出—JAVA(4)

    4.方法操作实例变量

  4. Ubuntu下配置修改IP地址

    一.使用命令设置Ubuntu IP地址 1.修改配置文件blacklist.conf禁用IPV6:sudo vi /etc/modprobe.d/blacklist.conf 2.在文档最后添加 bl ...

  5. Week15(12月19日):授课综述2

    Part I:提问 =========================== 1.为了编辑应用程序的统一布局,可打开位于Views\Shared子目录中的(    )文件. A.MasterPage.h ...

  6. Mylyn

    Mylyn(旧称Mylar)是eclipse的一个插件,用于将任务管理和上下文管理无缝集成到Eclipse中.1. 安装 下载相应的Mylyn zip包,解压缩开就是两个文件夹:features和pl ...

  7. HDU_1003Max Sum 简单动归

    以前做过这道题目,那是还不懂状态方程.乱搞一气: #include<cstdio> #include<algorithm> using namespace std; +; in ...

  8. UVA 270 Lining Up 共线点 暴力

    题意:给出几个点的位置,问一条直线最多能连过几个点. 只要枚举每两个点组成的直线,然后找直线上的点数,更新最大值即可. 我这样做过于暴力,2.7s让人心惊肉跳...应该还能继续剪枝的,同一直线找过之后 ...

  9. Linux 开放服务端口

    CentOS 6.5上安装Tomcat 服务器,需要开放服务端口,供其他计算机访问部署在Tomcat中的Web应用.下面是开放端口的方法. 我知道的方法有两种.下面以开放8080端口为例. 方法一:命 ...

  10. 用Flask实现视频数据流传输

    Flask 是一个 Python 实现的 Web 开发微框架.这篇文章是一个讲述如何用它实现传送视频数据流的详细教程. 我敢肯定,现在你已经知道我在O’Reilly Media上发布了有关Flask的 ...