我们经常见到类似“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函数提供的强大功能,在设计编译器的时候十分重要。