在上一篇博客中小览call stack(调用栈) (一)中,我展示了如何在windbg中 观察调用栈的相关信息:函数的返回地址,参数,返回值。这些信息都按照一定 的规则存储在固定的地方。这个规则就是调用约定(calling convention)。
调用约定在计算机界不是什么新鲜的概念,已经有许多相关的文献给予详细 的介绍。比较全面的介绍可以参见wikipedia上的相关页面。然而,如果你和我 一样,在第一次接触调用约定的时候,觉得这个概念是个高深神秘的冬冬,那么 就请跟随我一起,在这篇博客中看看他的由来,他的范畴以及他的用途。
为什么需要调用约定?
在具体介绍调用约定的定义之前,我们先来看看为什么我们需要一个称之为 调用约定的冬冬。如果各位了解汇编语言(不了解的话,看下面的这段会稍微有 些费力,不过我尽可能把汇编的相关知识解释的清楚一些),那么回忆一下我们 是怎么来做一个函数调用的。
汇编语言提供了一条指令,call ptr,其功能是把CS:IP (指令段:指令指针 ,决定着下一条执行指令的地址)压栈,并且修改CPU的指令指针,作一个跳转。 在函数结束的地方,我们使用另一条指令,ret,其功能是把栈中的返回地址取 出,并且跳转到那条指令。
在这里汇编语言只提供了指令跳转的命令,作为函数调用另一个重要组成部 分的参数传递,其方式就很灵活,你可以通过寄存器传值,可以通过调用栈传值 ,可以通过某一块具体的内存传值(类似全局变量)。然后在被调用函数中,从寄 存器,栈或者是内存中读取这些信息。想象一下如果被调用函数是某一个程序员 所编写的,调用者是另一个程序员,那么他俩之间对于参数的传递方式就有了一 个约定。
高级语言的出现,把这个问题隐藏了起来。我们在编写一般的c++程序的时候 ,通常不需要顾虑参数传递的底层实现,但是,这并不意味着这一问题不再出现 ——我们只是把责任推给了编译器。编译器作为一个计算机程序,总 是遵照一定的规则工作,每一个规则对应了一种调用约定。
久而久之,那些经典的规则所产生的调用约定,就成了耳熟能详的冬冬:
耳熟能详的调用约定
在介绍这些调用规范之前,我想先说明的是,下面所涉及的调用规范是在32 位x86处理器windows平台上的。把范畴限定在32位处理器的原因是:16位处理器 已经退出CPU的历史舞台,64微处理器无论是IA64还是AMD64都只有一个调用规范 ——只有32位处理器呈现百家成名,百花齐放的景象。(对了,你当 然明白调用规范是绑定在处理器架构上的概念,因为它涉及太多的诸如寄存器之 类的处理器架构细节。)聚焦于windows则是因为我现在的工作只涉及这一平台。
下表的出处来自于The Old New Thing以及张羿的csdn专栏,并作了适当修改 。
首先来看所有的调用规范都遵循的规定:返回值存储在EDX:EAX中,EDI,ESI ,EBP,EBX是保留的存储器。(即函数可以任意使用这些寄存器,无需担心破坏 了调用者的寄存器状态)
调用约定名称 | 清理堆栈 | 参数压栈顺序 | 备注 |
cdecl | 调用者 (Caller) | 从右往左 | 因为是调用者清理Stack,因此允许变参 (如 printf) |
stdcall | 被调用者 (Callee) | 从右往左 | 一般在Windows API和COM中使用,也是.NET和 Native代码调用的缺省Calling Convention。 顺便提一下,Windows中API的 Calling Convention所使用到的WINAPI宏在PC机上是__stdcall,而在WinCE上则 是__cdecl,并非一成不变。 |
Thiscall (Microsoft) | 被调用者 (Callee) | 从右往左 | 基本上等价stdcall, 除了this指针用ECX传递 |
Fastcall (Microsoft) | 被调用者 (Callee) | 从右往左 | 和Stdcall类似,但是会选择两个从左往右数最 先可以放在寄存器里面的参数放在ECX和EDX中 |