说得直白一点,栈就是一段内存!对这段内存的访问规则是后进先出(LIFO),如果只从CPU的设计角度来
看,栈的功能只有一个:为了支持函数的嵌套和递归,保存函数调用时的返回地址。当然,这只是我的一点浅
薄认识。在 X86 芯片上,函数的调用指令和返回指令分别是 CALL 和 RET,为了看清楚调用和返回动作,下
面我将这两条指令拆解一下。
- 用 CALL 和 RET 实现函数调用和返回
_Sub PROC
ADD EAX,EBX
RET
_Sub ENDP
_Main PROC
MOV EAX,1234
MOV EBX,5678
CALL _Sub
...
_Main ENDP
- 用 PUSH、POP、JMP 实现函数调用和返回
_Sub PROC
ADD EAX,EBX
;取出返回地址,跳转过去
POP ECX
JMP ECX
_Sub ENDP
_Main PROC
MOV EAX,1234
MOV EBX,5678
;返回地址入栈,跳转至子函数
PUSH OFFSET _ReturnAddress
JMP _Sub
_ReturnAddress:
...
_Main ENDP
以上代码演示的是栈的最基本的功能,在高级语言中,通常还用栈来传递参数和分配临时变量,编译器为
我们完成了所有的这些工作,如果将一个简单的函数反编译过来,我们就可以清楚地看到这些细节。
- 一个简单的 C 函数
void Swap(int * lpA,int * lpB){
int nTemp = * lpA;
* lpA = * lpB;
* lpB = nTemp;
} - 编译器的编译结果
Swap PROC
;以下两条指令是高级语言编译器的常用手法,
;保存EBP的原始值,用EBP来访问栈(参数和临时变量)
;这也是为了实现函数的嵌套、递归。
PUSH EBP
MOV EBP,ESP
;这时候栈的状态应该如下:
;[EBP + 12] = lpB
;[EBP + 8] = lpA
;[EBP + 4] = return address
;[EBP] = EBP 原始值
;int nTemp
;这一条指令就是分配临时变量
SUB ESP,4
;nTemp = * lpA,分解为三步。
;EAX = lpA
;EBX = * lpA
;nTemp = EBX
;这里尤其需要注意:
;1.ASM 怎样访问指针变量
;2.内存之间不能直接传递数据,需要以寄存器为中介
MOV EAX,DWORD PTR [EBP + 8]
MOV EBX,DWORD PTR [EAX]
MOV DWORD PTR [EBP - 4],EBX
;*lpA = * lpB
MOV EBX,DWORD PTR [EBP + 12]
MOV ECX,DWORD PTR [EBX]
MOV DWORD PTR [EAX],ECX
;* lpB = nTemp
MOV EAX,DWORD PTR [EBP - 4]
MOV DWORD PTR [EBX],EAX
;释放临时变量,按理说这条指令大可不必,
;但我跟踪过的函数中都有
ADD ESP,4
;恢复ESP
MOV ESP,EBP
;恢复EBP
POP EBP
;返回
RET
Swap ENDP
下面我将要总结一下几个函数调用规则。 函数的调用规则包括参数的传递和堆栈得修正,在编写回调函数
(CALLBACK)和调用 WinAPI 的时候必须遵循系统约定的调用规则,否则肯定会导致异常!在这里,我只总结
几个常见的调用规则。
规则名称 参数传递 堆栈修正
__stdcall 从右向左传 被调函数修正堆栈
__cdecl 从右向左传 调用者修正堆栈
__pascal 从左向右传 被调函数修正堆栈
还是以上面的 Swap 函数为例说明一下:
- __stdcall:(绝大多数 Win API 都采用该规则编写)
void __stdcall Swap(int * lpA,int * lpB);
函数格式:
Swap PROC
...
;函数返回时修正堆栈
RET 8
END PROC
调用方法:
PUSH lpB
PUSH lpA
CALL Swap
- __cdecl:(C/C++ 中的默认规则)
void __cdecl Swap(int * lpA,int * lpB);
函数格式:
Swap PROC
...
;函数返回时不做堆栈修正
RET
END PROC
调用方法:
PUSH lpB
PUSH lpA
CALL Swap
;函数返回后修正堆栈
ADD ESP,8 - __pascal:(Win API 和 C/C++ 中很少使用,Pascal 的默认规则)
void __pascal Swap(int * lpA,int * lpB);
函数格式:
Swap PROC
...
;函数返回时修正堆栈
RET 8
END PROC
调用方法:
PUSH lpA
PUSH lpB
CALL Swap