Enter和Leave指令

原作者不知道,转载于[360doc] 移植好奇Enter 的第二个参数是什么意思,今天终于知道了。
enter和push。。。相比消耗更多的时钟周期,但功能多。
 
Leave指令很简单,相当于move esp,ebp和pop ebp。

  Enter SRC1,SRC2 也不复杂,只是不了解的话动态调试起来会很晕,Enter作了下面的事。

  push ebp

  mov ebp,esp

  现在栈顶上是上一个函数的基地址(就是上一个函数里的ebp,刚被压入;这个ebp很重要,习惯上进入一个子函数时,在call的时候自动压入call后面那条语句的epi,然后压入ebp来保存这个即将被改变的ebp值,然后把ebp指向当前的栈顶,这样调用这个函数领空里用到的所有本地局部变量用ebp就能找到了,所以这时ebp的值叫基址),用这个刚保存的基址,去栈的深处(也不深,也就是调用本函数的父函数的领空)把这个基址上SRC2-1个内容复制到栈顶往上的相同大小的空间里,最后再把ebp的值压上去。

  上面这段话里莫名其妙的操作正好使用了SRC2那么大的栈空间。

  sub esp,SRC1

  就走了这四步,而除了第三步,剩下那三步大家都很熟悉(如果不熟这篇文章还不是你该看的时候)并且经常自己会写的。关键是这个第三步有什么用?

  很多书上都叫第二参数是“嵌套层数”。Bingo,很正解。比如说主程序调用了proc1,而proc1调用了proc2,而proc2调用了proc3……总之突然procN要调用procM(当然N>M)中的一个局部变量,按照传统的调用子函数编写方法,这个访问实现起来简直无计可施。而如果你在proc1中用了个 enter SRC1,1 ,proc2中用了个 enter SRC1,2 ……这样就有个简单的方法了。假设procN中要被赋值的的变量是第一个,procM中要被读取的变量也是第一个,在要赋值时这样就可以:

  mov eax, DWORD PTR [ebp-4*M]

   ;把procM的基地址暂存到eax里

  push DWORD PTR [eax-4*M-4]

  ;把按这个地址找到的procM中的变量入栈(草,寄存器不够使了)

  pop DWORD PTR [ebp-4*N-4]

  ;出栈,把值赋给procN中的那个饥渴的变量。

  思考一下吧,很简单。可见要使用enter必须保证要访问的那个proc到现在所处的proc之间的一系列proc都在开头用了enter。当然,当SRC2为零时就无所谓了,而且比自己写push ebp 和 mov ebp , esp省劲的多。这样在一系列的enter作用下,到了procN时,栈中最上面的一部分是procN的领空,而这个领空的内容是这样的(从下往上):返回procN-1(调用procN的proc)的地址(call是epi应有的内容);刚进入procN的ebp,即procN-1的基址;大小为SRC2*4的一段指针列表(指向前面各个用了enter的proc的基址),这段类表的原理就是递推的方法,其中SRC2*4-4的内容是从procN-1的领空中复制的,为了能让procN+1也能用enter创建一断指针列表,列表最上面也要压入指向procN自己基址的指针,虽然在procN中,这最后一个没什么用处;然后是依据SRC1开辟的本地局部变量区域。其实这个结构有个名字,堆栈桢,貌似《编译原理》里会学到这个概念。利用堆栈桢,就可以方便的访问之前嵌套了自己的各辈函数的本地局部变量。总之,要使用这个功能,必须保证一路上都用了enter,否则即使procN用了enter,在复制procN-1的指针段的时候复制的东西根本不是指针,而是,比如procN-1的本地局部变量什么的(因为procN-1根本没有这么一段指针表啊)。

下面给段代码,可以动态调试一下便于理解

    .486                                ; create 32 bit code
    .model flat, stdcall                ; 32 bit memory model
    option casemap :none                ; case sensitive


    .data


    .code
proc5 proc
 enter 8,5
 mov DWORD PTR [ebp-4-4*5],54
 mov eax,DWORD PTR [ebp-4-4*0]
 push DWORD PTR [eax-4-4*1]
 pop DWORD PTR [ebp-4-4*5-4] 
 ;这里把第二个本地变量设为proc1的值 
 leave
 ret 
proc5 endp
proc4 proc 
 enter 4,4
 mov DWORD PTR [ebp-4-4*4],53
 call proc5
 leave
 ret 
proc4 endp
proc3 proc
 enter 8,3
 mov DWORD PTR [ebp-4-4*3],52
 mov eax,DWORD PTR [ebp-4-4*1]
 push DWORD PTR [eax-4-4*2]
 pop DWORD PTR [ebp-4-4*3-4] 
 ;这里把第二个本地变量设为proc2的值 
 call proc4
 leave
 ret 
proc3 endp
proc2 proc 
 enter 4,2
 mov DWORD PTR [ebp-4-4*2],51
 call proc3
 leave
 ret 
proc2 endp
proc1 proc 
 enter 4,1
 mov DWORD PTR [ebp-4-4*1],50
 call proc2
 leave
 ret 
proc1 endp
start:
 call proc1  
 ret
end start
 

通常在函数调用中使用堆栈来传递参数,保存函数返回地址和为自动变量分配内存。

通常在进入函数中时有两条命令,如下:

push ebp  ; 保存上一个函数的栈帧基地址

mov ebp,esp ; 设置新的函数栈帧基地址

在返回函数前通常有如下两条指令:

mov esp,ebp ; 将当前函数栈帧基地址保存到esp中

pop ebp ; 恢复上一个函数的栈帧基地址

这是前奏。。之后Intel又设计了两条指令来简化上面的两个步骤,那就是ENTER和LEAVE指令。

我先说LEAVE指令吧。。这条指令就相当于 mov esp,ebp 和 pop ebp 两条指令的执行效果。

而ENTER指令要麻烦一点。我先将它的指令格式列出来:

ENTER A,B 

A表示的是在栈上分配的临时变量的空间大小,B表示词法嵌套级(我英文不好,原英文是:lexical nesting level)

A很容易理解,就是sub esp,N之类的指令,用来给当前函数的栈帧分配局部变量空间。

B我也不太明白啥意思。不过没关系,一般这个值为0。

举个例子吧,例如:ENTER 4,0 相当于如下三条指令:

push ebp

mov ebp,esp

sub esp,4

就这么简单!但是这条enter指令也有一个先天上的不足,那就是速度慢。所以一般编译器生成代码时很少使用enter指令。还是用以前的老方法,倒是LEAVE指令经常被用到。

posted @ 2014-01-13 11:23  BinSys  阅读(2212)  评论(0编辑  收藏  举报