关于函数调用得到传递参数的想法(汇编应用)原贴地址:
http://suiyunonghen.blogspot.com/
不得闲于 2008-3-29
起源:
近日来,一直致力于扩充公司的脚本解析引擎!脚本引擎的大致扩充功能如下:
1、可在脚本中由用户自定义函数功能了,原来只有一个程序入口所有代码都在Begin..End之间写
Begin
//代码脚本
end
现在分成主脚本入口(既原Begin..End)与函数定义如
function GetMax(x,y: integer): integer;
begin
if x > y then
result := x
else
result := y;
end;
begin
MessageBox(GetMax(2,3));
end;
2、可支持在脚本中动态创建对象于对象之间传递引用
原引擎只能固定的使用一些对象,其间不可赋值与引用如
在Delphi中固定好一个对象 Button,则在脚本中使用就只能使用
Button.Caption := 'sdf';
而不支持 B := Button; 然后操作B;
现在扩充如:
MyBtn := TButton.Create(self);//支持了动态创建
MyBtn.caption := 'test';
MyBtn.parent := self;
CBtn := MyBtn;//支持了引用
MessageBox(CBtn.Caption);
3、可支持在脚本中使用编译预处理指令了如
{$I MyFuncLib} //引用外部库,相当于Include
{$IFDEF gg}
{$ELSEIFDEF bb}
{$ENDIF}
{$IFNDEF cc}
等编译预处理指令
现在问题就出现于第四点的实现方式上了!
那就是 动态的指定对象的事件了!
此时,由于事件的类型有很多种!如果一个个的定义下来处理的话,实在太过繁琐!所以想到使用一个函数来实现
调用所有对象事件的实现!而这些对象事件的的实现代码出现在脚本中,所以所有的事件执行都通过一个函数
DoEvent来实现,该函数中只处理某对象事件属性对应的函数脚本。那么就需要传递触发该事件的时候传递到事件中的
事件参数了!于是一个如何得到传递参数值的想法产生!
实践!
定义两个窗体,Form1,form2,在Form1中定义一个事件
TTestEvent = procedure(Sender: TObject;x,y,z: integer) of object;
property OnTest: TTestEvent read FonTest Write FOnTest;
然后在窗体创建的时候设定OnTest属性的值!
var
Method: TMethod;
begin
MeThod.Data := Sender;
Method.Code := @Test;
SetMethodProp(self,'OnTest',Method)
end;
Test过程没有任何参数,此时的目的就是要在Test中得到由触发该事件的时候传递过来的各个参数的值!
如
procedure Test;
begin
//这里要求得到参数值!
end;
一段触发该事件的代码如下:
Form1.OnTest(form1,1,2,3);
打个断点。运行到这里的时候打开CPU窗口得到如下代码:
{说明:对于一般的函数或过程,前三个参数分别放在 EAX、EDX、ECX,后面如果还有更多参数的话,就在堆栈
里面;对于类的方法,EAX 固定用于存放类实例的地址,EDX、ECX 分别存放前两个参数,其余参数进栈。在堆
栈中每个元素占用 4 Bytes,而前面说了,TVarRec 中储存的数据也是 4 Bytes,刚好一个参数在堆栈里面占
一个位子,处理方便。另外,结果返回到 EAX 中。
对于调用类的方法,其实有一个默认的隐藏参数 Self 作为第一个参数传入,放入EAX寄存器。
因此我们看到的第一参数其实是第二个,因此我们处理的时候要注意。}
Unit2.pas.36: Form1.OnTest(form1,1,2,3);
00461481 6A02 push $02 //这里可知道是第三个参数压栈,2
00461483 6A03 push $03 //第四个参数压栈,3
00461485 8B1DA83C4600 mov ebx,[$00463ca8] //这里的[$00463ca8]则为存放form1的地址的值
0046148B 8B1B mov ebx,[ebx] //取得form1的值
0046148D 8B15A83C4600 mov edx,[$00463ca8]
00461493 8B12 mov edx,[edx]//这一句和上面一句,完全可用 mov edx,ebx来代替,就是传递入form1参数值
00461495 B901000000 mov ecx,$00000001 //这里是第二个参数的值
0046149A 8B8304030000 mov eax,[ebx+$00000304]//本是作为form1的self指针传递,其实这里是按扭事件中的Button的值
//也就是OnTest事件转化为TMethod后中的Data数据段
{TMethod = record
Code, Data: Pointer;
end;如此可知道其代码还是指针偏移位置为$00000304-4则为下面的$00000300}
004614A0 FF9300030000 call dword ptr [ebx+$00000300] //调用Tmethod中的Code函数,该处程序指令执行的位置为
//EIP = 004614A0 注意调用了该方法之后堆栈的变化 ,此时程序为了在调用了该事件处理函数之后能够正确的处理下一条
//指令,所以必须将当前的指令位置的下一条指令位置保存起来,所以调用事件处理函数的同时,会将当前指令指针EIP的值
加上当前指令的长度作为下一条指令位置压入栈保存起
//来,所以此时又有一个入栈的操作了。
Unit2.pas.37: end;
到这里整个触发过程完成!此时程序接受Call指令后进入到另一个函数栈空间,我们为了在另一个函数的栈空间中得到事件
触发时候传递过来的参数,就必须要记下参数的位置了。
通过上面可知道,其共传递了4个参数,由于隐含一个Self操作,也就是说传递了5个参数
其操作过程为,三个在寄存器中,另外两个参数存在于堆栈中!由于调用函数要保存当前指令运行位置便于返回!所以此时
多进行一次入栈操作。
下面进入到 Test过程代码分析段,
程序进入到了Test段,先得到最后两个参数(堆栈中的参数)
通过上面分析,此时必须要将当前的栈顶指针向后移动一个才可访问到最后一个参数
//初步写下如下试验代码
procedure Test
begin
Pop Eax
Pop Eax //此时则得到了最后一个参数的值了
end;
如果这样写的话,就存在一个很大的问题了,如果全部出栈,出栈,那么调用事件处理函数之前入栈的事件处理函数完成之后
的下条指令执行的位置丢失了!所以,在出栈其他参数之前,务必要先将调用事件处理函数完成之后的指令位置保存起来,才
可出栈!而且将入栈的参数全部出栈得到之后,再将刚刚出栈的下条指令地址入栈,就能使程序ret时候得到正确的指令执行
位置了。代码如下:
procedure Test;
begin
Pop EDX //指令执行位置保存到ESI
Pop EAX //最后一个参数3
Pop EAX //倒数第二个参数2
Push EDX //指令执行位置入栈,然后下面Delphi自动调用一个ret过程,也就是一个出栈的过程弹出下条执行指令位置
ret //该句可加可不加
end;
如此看来,通过这里可看出ret指令其实包含有两个过程,首先 出栈执行指令位置,然后跳转到那个位置,那么代码应该是
如下方式:
Pop NextAddr
Jmp NextAddr
分析透了,所以上面的Test函数还可再减化一下,都是跳到下个地方执行,所以直接使用跳转指令jmp来改变EIP位置
代码如下:
procedure Test;
begin
Pop EDX
Pop EAX
Pop EAX
Jmp EDX //这样就省去了他的一个入栈和一个出栈操作而直接跳转
end;
至于其他的几个参数还是在EAX,EDX,ECX中,如果你不动的话!
将传递的参数的值显示出来,那么如果要显示的话,就需要定义局部参数了!
定义了局部参数,则栈空间就会增加,然后将声明的局部变量分别入栈。
想想要使程序不报错!就必须得使得调用事件出来函数前后的栈平衡,所以就需要将前面入的参数
全部Pop弹出来,来改变堆栈栈顶指针。以达到堆栈平衡。
procedure Test
var
x,y,z: integer;
begin
//这里声明了三个变量x,y,z,一般做操作如下:
{Push Ebp //触发调用该函数的EBP值!也就是那个函数的入口栈基址。入栈保存
Mov Ebp,Esp //然后将当前的函数的入口栈基址给EBP保存
Add ESP,-12 //将堆栈扩充3个变量的位置
Push X
Push Y
Push Z //然后再将局部变量入栈
如此的话,则可通过基地址EBP来得到堆栈中的参数值了
}
asm
push esi //保存Esi值,注意了,此时又添加了一个栈变量了Esp在原来的基础上减4
mov esi,[ebp+8] //Ebp将自己入栈之后才是本入口栈的基地址,所以[Ebp+4]为程序返回跳转指令的位置,
mov z,esi
mov esi,[ebp+12] //同理类推
mov y,esi
mov x,ecx //由于x是第三个参数值,所以没有入栈
pop esi //返回上面的保存的esi内容的值
mov ecx,ebp
sub ecx,esp //此时ecx保存着基本地址和栈顶指针的差距,用来后面进行出栈循环用
end;
ShowMessage(IntToStr(x)); //得到的x值
ShowMessage(IntToStr(y)); //得到的y值
ShowMessage(IntToStr(z)); //得到的z值
//这个函数由于没有参数,而为了达到堆栈平衡,所以下面必须使用代码来手动达到目的
asm
@PopUp:
pop eax //根据基地址与当前地址的距离进行出栈,使堆栈到达基地址的位置。
sub ecx,4
jnz @PopUp //为0了则说明到达本函数的入口基栈地址了
pop ebp //由于在最开始初始化的时候压入了该函数的返回后地址,所以这里出栈该地址到ebp中
//此时已经回到了该函数的入口地址时候了,然后再由于传递过来的两个参数入栈占据了8大小的位置
//所以这里直接使用 ret 8则可返回到当前的堆栈位置+8的地方那就是原来触发函数的位置了。
ret 8
end;
end;
总体归纳说来的话,就是出栈到函数的入口基址,然后ret 到其入栈参数所占据的空间
然而最后函数地址返回那里 ,我使用
push ebx //用这段注释代码却也可通过,只是不知道原因何在了!
push [ebp+4]
ret
也可通过!只是不晓得,其原因在哪里了!