现有如下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再跳转到下一条指令,一直这样循环往复,程序就跑了起来。