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存入父栈栈顶,便于恢复到原节点处继续执行。
这样,就可以有规律的一直嵌套下去。
如果使用递归函数,就是一个码堆栈的过程,知道最顶部的堆栈返回,函数就像多米诺骨牌一样收回所有的堆栈。
这也是递归函数占用空间比较多的原因之一。如果没有很好地退出机制,有可能内存溢出。