2015年(65)
分类: LINUX
2015-03-26 10:49:22
最近网易云课堂开放了一节叫 的课程。一直对操作系统和计算机本质很感兴趣,于是进去看了下,才第一堂课,老师就要求学生写一篇关于课时1的博客作为作业。对于这种新颖的作业形式,笔 者相当惊讶。好吧,作为任务,还是完成一下吧,刚好需要消化一下。本文将会按照要求,将一段C语言代码编译成汇编,并给予分析和自己的思考。
首先对会涉及到的一些CPU寄存器和汇编的基础知识罗列一下:
准备一段C代码:
int g(int x) { return x+5; } int f(int x) { return g(x); } int main(void) { return f(10)+1; }
使用 环境
使用如下编译上面的c代码
gcc -S -o main.s main.c -m32
去掉不重要的部分后,得到:
汇编代码结果为:
g: pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax addl $5, %eax popl %ebp ret f: pushl %ebp movl %esp, %ebp subl $4, %esp movl 8(%ebp), %eax movl %eax, (%esp) call g leave ret main: pushl %ebp movl %esp, %ebp subl $4, %esp movl $10, (%esp) call f addl $1, %eax leave ret
具体的逐步分析,这里就省了,老师课上讲的很详细了,这里主要是要进行思考和归纳。
首先,我们看到3个C函数对应生成了3个部分的汇编代码,分别用函数名作为标号隔开了
int g(int x) -> g: int f(int x) -> f: int main(void) -> main:
我们知道程序是从 main 函数开始执行的,那么当程序被加载并运行时,上面的汇编代码会被加载到内存的某一个区域。而且,CPU中的很多寄存器都会初始化,当然其中最重要的是 eip ,因为 eip 是指向下一条将要执行的命令所在的内存地址,所以此时的 eip 应该指向 main 标号下的 pushl %ebp :
main: eip -> pushl %ebp
程序开始执行...
我们捆绑着看,首先先看这两条:
pushl %ebp movl %esp, %ebp
再观察一下整个代码,有没有发现不仅仅是 main 函数,函数 f 和 g 的开头也是这两个指令。分析一下,不难得出,这两条指令是指 将当前栈基地址压栈后,重新将基地址定位到栈顶 ,这个含义其实是保存好当前的基地址,重新开始一个新的栈。由于函数可以调函数, 这里的当前基地址,实际上是上一个函数的栈基地址 。例如,在 f 函数中的这两句指令,实际上保存的是 main 函数的栈基地址。
接着来分析两句:
subl $4, %esp movl $10, (%esp)
对照C代码不难发现,这是 参数进栈 ,将立即数,保存到栈顶(esp所指向的内存地址是栈顶)。而在 f 函数中也可以发现类似的语句:
subl $4, %esp movl 8(%ebp), %eax movl %eax, (%esp)
所以,我们可以得出结论是,在调用函数前需要把参数逐个压栈,而压栈的顺序根据笔者的测试是从右向左的。
接着调用 l 指令,跳转到 f 函数,我们知道 call 指令等同于下面的伪代码:
pushl %eip+1 movl %eip f
即把 call 指令的后一条指令进栈后,将 eip 赋值为目标函数的第一个指令地址。这样做显而易见:当所调用的函数结束后,需要返回当前函数继续执行,所以必须要保存下一条指令,否则回来的时候就找不到了。
来到 f 函数,首先是保存main函数的栈基地址,然后需要调用 g 函数,于是需要参数先进栈:
subl $4, %esp movl 8(%ebp), %eax movl %eax, (%esp)
这里重点思考一下, f 函数是如何获得main函数传递过来的参数的,我们看到
movl 8(%ebp), %eax
为什么参数是从 8(%ebp) 中获得的呢?我们知道 8(%ebp) 表示的是以ebp为基准向栈底回溯8个字节得到,为什么是8个字节呢?
回想一下,在 main 函数中完成了参数进栈后做了两件事情:
于是通过 8(%ebp) 可以找到前一个函数的第一个整型参数的值。
一张图告诉你怎么回事:
看过了进入函数,调用函数的过程,再看一下函数是如何退出的。观察 main 和 f 不难发现,退出函数使用的是如下指令
leave ret
leave 指令相当于如下指令:
movl %ebp, %esp popl %ebp
接着 ret 就是相当于,恢复指令指向:
popl %eip
为什么g函数没有leave呢?因为g函数内部没有任何的变量声明和函数调用栈一直都是空的,所以编译器优化了指令
最后,通过这个例子,总结一下函数调用的过程:
进入函数:
调用其他函数:
退出函数: