函数调用约定

我们经常见到类似“WINAPI”、“PASCAL”、“STDCALL”、“CALLBACK”这样的函数(注1)修饰符,如在windows.h头文件中MessageBox的声明:

WINUSERAPI int WINAPI MessageBoxA(

    HWND hWnd ,

    LPCSTR lpText,

    LPCSTR lpCaption,

    UINT uType);

WINUSERAPI int WINAPI MessageBoxW(

    HWND hWnd ,

    LPCWSTR lpText,                                                  

    LPCWSTR lpCaption,

UINT uType);

在WinDef.h 头文件中,WINAPI的定义如下:

#define CALLBACK    __stdcall
#define WINAPI      __stdcall
#define WINAPIV     __cdecl
#define APIENTRY    WINAPI

可以看到,不管是WINAPI还是CALLBACK,其真面目都是__stdcall,也就是通常的STDCALL。这就是函数的调用约定,这个调用约定到底指的是什么呢?我们知道在一般的编程语言中,函数调用的方式一般都是把函数参数和返回地址压入程序的堆栈中,用完之后再清理堆栈,使之恢复到调用前的状态,这样才能保证堆栈不被破坏。这个过程涉及到两个问题:1、如何将函数参数压入堆栈,是从左到右还是从右到左?2、函数调用完毕,谁来清理堆栈,是调用者还是被调用者?

其实到底如何实现本是无谓的问题,但要在各种语言各种编译器下让模块变得通用,比如我们会在VB、VC、VS、Delphi下调用Windows API函数,那就要有规范了,这样所有的编译器才能知道如何生成最终代码。常见的调用约定如下(注2):

 

C

SysCall

StdCall

BASIC

FORTRAN

PASCAL

最先入栈参数

清除堆栈者

调用者

子程序

子程序

子程序

子程序

子程序

允许使用VARARG

“允许使用VARARG”指的是是否允许可变参数,比如C语言中的printf,可以有任意个参数,是否允许可变参数跟入栈顺序有关。

这个表只是一个总结,要想看到本质,还需要查看汇编代码(注3):

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

Sub1    proc         C _Var1,_Var2

        mov          eax,_Var1

        mov          ebx,_Var2

        ret

Sub1    endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

Sub2    proc         PASCAL _Var1,_Var2

        mov          eax,_Var1

        mov          ebx,_Var2

        ret

Sub2    endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

Sub3    proc         _Var1,_Var2

        mov          eax,_Var1

        mov          ebx,_Var2

        ret

b3                   endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        …

        invoke       Sub1,1,2

        invoke       Sub2,1,2

        invoke       Sub3,1,2

编译后再进行反汇编,看编译器是如何转换处理不同类型的子程序的:

; 这里是Sub1 - C类型

:00401000 55                    push ebp

:00401001 8BEC                 mov ebp, esp

:00401003 8B4508                 mov eax, dword ptr [ebp+08]

:00401006 8B5D0C                mov ebx, dword ptr [ebp+0C]

:00401009 C9                    leave

:0040100A C3                    ret

; 这里是Sub2 - PASCAL类型

:0040100B 55                    push ebp

:0040100C 8BEC                  mov ebp, esp

:0040100E 8B450C                mov eax, dword ptr [ebp+0C]

:00401011 8B5D08                mov ebx, dword ptr [ebp+08]

:00401014 C9                    leave

:00401015 C20800                ret 0008

; 这里是Sub3 — StdCall类型

:00401018 55                    push ebp

:00401019 8BEC                  mov ebp, esp

:0040101B 8B4508                mov eax, dword ptr [ebp+08]

:0040101E 8B5D0C                mov ebx, dword ptr [ebp+0C]

:00401021 C9                    leave

:00401022 C20800                ret 0008

        …

; 这里是invoke Sub1,1,2 — C类型

:00401025 6A02                  push 00000002

:00401027 6A01                  push 00000001

:00401029 E8D2FFFFFF            call 00401000

:0040102E 83C408                add esp, 00000008

; 这里是invoke Sub2,1,2 — PASCAL类型

:00401031 6A01                  push 00000001

:00401033 6A02                  push 00000002

:00401035 E8D1FFFFFF            call 0040100B

; 这里是invoke Sub3,1,2 — StdCall类型

:0040103A 6A02                  push 00000002

:0040103C 6A01                  push 00000001

:0040103E E8D5FFFFFF            call 00401018

可以看到,调用每个子过程之前,调用者将参数按从左到右的顺序或从右到左的顺序压入堆栈,在每一个子过程中,因为在以下的指令中ebp会被当作指针来使用,值会被改变,所以汇 编编译器会在子过程开头将ebp压入堆栈。这样,整个堆栈看起来大致如下图所示:

形参1

形参2

call的返回地址

ebp

这里有另一个疑问,我们知道MASM6支持很多高级语言才有的宏指令,当调用一个子过程时,跟堆栈有关的因素有五个,分别是子过程形参、call的返回地址、局部变量、USES指令保护的寄存器、ebp,那么,当我们调用一个子过程时,这些值进入堆栈的顺序是怎么样的呢?通过上面invoke的反汇编代码我们知道,子过程形参肯定是第一个被压入堆栈的,然后是call指令自动将子过程返回地址压入堆栈,关键是后面的局部变量、USES指令保护的寄存器、ebp的顺序是怎样的呢?我们先看一段未经编译的汇编代码:

