函数的调用机制和内部处理

函数的调用机制
首先,让我们从MyFunc函数调用AddNum函数的汇编语言部分开始,来对函数的调用机制进行说明。栈在函数的调用中发挥了巨大的作用,下面是经过处理后的MyFunc函数的汇编处理内容:
函数的调用机制和内部处理
代码解释中的(1)、(2)、 (7)、 (8)的处理适用于C语言中的所有函数,我们会在后面展示AddNum 函数处理内容时进行说明。这里希望大家先关注(3) — (6)这一部分,这对了解函数调用机制至关重要。

(3)和(4)表示的是将传递给AddNum函数的参数通过push入栈。在C语言源代码中,虽然记述为函数AddNum(123,456),但入栈时则会先按照456, 123 这样的顺序。也就是位于后面的数值先入栈。这是C语言的规定。(5) 表示的call指令,会把程序流程跳转到AddNum函数指令的地址处。在汇编语言中,函数名 表示的就是函数所在的内存地址。AddNum函数处理完毕后,程序流程必须要返回到编号(6)这一行。 call 指令运行后,call 指令的下一行(也就指的是(6)这一行)的内存地址(调用函数完毕后要返回的内存地址)会自动的push入栈。该值会在AddNum函数处理的最后通过ret 指令pop出栈,然后程序会返回到(6)这一行。

(6)部分会把栈中存储的两个参数(456 和123)进行销毁处理。虽然通过两次的pop指令也可以实现,不过采用esp寄存器+8的方式会更有效率(处理1次即可)。对栈进行数值的输入和输出时,数值的单位是4字节。因此,通过在负责栈地址管理的esp寄存器中加上4的2倍8,就可以达到和运行两次pop命令同样的效果。虽然内存中的数据实际上还残留着,但只要把esp寄存器的值更新为数据存储地址前面的数据位置,该数据也就相当于销毁了。
我在编译Sample4.c 文件时,出现了下图的这条消息:
函数的调用机制和内部处理
图中的意思是指c的值在MyFunc定义了但是一直未被使用,这其实是一项编译器优化的功能,由于存储着AddNum函数返回值的变量c在后面没有被用到,因此编译器就认为该变量没有意义,进而也就没有生成与之对应的汇编语言代码。
下图是调用AddNum这一函数前后栈内存的变化:
函数的调用机制和内部处理
函数的内部处理
上面我们用汇编代码分析了一下Sample4.c整个过程的代码,现在我们着重分析一下 AddNum函数的源代码部分,分析一下参数的接收、返回值和返回等机制:
函数的调用机制和内部处理
ebp寄存器的值在(1)中入栈,在(5)中出栈,这主要是为了把函数中用到的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寄存器返回,这也是规定。也就是函数的参数是通过栈来传递,返回值是通过寄存器返回的。

(6)中ret指令运行后,函数返回目的地内存地址会自动出栈,据此,程序流程就会跳转返回到(6)(Call AddNum) 的下一行。这时,AddNum 函数入口和出口处栈的状态变化,就如下图所示:
函数的调用机制和内部处理