C程序汇编运行模式简析,汇编模式简析


SJTUBEAR 原创作品转载请注明出处 /《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

 

1. 汇编

        在修习LINUX内核这门课的初始阶段,首先需要掌握的就是汇编以及汇编程序对于堆栈的操作。

        下面我们就来分析一下一个简单地C程序是如何被汇编程序所表达的!

2. 得到汇编代码

        首先,我们写一个简单地C程序,命名为exp1.c:

 1 #include <stdio.h>
 2 
 3 int g(int x)
 4 {
 5     return x+3;
 6 }
 7 
 8 int f(x)
 9 {
10     return g(x);
11 }
12 
13 int main()
14 {
15     return f(8)+1;    
16 }

      程序非常的简单,我们此时再通过编译指令将其编译为汇编程序:

1 gcc –S –o main.s main.c -m32

      这样我们就得到了这个简单C程序的汇编代码:

 1     .file    "exp1.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

3.汇编代码分析

    汇编出的代码,多了很多辅助信息,为了能够更好地看清主干,我们删减一下:

 1 g:
 2     pushl    %ebp            //保存现场,将父函数的栈底寄存器存入当前程序栈中
 3     movl    %esp, %ebp      //构建当前函数堆栈
 4     movl    8(%ebp), %eax   //从父函数堆栈中取得参数,存入ax寄存器
 5     addl    $3, %eax        //完成+3操作
 6     popl    %ebp            //恢复原父函数堆栈
 7     ret                     //pop出原EIP地址,恢复执行
 8 f:
 9     pushl    %ebp            //保存现场,将父函数的栈底寄存器存入当前程序栈中
10     movl    %esp, %ebp      //构建当前函数堆栈
11     subl    $4, %esp        //栈顶加一,用以储存变量传递给g函数
12     movl    8(%ebp), %eax   //取得参数
13     movl    %eax, (%esp)    //将参数传入变量位置
14     call    g               //调用g
15     leave                   //清楚局部变量空间
16     ret                     //返回
17 main:
18     pushl    %ebp
19     movl    %esp, %ebp
20     subl    $4, %esp        //空出局部变量空间
21     movl    $8, (%esp)      //为变量赋值
22     call    f               //调用f
23     addl    $1, %eax        //完成+1操作
24     leave                   //清理局部变量
25     ret                     //返回

我们对f函数进行详细的分析:

 1. 首先进行enter指令:

      此时,ebp当前所指向的位置存入栈顶,并且将ebp重定向指向esp:

     

    2.栈顶加一并存入变量值:

         

    3.调用g

  

    4.从g返回后,返回值储存在AX寄存器中,不用操作,调用leave,清理变量

    

  5.最后ret,同时EIP被读出恢复到原位置继续执行,返回值在AX中传递给调用函数

       

3.个人的一点感悟:

     程序的调用就是这样嵌套的执行下去,每个函数都有自己的堆栈用以储存当前变量以及环境值,并通过将父函数的EBP放入栈底用以恢复环境。

     同时EIP存入父栈栈顶,便于恢复到原节点处继续执行。

     这样,就可以有规律的一直嵌套下去。

     如果使用递归函数,就是一个码堆栈的过程,知道最顶部的堆栈返回,函数就像多米诺骨牌一样收回所有的堆栈。

     这也是递归函数占用空间比较多的原因之一。如果没有很好地退出机制,有可能内存溢出。

相关内容