|
楼主 |
发表于 2020-3-6 21:32:20
|
显示全部楼层
本帖最后由 pukr 于 2020-3-6 21:33 编辑
2020-03-06
C语言内嵌汇编实现模拟函数调用
内嵌汇编,目的是理解函数调用过程中堆栈的变化。
环境:VC++6.0 特征指令程序通过调用程序来调用函数,在程序执行后又返回调用程序继续执行。函数的返回地址随参数压栈,一起传给调用函数。有多种方法可以实现,最常见的是call/ret指令。 #实例分析
交换函数。 C语言代码- #include<stdio.h>
- void swap(int *m,int *n);
- void main(){
- int a=2;
- int b=3;
- char *str1="a=%d,b=%d\n";
- printf(str1,a,b);
- swap(&a,&b);
- printf(str1,a,b);
- }
- void swap(int *m,int *n){
- int temp;
-
- temp=*m;
- *m=*n;
- *n=temp;
- }
复制代码 效果:
汇编实现函数直接调用方式使程序变得简单。但也有例外,程序间接调用函数,即通过寄存器传递函数地址或者动态计算函数地址调用: 汇编代码
- #include<stdio.h>
- //void swap(int *m,int *n);
- void main(){
- int a=2;
- int b=3;
- char *str1="a=%d,b=%d\n";
- printf(str1,a,b);
- __asm{
- lea eax,dword ptr [ebp-8h]
- push eax
- lea ecx,dword ptr [ebp-4h]
- push ecx
- call swap
- add esp,8
- mov edx,dword ptr [ebp-8h]
- push edx
- mov eax,dword ptr [ebp-4h]
- push eax
- push str1
- call printf
- add esp,0ch
- pop edi
- pop esi
- pop ebx
- mov esp,ebp
- pop ebp
- retn
- swap:
- push ebp
- mov ebp,esp
- sub esp,44h
- push ebx
- push esi
- push edi
- lea edi,dword ptr [ebp-44h]
- mov ecx,11h
- mov eax,0xCCCCCCCC
- rep stos dword ptr es:[edi]
- mov eax,dword ptr [ebp+8h]
- mov ecx,dword ptr [eax]
- mov dword ptr [ebp-4h],ecx
- mov edx,dword ptr [ebp+8h]
- mov eax,dword ptr [ebp+0ch]
- mov ecx,dword ptr [eax]
- mov dword ptr [edx],ecx
- mov edx,dword ptr [ebp+0ch]
- mov eax,dword ptr [ebp-4h]
- mov dword ptr [edx],eax
- pop edi
- pop esi
- pop ebx
- mov esp,ebp
- pop ebp
- retn
- }
- }
复制代码
原理参数压栈,然后返回地址压栈,寄存器压栈,分配内存空间,空间清C。之后变量赋值,程序开始。最后清理堆栈,堆栈平衡。
函数参数函数传参有三种方式:
⊙ 栈方式(明确参数入栈顺序,并约定堆栈平衡方式)
⊙ 寄存器方式(存在哪个寄存器中)
⊙ 通过全局变量进行隐含参数传递的方式 利用栈传递参数堆栈先进后出,栈顶指针esp指向第一个可用数据。调用函数时参数依次入栈,然后调用函数。调用后,从堆栈中取数据并进行计算,然后由调用者本身恢复堆栈,使堆栈平衡。 调用约定
[td]约定类型 | __cdecl | pascal | stdcall | Fastcall | 参数传递顺序 | 从右到左 | 从左到右 | 从右到左 | 使用寄存器和栈 | 平衡堆栈 | 调用者 | 子程序 | 子程序 | 子程序 | 允许使用VARARG | 是 | 否 | 是 |
⊙ C规范(即cdecl)是C和C++默认调用约定。
⊙ stdcall是Win32API采用的约定方式。其中wsprintf采用cdecl。
[td]__cdecl | pascal | stdcall | push par3 | push par1 | push par3 | push par2 | push par2 | push par2 | push par1 | push par3 | push par1 | call test | call test | call test | add esp,0c | |
一般通过ebp来存取栈,以上面的swap函数为例。
(1)当前esp为K。
(2)根据stdcall,b先入栈,此时esp移动一个存储单元到K-04h。
(3)a入栈,esp继续移动一个存储单元至K-08h。
(4)call指令。执行call指令会把call下一个地址压栈,即返回地址。此时esp为K-0Ch。
(5)为保证恢复时的ebp,所以把ebp先push再pop。此时esp是K-10h。
(6)mov ebp,esp。ebp作为基址指针,[ebp+8]是参数一,[ebp+C]是参数二。
(7)sub esp,xxx为定义局部变量。局部变量一[ebp-4],局部变量二[ebp-8]。调用结束时通过add esp,xxx释放局部变量占用的栈。即子函数调用完局部变量就不在有作用。
(8)ret 8表示先ret再add esp,8。 此外,enter和leave指令可以帮助对栈的维护。 - enter语句相当于:
- push ebp
- mov ebp,esp
- add esp,xxx
复制代码- push b
- push a
- call swap
- add esp,8
- swap:
- mov eax,dword ptr [esp+04h]
- mov ecx,dword ptr [esp+08h]
- ...
- retn
复制代码 利用寄存器传参一般没有标准。但大多数编译器都在不对兼容性进行声明的情况下遵循相应规范,即Fastcall规范。
不同编译器稍有不同。VC++6.0规定左边两个不大于4字节的参数分别在ecx和edx中,寄存器用完后,其余参数从右至左入栈。浮点值、远指针、__int64类型总是通过栈传参。
Borland Delphi/C++编译器左边三个不大于四字节的参数分别存在eax,edx,ecx中。寄存器用完后,其余参数按照pascal约定从左至右入栈。
另一个编译器Watcom C通过寄存器传参。依次使用eax,edx,ebx 实际上可以指定任意寄存器。在不指定的情况下通过Fastcall约定完成。 利用寄存器而不是堆栈传参:
- #include<stdio.h>
- void main(){
- int a=2;
- int b=3;
- char *str1="a=%d,b=%d\n";
- printf(str1,a,b);
- __asm{
- lea edx,dword ptr [ebp-8h]
- lea ecx,dword ptr [ebp-4h]
- call swap
- push eax
- push ecx
- push str1
- call printf
- add esp,0ch
- pop edi
- pop esi
- pop ebx
- add esp,4ch
- mov ebp,esp
- pop ebp
- retn
- swap:
- push ebp
- mov ebp,esp
- sub esp,4ch
- push ebx
- push esi
- push edi
- push ecx
- lea edi,dword ptr [ebp-4ch]
- mov ecx,13h
- mov eax,0xCCCCCCCC
- rep stos dword ptr es:[edi]
- pop ecx
- mov dword ptr [ebp-08h],edx
- mov dword ptr [ebp-04h],ecx
- mov eax,dword ptr [ebp-04h]
- mov ecx,dword ptr [eax]
- mov dword ptr [ebp-0ch],ecx
- mov edx,dword ptr [ebp-04h]
- mov eax,dword ptr [ebp-08h]
- mov ecx,dword ptr [eax]
- mov dword ptr [edx],ecx
- mov edx,dword ptr [ebp-08h]
- mov eax,dword ptr [ebp-0ch]
- mov dword ptr [edx],eax
- pop edi
- pop esi
- pop ebx
- mov esp,ebp
- pop ebp
- retn
- }
- }
复制代码另外,thiscall约定也采用寄存器传参。thiscall是C++中的非静态类成员函数的默认调用约定,对象每个函数隐含接收this参数。thiscall从右到左顺序压栈,子函数返回前清理堆栈。
仅通过ecx传递额外指针this指针。 名称修饰约定为允许使用操作符和函数重载,C++编译器往往会按照某种规则该写每一个入口点的符号名,从而允许同一个名字(具有不同的参数类型或者不同的作用域)有多个用法且不会破坏现有的基于C的链接器。
在VC++中,函数修饰名由编译类型(C/C++),函数名、类名、调用约定、返回类型、参数等共同决定。简单来说:
C:
⊙ stdcall 函数名前加下划线,后接@ +参数字节数“_functionname@number”。
⊙ __cdecl 函数名前加下划线。“_functionname”
⊙ Fastcall 函数名前加@,后接@ +参数字节数“@functionname@number” 均不改变函数名中的字符大小写。但是pascal约定输出的函数名不能有修饰且只能是大写。 C++:
待完善。 函数返回值return操作符一般情况下,返回值放在eax寄存器中,如果结果大小超出eax的容量,高32位就会放在edx中。 传引用方式传参的返回值函数传参有两种方式,传值和传引用。 传值调用时,会建立一份参数的副本,把它传给子函数,子函数中修改参数值不会影响到参数原本的值。 传引用调用允许调用函数修改原始变量的值。调用函数时,把变量地址传给函数就可以修改内存单元中变量的值。 为局部变量分配内存空间sub esp,44
寄存器入栈、空间清C(初始化变量,将原来内存中的脏数据全部置为CCCCCCCC):
int i的话内存就为CCCCCCCC,int i=1的话内存就是00000001。 总结1.call printf按顺序输出,先入栈的后输出。
2.寻址花了很长时间。(刚刚看汇编5天,数据结构也还没学过,C语言也还停留在大一上水平)所以改变寻址调试好久,这里需要注意。
3.调试过程划重点。 4.一个月后,回来修改补充了本文。我已经忘了为什么寻址会花很长时间,明明这个没有什么难跳转的地址。
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有帐号?立即注册
x
|