Flier's Sky

天空,蓝色的天空,眼睛看不到的东西,眼睛看得到的东西
posts - 115, comments - 327, trackbacks - 42, articles - 0

导航

公告

The history of calling conventions

Posted on 2004-07-08 10:57 Flier Lu 阅读(...) 评论(...) 编辑 收藏
http://www.blogcn.com/user8/flier_lu/index.html?id=1558534&run=.0F8ED5D

 Raymond Chen完成了一个非常出色的系列文章,讨论了从16bit x86架构到 IA64/AMD64、从CISC到RISC的各种调用约定(calling convention)。加上其后的讨论,让我们能够从头到尾对调用约定的发展有所了解。

     The history of calling conventions, part 1中讨论了16bit的x86架构下的调用约定。这些调用约定都需要对 BP, SI 和 DI 寄存器进行保护,并通过 AX 或 DX:AX 返回 16/32 位返回值。
 

以下为引用:

 调用约定    语言      参数压栈顺序  清理堆栈方  使用寄存器
 __cdecl     C           从右到左      调用者
 __pascal    Pascal      从左到右     被调用者
 __fortran   Fortran     从左到右     被调用者
 __fastcall  C/Delphi    从右到左      调用者      DX, CX (MS)
                                                                          AX, DX, CX (Borland)
 


     因为Pascal调用由被调用者清理堆栈,可以让调用函数者省下3个字节的代码,所以被Windows早期版本定为API的标准调用约定,并一直沿用下来。这让我想起那个罗马时代的两匹马屁股宽度,决定了现代火箭推动器宽度的笑话 :D 虽然这个笑话没什么真实性,但Windows下这种问题的确存在 :P

     The history of calling conventions, part 2中讨论了RISC架构下的一些常见处理器上的调用约定,如Alpha AXP、MIPS R4000、PowerPC。
     可以发现这些RISC处理器,包括后面要讨论的IA64处理器,基本上都是通过寄存器而非堆栈来传递参数的,毕竟RISC的寄存器多啊,呵呵,羡慕的说。而且这些RISC处理器一般硬件上就限定了唯一的调用约定,也就是传递参数的寄嫫鞯墓潭ㄐ蛄小?

     The history of calling conventions, part 3中讨论了我们比较熟悉的32位 x86 架构下的调用约定。这些调用约定都需要对EDI, ESI, EBP 和 EBX 寄存器进行保护,并通过 EAX 或 EDX:EAX 返回 32/64 位返回值。
 

以下为引用:

 调用约定    参数压栈顺序  清理堆栈方  使用寄存器    函数名编码方式
 __cdecl       从右到左      调用者                    加 '_' 前缀
 __stdcall     从右到左     被调用者                   加 '_' 前缀,'@' 后缀加上参数字节数字
 __fastcall    从右到左     被调用者   ECX, EDX (MS)   加 '@' 前缀,'@' 后缀加上参数字节数字
                                     EAX, ECX, EDX (Borland)
 thiscall      从右到左      被调用者   ECX (this)      C++编译器相关编码算法
 


     MSDN上有一个非常详细的调用示例图
 
