小览call stack(挪用栈) (二)——挪用约定
副标题#e#
在上一篇博客中小览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中 |
#p#副标题#e#
各人大概对清理仓库,参数压栈顺序这些观念不是很清楚,在这里我会通过 一个详细的例子来说明。下面列出了一小段措施和它的汇编代码:
#p#分页标题#e#
#include <stdio.h>
int __stdcall Test(int a, char b, short c)
{
printf("%d %c %d", a, b, c);
return a+c;
}
void main()
{
int a = Test(5, 'a', 10);
}
#include <stdio.h>
int __stdcall Test(int a, char b, short c)
{
printf("%d %c %d", a, b, c);
return a+c;
}
void main()
{
int a = Test(5, 'a', 10);
}
在main中对Test的挪用对应了如下的汇编代码:
00412004 6a0a push 0Ah
00412006 6a61 push 61h
00412008 6a05 push 5
0041200a e800f0feff call test!ILT+10(?TestYGHHDFZ) (0040100f)
0041200f 8945fc mov dword ptr [ebp-4],eax ss:002b:001
00412004 6a0a push 0Ah
00412006 6a61 push 61h
00412008 6a05 push 5
0041200a e800f0feff call test!ILT+10(?TestYGHHDFZ) (0040100f)
0041200f 8945fc mov dword ptr [ebp-4],eax ss:002b:001
在这个例子中,我们可以调查到如下信息:
1. 压栈顺序:栈中首先压入的是0A(十进制中的10),是最后一个参数,其次 是’a’,最后是5,所以说__stdcall的压栈顺序是从右向左。
2. 返回值存放在eax中:在call指令之后,把eax的值存入到[ebp-4]中,对应 了c++代码中对a的赋值,可见eax是返回值的存放之所。
3. 被挪用函数清理栈:在call指令和mov指令没有特另外其他指令,可见之 前放到栈里的参数,都已经被函数Test清理了(Test的最后一条指令是ret 0c), 把栈的指针调解了三个变量的位置。
4. 函数改名:细心的读者会发明call指令后头跟的是如同乱码般的test! ILT+10(?TestYGHHDFZ),这是编译器做的手脚(name mangling),差异的挪用规 范下,编译器会凭据差异的法则对函数举办改名。我不想细究的原因在于:一方 便,函数改名的法则自己就在变革,我今朝利用的编译器,会凭据以前 __thiscall的法则来改名__stdcall的函数。另一方面,很多debuger好比windbg ,会自动的把定名调解返来。
如何指定挪用约定
凡是,我们真正需要思量到挪用约定的场景,是对一些外部类库的利用。举 例来说,假如我们要挪用的函数由别的一个类库提供,那么,我们需要按照这个 函数所声明的挪用约定来利用这个函数。也就是说,我们要汇报编译器,请凭据 这个挪用约定,生成相关的代码,来利用谁人来自于类库的函数。对付MSVC的编 译器来说,有下面的这些开关:
编译器开关 | 挪用类型 |
/Gd | __cdecl |
/Gr | __fastcall |
/Gz | __stdcall |
个中/Gz是c++的默认选项。
别的一个例子是,提供应别人的回调函数,需要按照挪用者的要求,声明调 用约定,举一个例子来说,在windows中开始一个新的线程。
这时候,可以在函数声明的语句中,在返回值范例后头插入相关的挪用类型 ,如前面的例子中所示。
int __stdcall Test(int a, char b, short c)
int __stdcall Test(int a, char b, short c)
假如你是一个.NET用户(终于,我可以谈及一些我们的产物了),那么你在 P/Invoke的时候仍然需要挪用约定。DllImportAttibute中,有一个字段 CallingConvention,就是对应这个需求生成的。
[DllImport("ole32.dll", EntryPoint="CoCreateInstance", CallingConvention=CallingConvention.StdCall)]
public static extern int CoCreateInstance(ref Guid rclsid, IntPtr pUnkOuter, uint dwClsContext, ref Guid riid, ref System.IntPtr ppv) ;
挪用约定的用武之地
看了上面的先容之后,你大概会想,我们只需要按照文档上声明的挪用约定 ,在本身的代码中指定相应的挪用约定就可以了。那么,相识清楚每一个挪用约 定的详细内容对我们有什么辅佐呢?
我认为,相识挪用约定首先可以辅佐我们深入相识函数挪用部门的汇编代码 的道理。有许多时候,错误的利用了挪用类型是一个很难察觉的bug。
#p#分页标题#e#
其次,相识挪用约定在只拥有民众标记(public symbol)举办调试的时候对 我们辅佐很大,民众标记凡是只能让我们调查到挪用栈信息。那么相识了挪用约 定之后,我们至少能操作挪用栈找到函数参数,函数返回值等信息。
总结以及下期预告
本日我耗费了蛮多笔墨讲授挪用类型,对付这一系列的主题“挪用栈 ”来说,挪用类型是一个息息相关的观念。下一次,我将通过一个windbg 调试脚原来调查遵循stdcall的挪用栈,作为这一系列的收尾,敬请等候。