对C和汇编的简略分析以及计算机的工作原理
现有如下C程序代码:
int g(int x)
{
return x + 4;
}
int f(int x)
{
return g(x);
}
int main(void)
{
return f(10) + 5;
}
使用
gcc –S –o main.s main.c -m32
得到汇编代码如下(为方便阅读已去除链接信息):
g:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
addl $4, %eax
popl %ebp
ret
f:
pushl %ebp
movl %esp, %ebp
pushl 8(%ebp)
call g
addl $4, %esp
leave
ret
main:
pushl %ebp
movl %esp, %ebp
pushl $10
call f
addl $4, %esp
addl $5, %eax
leave
ret
首先说明一下enter和leave两条宏指令的作用:
- enter相当于以下两条指令:
pushl %ebp movl %esp %ebp
效果是将上一个栈帧的基址压栈并将esp和ebp都指向新栈帧的起点。
- leave相当于以下两条指令:
movl %ebp %esp popl %ebp
效果是将栈帧基址和栈顶指针还原为指向上一个栈的样子。
一般来说,enter和leave分别在进入栈帧和离开栈帧的时候使用
我们来看看上面那段C代码是怎样执行的。 编译器生成的汇编代码如下:
从main函数开始,17行和18行相当于enter指令,开辟了一个新的栈帧;从19行开始main函数的内容,19行压入一个立即数10作为接下来调用f的参数;接下来第20行调用f,注意call是一个短跳转指令,它会先将下一条指令即第21行压进栈中并将eip置为f的地址。 为方便说明,我们相信f执行完之后main的栈帧会恢复到执行f之前的样子,只是eax发生了变化(因为eax存储了f的返回值)。执行完f之后的下一条指令是第21行,这条指令将esp的值加上4,也就是将19行压入的10弹出,此时esp以恢复到执行19行之前的样子;22行将f的返回值加上了5,接着执行leave清理刚申请的栈帧,ret将eip置为紧接main函数之后的一条语句,因此在这里main函数就执行完毕了。
有一个细节就是在g函数里面并没有使用leave指令而是使用了popl %ebp代替,两者的区别为是否恢复了esp,原因应该是在g函数里enter之后并没有使用push和pop,因此esp是没发生变化的,自然不需要恢复esp了,具体可见这篇文章。
以上就是main函数执行的大致过程,g和f同理。
汇编指令几乎与机器指令一一对应,因此我们可以认为汇编指令就是计算机能直接执行的指令。在指令执行的时候,指令被载入内存,eip始终指向下一条将要执行的指令的地址,eip可以通过call和ret等指令实现跳转。CPU总是从eip指向的内存中取出一条指令然后执行,eip再跳转到下一条指令,一直这样循环往复,程序就跑了起来。