以下为引用:

 The following example shows the results of making a function call using various calling conventions.
 This example is based on the following function skeleton. Replace calltype with the appropriate calling convention.

 void    calltype MyFunc( char c, short s, int i, double f );
 .
 .
 .
 void    MyFunc( char c, short s, int i, double f )
     {
     .
     .
     .
     }
 .
 .
 .
 MyFunc ('x', 12, 8192, 2.7183);


 __cdecl
 The C decorated function name is “_MyFunc.”

 The__cdecl calling convention
 

 __stdcall and thiscall
 The C decorated name (__stdcall) is “_MyFunc@20.” The C++ decorated name is proprietary.

 The __stdcall and thiscall calling conventions
 

 __fastcall
 The C decorated name (__fastcall) is “@MyFunc@20.” The C++ decorated name is proprietary.

 The__fastcall calling convention
 
 


     The history of calling conventions, part 4: ia64中讨论了Itanium中128个整数寄存器如何被函数调用使用。
     其中r0到r32作为全局寄存器不参与共享,胜于96个寄存器用于局部使用。例如调用一个函数时用到六个参数(r32-r37)和三个返回参数寄存器(r38, r39, r40),则寄存器r41-r127可以被使用。然后在此函数中再调用一个函数,参数就从r38开始存放,如三个参数r38, r39, r40。当调用call后,CPU会做一个保护操作,将原本上一层函数调用中使用的参数寄存器r32-r37保存到一个独立的寄存器堆栈中,而将此次函数调用的参数寄存器移动到开始处,r32 = r38, r33 = r39, r34 = r40,因此对每个函数本身来说,其调用参数永远是从r32开始的。而因为此过程中根本不使用堆栈,也就不存在x86架构下清理堆栈的问题。而返回值直接通过一个预定义寄存器r8返回,因此也不存在缓冲区溢出的问题了。
     其中两个比较特殊的地方是:
     首先,堆栈的头16个字节是可以自由使用的,而且对其上层调用者无意义,这个区域也被称为red zone。类似于x86架构下堆栈指针后面的空间。
     其次,IA64下的函数指针不是直接指向函数代码头部,而是指向一个函数描述结构,此结构的第一个64位值指向实际函数入口地址。这种机制被很多RISC结构的系统采用,如PPC。而且

     而对于浮点数参数也使用了类似的机制,f0-f7被保留给全局使用,第一个浮点参数从f8开始。例如一个函数有四个参数,第三个参数为浮点数,则传入函数的有效参数寄存器是r32, r33, r35和f8,中间的r34被保留。

     The history of calling conventions, part 5: amd64中讨论了AMD64下的调用约定。为最大限度兼容32位x86架构,AMD64在把通用寄存器扩展为rax, rbx之后,另外新增了8个64位存器r8-r15。
     函数调用时,头4个参数通过rcx, rdx, r8和r9寄存器传递,其他参数还是通过堆栈传递,堆栈中为通过寄存器传递的参数保留空间。而小于64位的参数不会被自动扩展用0填充高位,使用时需要显式清空无效的高位。返回值通过rax寄存器返回,大于64位时通过堆栈保存并返回指针。而堆栈的清除工作由函数调用者完成。为保障效率,堆栈必须是16字节对其的,如压栈8字节数据还需要进行补齐工作。

     例如下面的函数调用的汇编代码和堆栈情况如下
 

以下为引用:

 void SomeFunction(int a, int b, int c, int d, int e);
 void CallThatFunction()
 {
     SomeFunction(1, 2, 3, 4, 5);
 }

 mov     dword ptr [rsp+0x20], 5     ; output parameter 5
 mov     r9d, 4                      ; output parameter 4
 mov     r8d, 3                      ; output parameter 3
 mov     edx, 2                      ; output parameter 2
 mov     ecx, 1                      ; output parameter 1
 call    SomeFunction                ; Go Speed Racer!


 xxxxxxx8 .. rest of stack (minus arg area) ..
 xxxxxxx0 (arg5)
 xxxxxxx8 (arg4 spill)
 xxxxxxx0 (arg3 spill)
 xxxxxxx8 (arg2 spill)
 xxxxxxx0 (arg1 spill) <- ".. rest of stack .." from first diagram
 xxxxxxx8 return address <- RSP upon entry to callee
 



     虽然AMD64的返回值仍然通过堆栈传递,存在被缓冲区溢出的可能性,但因为新架构实现了支持堆栈部分内存不可执行的保护特性,所以想通过溢出获取权限的技术难度大大增加。加上操作系统如Win2003在堆栈溢出上的保护代码,以后堆栈溢出的攻击手段将被大大削弱。

     最后Raymond Chen还在What can go wrong when you mismatch the calling convention? 中讨论了错误使用调用约定的潜在问题。其中也提到了RunDll32.exe的使用,具体情况可以参考我一起的一篇BLog《RunDll32 的使用方法与实现原理》

     再次感谢Raymond Chen为我们带来了如此精彩的一系列讨论。 :P