ProcWinMain    proc  uses ebx edi esi,hWnd,uMsg,wParam,lParam

local stPs:PAINTSTRUCT

local stRect:RECT

local hDC

mov    eax,uMsg

.if      eax == WM_PAINT

Invoke  BeginPaint,hWnd,addr stPs

mov    hDC,eax

invoke  GetClientRect,hWnd,addr stRect

invoke  DrawText,hDC,addr szText,-1,/

addr stRect,/

DT_SINGLELINE or DT_CENTER or DT_VCENTER

invoke   EndPaint,hWnd,addr stPs

.elseif   eax == WM_CLOSE

invoke  DestroyWindow,hWinMain

invoke  PostQuitMessage,NULL

.else

invoke  DefWindowProc,hWnd,uMsg,wParam,lParam

ret

.endif

 

xor    eax,eax

ret

ProcWinMain    endp

编译后,通过W32Dasm反汇编代码如下:

+++++++++++++++++++ ASSEMBLY CODE LISTING ++++++++++++++++++

//********************** Start of Code in Object .text **************

Program Entry Point = 00401163 (SimpleWindow.exe File Offset:00003163)

:00401000 55                      push ebp

:00401001 8BEC                   mov ebp, esp

:00401003 83C4AC                 add esp, FFFFFFAC

:00401006 53                      push ebx

:00401007 57                      push edi

:00401008 56                      push esi

:00401009 8B450C                  mov eax, dword ptr [ebp+0C]

:0040100C 83F80F                  cmp eax, 0000000F

:0040100F 753E                    jne 0040104F

:00401011 8D45C0                  lea eax, dword ptr [ebp-40]

:00401014 50                      push eax

:00401015 FF7508                  push [ebp+08]

* Reference To: USER32.BeginPaint, Ord:000Ch

                                  |

:00401018 E853010000              Call 00401170

:0040101D 8945AC                  mov dword ptr [ebp-54], eax

:00401020 8D45B0                  lea eax, dword ptr [ebp-50]

:00401023 50                      push eax

:00401024 FF7508                  push [ebp+08]

* Reference To: USER32.GetClientRect, Ord:00F0h

                                  |

:00401027 E86E010000              Call 0040119A

:0040102C 6A25                    push 00000025

:0040102E 8D45B0                  lea eax, dword ptr [ebp-50]

:00401031 50                      push eax

:00401032 6AFF                    push FFFFFFFF

:00401034 686A204000              push 0040206A

:00401039 FF75AC                  push [ebp-54]

* Reference To: USER32.DrawTextA, Ord:00AFh

                                  |

:0040103C E84D010000              Call 0040118E

:00401041 8D45C0                  lea eax, dword ptr [ebp-40]

:00401044 50                      push eax

:00401045 FF7508                  push [ebp+08]

* Reference To: USER32.EndPaint, Ord:00BBh

                                  |

:00401048 E847010000              Call 00401194

:0040104D EB31                    jmp 00401080

* Referenced by a (U)nconditional or (C)onditional Jump at Address:

|:0040100F(C)

|

:0040104F 83F810                  cmp eax, 00000010

:00401052 7514                    jne 00401068

:00401054 FF3504304000            push dword ptr [00403004]

* Reference To: USER32.DestroyWindow, Ord:008Eh

                                  |

:0040105A E823010000              Call 00401182

:0040105F 6A00                    push 00000000

* Reference To: USER32.PostQuitMessage, Ord:01E0h

                                  |

:00401061 E846010000              Call 004011AC

:00401066 EB18                    jmp 00401080

* Referenced by a (U)nconditional or (C)onditional Jump at Address:

|:00401052(C)

|

:00401068 FF7514                  push [ebp+14]

:0040106B FF7510                  push [ebp+10]

:0040106E FF750C                  push [ebp+0C]

:00401071 FF7508                  push [ebp+08]

* Reference To: USER32.DefWindowProcA, Ord:0084h

                                  |

:00401074 E803010000              Call 0040117C

:00401079 5E                      pop esi

:0040107A 5F                      pop edi

:0040107B 5B                      pop ebx

:0040107C C9                      leave

:0040107D C21000                  ret 0010

* Referenced by a (U)nconditional or (C)onditional Jump at Addresses:

|:0040104D(U), :00401066(U)

|

:00401080 33C0                    xor eax, eax

:00401082 5E                      pop esi

:00401083 5F                      pop edi

:00401084 5B                      pop ebx

:00401085 C9                      leave

:00401086 C21000                  ret 0010

这就很清楚了,按往常,首先将ebp压入堆栈,然后是调整esp给局部变量预留空间,在这里可以看出局部变量是无法在定义时初始化的,其值也不可预测。然后才是三条push指令将uses ebx edi esi中的三个寄存器保存起来。这样,堆栈看起来如下所示(STDCALL约定):

lParam

wParam

uMsg

hWnd

call返回地址

ebp

stPs

stRect

hDC

ebx

edi

esi

函数调用约定意义重大,如果不遵守STDCALL约定,那么我们便不能使用Windows API函数提供的强大功能,在设计编译器的时候十分重要。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值