Win - 程序是怎样跑起来的——函数内部的处理

接下来,让我们透过执行AddNum函数的源代码部分,来看一下参数的接收、返回值的返回等机制(代码清单10-5)。

代码清单10-5 函数内部的处理

Win - 程序是怎样跑起来的——函数内部的处理

ebp寄存器的值在(1)中入栈,在(5)中出栈。这主要是为了把函数中用到的ebp寄存器的内容,恢复到函数调用前的状态。在进入函数处理之前,无法确定ebp寄存器用到了什么地方,但由于函数内部也会用到ebp寄存器,所以就暂时将该值保存了起来。CPU拥有的寄存器是有数量限制的。在函数调用前,调用源有可能已经在使用ebp寄存器了。因而,在函数内部利用的寄存器,要尽量返回到函数调用前的状态。为此,我们就需要将其暂时保存在栈中,然后再在函数处理完毕之前出栈,使其返回到原来的状态。

(2)中把负责管理栈地址的esp寄存器的值赋值到了ebp寄存器中。这是因为,在mov指令中方括号内的参数,是不允许指定esp寄存器的。因此,这里就采用了不直接通过esp,而是用ebp寄存器来读写栈内容的方法。

(3)是用[ebp+8]指定栈中存储的第1个参数123,并将其读出到eax寄存器中。像这样,不使用pop指令,也可以查看栈的内容。而之所以从多个寄存器中选择了eax寄存器,是因为eax寄存器是负责运算的累加寄存器。

通过(4)的add指令,把当前eax寄存器的值同第2个参数相加后的结果存储在eax寄存器中。[ebp+12]是用来指定第2个参数456的。在C语言中,函数的返回值必须通过eax寄存器返回,这也是规定。不过,和ebp寄存器不同的是,eax寄存器的值不用还原到原始状态。至此,我们进行了很多细节的说明,其实就是希望大家了解“函数的参数是通过栈来传递,返回值是通过寄存器来返回的”这一点。

(6)中ret指令运行后,函数返回目的地的内存地址会自动出栈,据此,程序流程就会跳转返回到代码清单10-4的(6)(Call _AddNum的下一行)。这时,AddNum函数入口和出口处栈的状态变化,就如图10-5所示。将图10-4和图10-5按照(a)(b)(c)(d)(e)( f )的顺序来看的话,函数调用处理时栈的状态变化就会很清楚了。由于(a)状态时处理跳转到AddNum函数,因此(a)和(b)是同样的。同理,在(d)状态时,处理跳转到了调用源,因此(d)和(e)是同样的。在(f)状态时则进行了清理处理。栈的最高位的数据地址,是一直存储在esp寄存器中的。

Win - 程序是怎样跑起来的——函数内部的处理

图10-5 AddNum函数内部的栈状态变化