子程序的常见调用方式:
void myadd(int x, int y)
{
x=x+y;
} 这个是一段最简单的子程序。
如果用程序的方法调用就是 myadd(5,3); 用汇编的方法就是 Push 3 Push 5 Call *******(myadd的地址) 注:这里的汇编调用写法按照_cdecl 调用约定 调用。什么是调用约定。 请百度 或者谷歌 调用约定。相信你能找到很多资料。 1、__stdcall调用约定:函数的参数自右向左通过栈传递,被调用的函数在返回前清理传送参数的内存栈, 2、_cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式。注意:对于可变参数的成员函数,始终使用__cdecl的转换方式。 3、__fastcall调用约定:它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈)。 4、thiscall仅仅应用于"C++"成员函数。this指针存放于CX寄存器,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。 5、naked call采用1-4的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。naked call不产生这样的代码。naked call不是类型修饰符,故必须和_declspec共同使用。 注:以上资料皆来自网上。 一般来说游戏都是使用C或者C++写的。绝大部分都是使用_cdecl调用约定 来编译游戏。 所以一般所指的无参CALL就是 没有通过任何寄存器,堆栈传递参数。更严格的说法就是CALL内部没有调用上一层代码中寄存器传入的参数。 如 武林的 无参数打坐CALL。即使你在调用之前加入 Push 0 Push 1 Call ******* Add esp,4 或者 Mov eax,100 Call ******* 也是同样可以调用的。为何?因为游戏打坐子程序内部根本就没理你传入的参数(0,1和100) 。 或者说在汇编里是 Push 0 Push 1 Call ******* Add esp,4 但如果CALL内部并未调用汇编传入的参数。那么我们只需要写CALL*********便可以。 所以,看一个反汇编CALL中是否是有参数CALL,是要看CALL内部是否调用了传入的参数。而不是看CALL指令之前的传参指令。 我们的最终目标是写出CALL的调用代码,而我们要写出CALL则要写出CALL所需要的参数。之前我说过,CALL的参数可能由堆栈和寄存器来传递。那么我们如何知道 哪些是CALL所需要的参数呢? 首先是堆栈,一般在指令CALL之前查看,反汇编是不会吃饱撑着 写着10行的push 而调用一个。(当然不能排除哪些空的发闲的程序员)还有一种是看CALL内部的尾部的retn 指令。比如retn 8 则恢复2个参数所用到的堆栈内存。在32位CPU中一个堆栈所占用的内存是4字节。故push 一个参数所用到的堆栈内存就是4字节。 当然也不排除一些特殊的CALL。 如: 005CCC03 |. B8 04000000 mov eax, 4
005CCC08 |. 8D5424 00 lea edx, dword ptr [esp]
005CCC0C |. 894424 00 mov dword ptr [esp], eax
005CCC10 |. 894424 04 mov dword ptr [esp+4], eax
005CCC14 |. 8D4424 04 lea eax, dword ptr [esp+4]
005CCC18 |. 83C1 10 add ecx, 10
005CCC1B |. 50 push eax
005CCC1C |. 52 push edx
005CCC1D |. C74424 10 000>mov dword ptr [esp+10], 0
005CCC25 |. E8 F6CF1100 call 006E9C20
005CCC2A |. 8B40 04 mov eax, dword ptr [eax+4]
005CCC2D |. 8B08 mov ecx, dword ptr [eax]
005CCC2F |. 8B50 04 mov edx, dword ptr [eax+4]
CALL 前面我们可以看到有2个push 。 所以我们可以先把堆栈处理了。 Push eax Push edx Call 6e9c20 一开始记住了,不要理会任何数据。先把大致的CALL模型写出来。 然后我们就要看CALL调用了哪些寄存器了。我们如何知道CALL内部调用了哪些寄存器呢? 一般学过外挂方面的知识的朋友大概都是 汇编指令 mov 的 实现的功能.
mov 操作数1,操作数2
将操作数2 的值放入到操作数1 里.
如: mov eax,ebx
将 ebx的值放入到eax里 也就是读取了EBX的值。并放入EAX。 寄存器的作用大家都知道是用来存放数据供CPU调用.所以寄存器本身是空的.
在调用一个CALL的时候 所有的寄存器都等于0的或者是调用上一个CALL遗留下来的残留数据.也就是说寄存器本身等于0,他只是一个用来存放数据的空间。相当于一个空的仓库并等你存放。
设: 假如: Void my(int a) { _asm { Mov eax,[a] } } 这里我们可以这样调用 Int a; My(&a); 而我们用汇编调用则是 Lea ecx,a Push ecx Call my 当我们如果直接调用CALL,会出现什么情况呢? Call my 那么这样就会出现内存无法读取的错误。 因为这个CALL所需要的是一个指针地址。我们如果事先不先传入参数的话。那么CPU就有可能访问 0或者一些不存在的内存地址。或者存在但无法读取的地址。 那么如果我们这样调用是否会出错? Mov ecx,65BF00 (假设 [65bf00]=100) Push ecx Call my 那么这个调用方法是不会出错的。因为这个程序需要的是一个可读的指针地址。 那么这样调用是否会出错呢? Mov ecx,65bf00 Call my 这样还是会出错的,因为CALL读取的是堆栈里的数据,而不是读取ECX里的数据。 那么这样会如何呢? Push 65bf00 Call my 这样是正确的,因为CALL内部读取的是堆栈的指针,而不会检查你到底是如何传入这个参数的。CALL只会从指定位置读取参数,并不会考虑这个是如何来的。 在反汇编里程序的调用代码为
mov eax,5
mov ebx,4
push eax
push ebx
call 5e0000 //假设 CALL的地址为5e00007 add esp,8
retn
你会如何写这个CALL呢?
如果CALL内部没有调用EAX和EBX值的话
我们的调用代码则可以这样写
push 58
push 47
call 5e00009
add esp,8
而不需要把58放入eax里 把47放入ebx里.
为何?因为CALL并没有读取eax 和ebx里的数据.
CALL内部需要的是 堆栈里的58 和47的2个参数。
在调用一个CALL的时候 所有的寄存器都是空的或者是调用上一个CALL遗留下来的残留数据.
如果这个CALL需要一个200的值,通过ebx 储存.
那么我们调用CALL而不给ebx赋值 ,调用的时候CALL还是会读取当前ebx的值,而这个时候寄存器的值则是0或者上一个CALL调用后残留数据.而不是CALL想要的200数据.
调用CALL之所以需要寄存器,是因为CALL通过调用相关的寄存器获取到特定的数值.
而CALL调用寄存器的语句常用则有 PUSH 寄存器, mov 寄存器,寄存器 lea 寄存器,寄存器 add 寄存器,寄存器等等. CALL参数的传递 总所周知,寄存器是用来传递参数的,但本身并没有值,他只是一个存放值的空间,存放用来传递的参数.CPU执行一个CALL的时候,我们可以把几个寄存器看成是一个空的空间.那么我们就可以理解,并不是所有的寄存器都会拿来存放数据的.看CALL需要传递几个参数了.
寄存器除了传递参数外,另外一个功能就是存放临时数据,如何判断一个CALL调用了那么寄存器的参数.你要会判断这个寄存器是用来传递的还是用来临时存放数据的.在分析CALL之前我们应该把寄存器都看成空的 除了ESP EIP2个特殊的寄存器外.
当你发现 mov eax,ebx的时候 EBX是否是CALL要调用的寄存器呢?我觉得这个要看寄存器是否有值(在假设寄存器都为空的情况下). 如果mov eax,ebx 前面没有给EBX赋值的指令的话那么 这个ebx就是CALL需要调用的寄存器. 也就是用来传递参数的寄存器. 那么EAX呢?
这里的EAX就是一个临时存放的寄存器.用来存放ebx里的数据.
如果mov eax,ebx 指令前面有给ebx赋值的话, 比如说mov ebx,123123 那么ebx也可以排除了 因为他用来存放临时数据了 .并不是用来传递数据的.
当然,在调试的情况下,调用一个CALL之前寄存器不可能为空,大部分都有之前CALL遗留下来的残留数据.
还有一种是堆栈传递.这个就不相信说了,只要看下调用CALL前的PUSH指令就能很好的得到.
b jAnaya 寄存器环境保护
这里我们要提一下寄存器环境保护,众所周知,CPU的寄存器只有一个EAX ,EBX,ECX,EDX...... 如果一个寄存器EAX里存放着一些资料供后面使用,但当前CALL却需要EAX储存一些临时的数值这个时候要怎么办?
这个时候我们则需要把寄存器EAX里的数值保存到一个地方,然后把EAX给CALL使用 用完后在把那个值放
回到寄存器EAX里去. 这个过程则是寄存器的环境保护. 在反汇编里保存寄存器的地方就是堆栈.
当你在一个子程序头部看到一些 push eax 而又在尾部看到 pop eax 的时候 这里的eax 就是寄存器数值保护,push eax 则是保存eax储存的数值, pop eax则是放回去.
那么这个push eax 则是被保护的寄存器。
例子:
如上图就是一个CALL的内部。首先我们可以看到第一句 读取了[esp+4] 的数据。 指令的意思大家可能都知道,读取了堆栈顶部地址+4 所在的数据,也就是读取了上一层指令CALL最后一个push指令所压入的参数。 下面 有一个push esi 这里其实是做寄存器保护的。因为下面用到了ESI这个寄存器。所以必须先把ESI原有的数据保存起来。而下面[esp+c]则是读取第二个,因为前面push esi esp要加上4.我们继续看下面 mov eax,[ecx+98] 这里读取了ECX的值,但上面没有给ECX赋值,所以这里我们需要给ECX赋值。这里的ECX就是我们所说的CALL需要的寄存器。而下面的MOV ECX,[eax+1c] 中的EAX呢?这里的EAX则不是CALL需要的寄存器,因为上面已经给EAX赋值了,所以EAX为空变为有数据。下面的EDX也是一样。 从retn 8 可以看出这个CALL压入了2个堆栈。而上面的指令也很好的说明了这一点。 所以我们可以从CALL的内部就可以写出这个参数。 Push 参数 Push 参数 Mov ecx,参数 Call 8AB3A0 有些人认为写CALL一定要按照游戏调用CALL的代码来写.其实,不是的.游戏调用CALL的代码只是用来参考的.我们只需要写出CALL所需要的寄存器即可. 这个是很多人存在的一个误区.只要你能理解这个概念那么CALL就会变的简单明了.
记住~游戏中调用CALL的代码只是一个参考的依据.
何为调用CALL?何为CALL内部? 新手经常会遇到一个奇怪的问题。断在游戏CALL的地方,用程序调用却不会断下来。而在游戏里执行动作却会断下来。 OD之所以会断下来,那是因为CPU执行了这个指令。那么就会很明白了。还是那个例子 viod myadd (int a, int b)
{
int c=a+b;
} 当游戏调用的时候 CPU执行代码会从 程序本身的代码执行 Myadd(6,2) <-----在这里下段,在游戏执行动作则会从这里开始断下 viod myadd (int a, int b) <-----然后是这里
{
int c=a+b;
} 而我们用程序调用的 则是 Push 2 <-------开始从这里运行 Push 6 Call myadd <----直接跳到 myadd子程序内部 Myadd(6,2) <-----在这里下段,我们自行调用的时候却不会执行到这里 viod myadd (int a, int b) <-----直接运行到这里了 {
int c=a+b;
} 也就是说 我们用程序调用的CALL是从其调用代码开始的,而不是从游戏的调用代码. 调用代码指 调用子程序的指令CALL. CALL内部指 整个子程序的代码.
游戏调用CALL时,值的内存传递方式。 :IlJQ{=W
<:
UP
除了堆栈和寄存器以外,游戏可能会用其他的方法传递数值,最常见的就是内存。写过游戏聊天CALL的朋友可能会遇到过频道信息放在内存里供CALL调用的。一般是在喊话CALL内部调用,用CE找到频道信息的地址吧。 当然这里既然在CALL内部调用了频道在内存里的值,也可以通过频道信息来找到喊话CALL的地址。
|