这段C代码是怎么工作的?

一、实验内容

  根据所给example.c代码分别生成.cpp,.s,.o和ELF可执行文件,并加载运行,分析.s汇编代码在CPU上的执行过程。通过实验理解计算机的工作机理。

  example.c代码如下

  
 1 int g(int x)
 2 {
 3     return x+3;
 4 }
 5 
 6 int f(int x)
 7 {
 8     return g(x);
 9 }
10 
11 int main(void)
12 {
13     return f(8)+1;
14 }
View example.c

、实验过程 

  1.首先生成预编译文件、汇编文件和ELF可执行文件,见图1所示。

   

                                                                     图1

  2.本文的目标主要是简单理解一下底层汇编的执行过程,上面生成的example.s代码摘抄如下

     
 1     .file    "example.c"
 2     .text
 3     .globl    g
 4     .type    g, @function
 5 g:
 6 .LFB0:
 7     .cfi_startproc
 8     pushl    %ebp
 9     .cfi_def_cfa_offset 8
10     .cfi_offset 5, -8
11     movl    %esp, %ebp
12     .cfi_def_cfa_register 5
13     movl    8(%ebp), %eax
14     addl    $3, %eax
15     popl    %ebp
16     .cfi_def_cfa 4, 4
17     .cfi_restore 5
18     ret
19     .cfi_endproc
20 .LFE0:
21     .size    g, .-g
22     .globl    f
23     .type    f, @function
24 f:
25 .LFB1:
26     .cfi_startproc
27     pushl    %ebp
28     .cfi_def_cfa_offset 8
29     .cfi_offset 5, -8
30     movl    %esp, %ebp
31     .cfi_def_cfa_register 5
32     subl    $4, %esp
33     movl    8(%ebp), %eax
34     movl    %eax, (%esp)
35     call    g
36     leave
37     .cfi_restore 5
38     .cfi_def_cfa 4, 4
39     ret
40     .cfi_endproc
41 .LFE1:
42     .size    f, .-f
43     .globl    main
44     .type    main, @function
45 main:
46 .LFB2:
47     .cfi_startproc
48     pushl    %ebp
49     .cfi_def_cfa_offset 8
50     .cfi_offset 5, -8
51     movl    %esp, %ebp
52     .cfi_def_cfa_register 5
53     subl    $4, %esp
54     movl    $8, (%esp)
55     call    f
56     addl    $1, %eax
57     leave
58     .cfi_restore 5
59     .cfi_def_cfa 4, 4
60     ret
61     .cfi_endproc
62 .LFE2:
63     .size    main, .-main
64     .ident    "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
65     .section    .note.GNU-stack,"",@progbits
View example.s

    先介绍几个寄存器以方便下面的阐述。ebp和esp是两个指针寄存器,用来指向当前所用栈的底部和顶部;eip指令寄存器是用来记录的是CPU将要执行的下  一条指令地址,也就是说CPU接下来要执行哪一条指令是通过eip来指引的;eax数据寄存器通常用来保存API的返回值,由于操作的效率比较高,因而使用      的频率也比较高。     

      从main()函数开始分析

                pushl  %ebp

                movl  %esp, %ebp   

        刚开始栈顶指针esp1指向初始位置,然后将之前的栈底指针ebp1入栈并且新的ebp和esp同时指向了地址减少4字节后的位置即图中所示           的esp2/ebp2,很明显这一步的作用就是保存之前栈环境的前提下建立新的堆栈框架,堆栈变化如图2所示。

                                            图2

               subl    $4, %esp

               movl   $8, (%esp)

        将esp向低地址增长4字节并将8入栈esp指向途中esp3所指位置,堆栈变化如图3所示。

                                    图3

        call     f

        CPU执行CALL指令时通常做两步操作,第一步将EIP压栈做保存;第二部跳转,堆栈变化如图4所示。

      

                                    图4

        将当前eip入栈并跳转到f中去。跳到f后做的第一件事儿仍然是

                pushl  %ebp

                movl  %esp, %ebp

        前面已经说过这是保存之前栈环境的前提下建立新的堆栈框架,堆栈变化如图5所示

                                   图5

         subl    $4, %esp

             movl   8(%ebp), %eax

                  movl   %eax, (%esp)

        在此过程中将8(%ebp)的参数传入eax中,并将eax入栈,堆栈变化见图6所示

    

                                   图6

         call     g

         调用g函数,堆栈变化见图7

                                   图7

        来到g中不变的首先做新堆栈框架搭建,堆栈变化见图8

                  pushl  %ebp

                movl  %esp, %ebp

                                  图8

            movl   8(%ebp), %eax

            addl    $3, %eax

            popl    %ebp

        ret

        在此过程中,将之前保存的变量再次转移到eax中,并将eas加3,实际上此事eas就为11了。然后出栈一次,ebp重归于ebp3,esp重归于esp7,堆栈变化见图9

                                   图9

        好了,现在又重新回到函数f中了   

        leave

        宏指令leave包括

                movl   %ebp, %esp

              popl   %ebp

        这两条指令,实际上就是刚进函数时建堆栈的逆过程,堆栈变化见图10

                      

                                   图10

        ret

        回到main()函数中,堆栈变化如图11所示

                                   图11

        addl    $1, %eax

        将eax中的数再次给加1

        leave

        ret

        最后一夜回到解放前,重回程序开始的地方,堆栈变化见图12

                                    图12

、实验总结

  上面的分析是对函数调用做了堆栈的分析。实际上计算机的工作原理就是如此,在调用其他函数时先将数据压栈做现场保护,接着去处理被调用函数,被调用函数还有可能调用其他函数,这时候同样需要做现场保护。如我在前面说的,call指令首先做现场保护(将EIP压栈等)然后转移到被调用函数。最后由leave指令做拆除被调用堆栈环境工作并由ret返回。其最核心的思想便是栈了,因此认真的写一篇博文再仔细体会一下栈操作的细节还是很有必要的。

posted @ 2013-05-15 00:43  zhouyoulie  阅读(539)  评论(0编辑  收藏  举报