



现在我们来想一个问题,函数传递参数,可以通过寄存器传递,为什么需要栈传递呢?显然这是为了解决CPU寄存器不够用的情况。x64除了8个通用寄存器rax、rcx、rbx、rdx、rsi、rdi、rsp、rbp外,还增加了r8、r9、r10、r11、r12、r13、r14、r15等8个寄存器,可用于辅助传递参数。尽管如此,寄存器的数量还是有限的,而且没有标准的约定,所以通过栈来传递是比较合理的。
根据用户要求,编译器以什么样的形式传递参数而生成函数栈框架呢?这就涉及了调用约定问题。有3种常用约定:stdcall、cdecl和fastcall。选择stdcall和cdecl约定的话,参数是按从右到左的顺序入栈,也就是末尾的参数先入栈,第一个参数最后入栈。这两者之间的差别又在于,stdcall不需要调用者恢复栈位置(ebp和esp指向正确的位置),而cdecl需要调用者恢复栈位置。约定fastcall是使用寄存器传递参数,第一个参数放在ecx,第二个参数放在edx,其余参数仍然按从右到左的顺序入栈。对于Visual Studio 2008的编译器,如果没有特别指明,一般使用stdcall约定生成栈框架函数。如果要使用特定的栈框架,需要在函数前指定,否则将由编译器默认的调用约定来编译代码。无论使用什么约定,栈都能恢复,所以大多数时候我们无须关心使用什么约定,但是如果要整理某些函数,那么这方面就需要注意。比如,使用调试器的反汇编窗口时就要确定此函数使用什么约定,一般来说stdcall生成的函数栈框架中,在函数的末尾ret后还会跟上一个数字n,这个n就是参数占用的栈大小。在执行流程返回后,栈顶自动恢复。
从原理上说,cdecl约定才是最合理的调用约定,这是为什么呢?我们知道,在调用函数前,入栈的操作者是调用者,那么栈恢复的操作者也应该是调用者,而不是交给函数来完成,再说函数有什么理由需要负责恢复栈呢?显然,stdcall调用约定强制函数来完成栈的恢复,这是野蛮的行为。野蛮的行为当然也会有野蛮的理由。stdcall调用约定认为,函数的设计是为了给调用者提供方便的,所以栈恢复也应当由函数完成。至于fastcall就不用说了,更加不合理,前两个参数使用寄存器,其余参数又使用栈,感觉比较混乱。当然,任何的调用约定都有其存在的理由,笔者不想花费太多时间去争论哪一种调用约定比较好。之所以在这里特意提出这些,是希望读者能清晰地理解什么是调用约定,以及调用约定为什么被提出来。此外,加密的程序没有标准的调用约定。