10.7函数调用机制
前面说了这么多,至此我们终于把阅读汇编语言源代码的准备工作完成了。让我们再来回顾一下代码清单 10-2 的内容。首先,让我们从MyFunc 函数调用AddNum 函数的汇编语言部分开始,来对函数的调用机制进行说明。函数调用是栈发挥大作用的场合。把代码清单 10-2 中的C 语言源代码部分去除,然后再在各行追加注释,这时汇编语言的源代码就如代码清单 10-4 所示。这也就是 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)进行销毁处理,也就是在第 5 章提到的栈清理处理。虽然通过使用两次 pop 指令也可以实现,不过采用 esp 寄存器加 8 的方式会更有效率(处理1次即可 )。对栈进行数值的输入输出时,数值的单位是 4 字节。因此,通过在负责栈地址管理的 esp 寄存器中加上 4的 2 倍 8,就可以达到和运行两次 pop 命令同样的效果。虽然内存中的数据实际上还残留着,但只要把 esp 寄存器的值更新为数据存储地址前面的数据位置,该数据也就相当于被销毁了。
前面已经提到,push 指令和 pop 指令必须以4字节为单位对数据进行入栈和出栈处理。因此,AddNum 函数调用前和调用后栈的状态变化就如图 10-4 所示。长度小与4 字节的 123 和456 这些值在存储时也占用了 4 字节的栈区域。
代码清单 10-1 中列出的 C 语言源代码中,有一个处理是在变量 c中存储AddNum 函数的返回值,不过在汇编语言的源代码中,并没有与此对应的处理。这是因为编译器有最优化功能。最优化功能是编译器在本地代码上费尽功夫实现的,其目的是让编译后的程序运行速度更快、文件更小。在代码清单 10-1 中,由于存储着 AddNum 函数返回值的变量 c在后面没有被用到,因此编译器就会认为“该处理没有意义”,进而也就没有生成与之对应的汇编语言代码。在编译代码清单10-1的代码时,应该会出现“警告 W8004 Sample4.c 1l:c的赋值未被使用(函数 MyFunc”这样的警告消息。