Windows 系统编程初探 (三)栈(Stack)的奥秘

        说得直白一点,栈就是一段内存!对这段内存的访问规则是后进先出(LIFO),如果只从CPU的设计角度来

看,栈的功能只有一个:为了支持函数的嵌套和递归,保存函数调用时的返回地址。当然,这只是我的一点浅

薄认识。在 X86 芯片上,函数的调用指令和返回指令分别是 CALL 和 RET,为了看清楚调用和返回动作,下

面我将这两条指令拆解一下。

  1. 用 CALL 和 RET 实现函数调用和返回
     _Sub PROC
      ADD  EAX,EBX
      RET
     _Sub ENDP
     
     _Main PROC
      MOV  EAX,1234
      MOV  EBX,5678
      CALL _Sub
      ...
     _Main ENDP
     
  2. 用 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


        以上代码演示的是栈的最基本的功能,在高级语言中,通常还用栈来传递参数和分配临时变量,编译器为

我们完成了所有的这些工作,如果将一个简单的函数反编译过来,我们就可以清楚地看到这些细节。

  1. 一个简单的 C 函数
     void Swap(int * lpA,int * lpB){
            int nTemp = * lpA;
            * lpA = * lpB;
           * lpB = nTemp;
     }
  2. 编译器的编译结果
     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 函数为例说明一下:

  1. __stdcall:(绝大多数 Win API 都采用该规则编写)
     void __stdcall Swap(int * lpA,int * lpB);
     
     函数格式:
      Swap PROC
       ...
       ;函数返回时修正堆栈
       RET  8
      END  PROC
     
     调用方法:
      PUSH lpB
      PUSH lpA
      CALL Swap

      
  2. __cdecl:(C/C++ 中的默认规则)
     void __cdecl Swap(int * lpA,int * lpB);
     
     函数格式:
      Swap PROC
       ...
       ;函数返回时不做堆栈修正
       RET
      END  PROC
      
     调用方法:
      PUSH lpB
      PUSH lpA
      CALL Swap
      ;函数返回后修正堆栈
      ADD  ESP,8


  3. __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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值