深入理解C函数传递参数的过程

来源:互联网 发布:c语言sscanf函数 编辑:程序博客网 时间:2024/04/28 05:24

1,大致分为传值和传地址,不过总体说来都是传值,地址也是一种数值。

传递参数都是拷贝数据进行传递的。这是编译器以及函数调用约定决定的。

首先了解一下主要的函数调用约定

__stdcall,__cdecl,__fastcall,__thiscall,__nakedcall,__pascal

  1.从右到左依次入栈:__stdcall,__cdecl,__thiscall

  2.从左到右依次入栈:__pascal,__fastcall

(1)__cdecl调用约定

  1、参数是从右向左传递的,也是放在堆栈中。
  2、堆栈平衡是由调用函数来执行的(在call B,之后会有add esp x,x表示参数的字节数)。
  3、函数的前面会加一个前缀_(_sumExample)
  下面来看看具体的反汇编代码,这是从VC反汇编的代码截取的一部分代码。

 10: int c = 0;

  00401088 C7 45 FC 00 00 00 00 mov dword ptr [ebp-4],0
  11: c = sumExample(2, 3);
  0040108F 6A 03 push 3
  00401091 6A 02 push 2
  从上面的两个push操作我们就可以知道参数是从右向左传递的了。另外这里也回答前面的问题为什么参数会被扩展为4个字节,因为堆栈的操作都是对一个字进行操作的,所以参数都是4个字节的。
  00401093 E8 7C FF FF FF call @ILT+15(_Max) (00401014)
  这里就是调用函数操作了。在进行call操作之后,会自动将call的下一条语句作为函数的返回地址保存在栈中,也就是下面的(00401098)。在地址00401014处我们可以看到这样的一小段代码
  @ILT+0(_Max):
  00401005 E9 26 00 00 00 jmp _sumExample (00401030)
  这里就可以知道程序在编译之后会在函数前面加上前缀_
  00401098 83 C4 08 add esp,8
  这里就是平衡堆栈操作了。可以看出是在调用者进行的。
  0040109B 89 45 FC mov dword ptr [ebp-4],eax
  保存值是由eax寄存器返回的,从这里就可以看出了。
  12:
  13: return 0;
  0040109E 33 C0 xor eax,eax
  14: }
  3: int __cdecl sumExample(int a, int b)
  4: {
  00401030 55 push ebp
  00401031 8B EC mov ebp,esp
  00401033 83 EC 40 sub esp,40h 为局部变量预留空间
  00401036 53 push ebx
  00401037 56 push esi
  00401038 57 push edi
  00401039 8D 7D C0 lea edi,[ebp-40h]
  0040103C B9 10 00 00 00 mov ecx,10h
  00401041 B8 CC CC CC CC mov eax,0CCCCCCCCh
  00401046 F3 AB rep stos dword ptr [edi]
  这上面的一段代码就是函数的开端了。也就是function prolog。通过将一些寄存器来对它们进行保存,也就像中断发生后,需要保护现场一样。
  5: return (a + b);
  00401048 8B 45 08 mov eax,dword ptr [ebp+8]
  0040104B 03 45 0C add eax,dword ptr [ebp+0Ch]
  6: }
  0040104E 5F pop edi
  0040104F 5E pop esi
  00401050 5B pop ebx
  00401051 8B E5 mov esp,ebp
  00401053 5D pop ebp
  00401054 C3 ret
  这里就是函数收尾,也就是function epilog
  经过上面的分析,相信你对__cdecl调用约定有了比较清晰的认识了。但是这里我们应该想想为什么不在被调函数内进行堆栈平衡呢?在这里我们应该要考虑类似于像scanf和printf这样的函数,这里我们应该明白这两个函数的参数都是可变的,如果参数不固定的话,在被调用函数内就无法知道参数究竟使用了多少个字节,所以为了实现可变参数,我们必须要在被调函数执行之后我们才知道参数究竟用了多少字节,所以我们在调用者来进行堆栈平衡操作。在后面我们将要对printf函数内部是怎么实现做一些探究。

(2)__stdcall调用约定

  Win32 API函数绝大部分都是采用__stdcall调用约定的。WINAPI其实也只是__stacall的一个别名而已。
  #define WINAPI __stdcall
  还是与上面一样,我们在函数的面前用__stdcall作为修饰符。此时函数将会采用__stdcall调用约定
  int __stdcallsumExample (int a, int b);
  __stdcall调用约定的主要特征是:
  1、参数是从右往左传递的,也是放在堆栈中。
  2、函数的堆栈平衡操作是由被调用函数执行的。
  3、在函数名的前面用下划线修饰,在函数名的后面由@来修饰并加上栈需要的字节数的空间(_sumExample@8)。
  main函数
  push 3
  push 2
  这两个push可以说明函数的参数是由右向左传递的。
  call _sumExample@8 //调用函数
  mov dword ptr [c], eax //eax寄存器保存函数的返回值,此时将返回值赋值给局部变量c。
  再来看看函数的代码。
  函数的开端与__cdecl调用约定是相同的
  mov eax, dword ptr [a]
  add eax, dword ptr [b]
  函数的收尾也是和__cdecl调用约定是相同的
  另外在最后面将对堆栈进行平衡操作。
  ret 8 //两个4字节的参数
  上面的是文章本来的说明,但在VC中却好像有点区别。
  main函数
  0040108F 6A 03 push 3
  00401091 6A 02 push 2
  00401093 E8 81 FF FF FF call @ILT+20(_sumExample) (00401019)
  00401098 89 45 FC mov dword ptr [ebp-4],eax
  sumExample函数
  5: return (a + b);
  00401048 8B 45 08 mov eax,dword ptr [ebp+8]
  0040104B 03 45 0C add eax,dword ptr [ebp+0Ch]
  00401054 C2 08 00 ret 8 //堆栈平衡操作
  因为栈的清理(堆栈平衡操作)是由被调用函数执行的。所以使用__stdcall调用约定生成的可执行文件要比__cdecl的要小,因为在每次的函数调用都要产生堆栈清理的代码。函数具有可变参数像我wsprintf这个函数,与前面的prinf一样,都必须使用__cdecl调用约定,因为只有调用者才知道参数的数量在每一次的函数调用,因此也只有调用者才能够执行堆栈清理操作。

(3)__fastcall调用约定

  __fastcall见名知其意,其特点就是快。__fastcall函数调用约定表明了参数应该放在寄存器中,而不是在栈中,VC编译器采用调用约定传递参数时,最左边的两个不大于4个字节(DWORD)的参数分别放在ecx和edx寄存器。当寄存器用完的时候,其余参数仍然从右到左的顺序压入堆栈。像浮点值、远指针和__int64类型总是通过堆栈来传递的。
  下面来看看使用测试的源代码
  #include <stdio.h>
  int __fastcall sumExample(int a, int b, int c)
  {
  return (a + b + c);
  }
  double __fastcall sumExampled(double a, double b)
  {
  return (a + b);
  }
  int main()
  {
  int c = 0;
  double d = 0.0;
  c = sumExample(2, 3, 5);
  d = sumExampled(2.3, 2.5);
  return 0;
  }
  15: int c = 0;
  004010C8 C7 45 FC 00 00 00 00 mov dword ptr [ebp-4],0
  16: double d = 0.0;
  004010CF C7 45 F4 00 00 00 00 mov dword ptr [ebp-0Ch],0
  004010D6 C7 45 F8 00 00 00 00 mov dword ptr [ebp-8],0
  17: c = sumExample(2, 3, 5);
  004010DD 6A 05 push 5
  004010DF BA 03 00 00 00 mov edx,3
  004010E4 B9 02 00 00 00 mov ecx,2
  004010E9 E8 26 FF FF FF call @ILT+15(@sumExample@8) (00401014)
  004010EE 89 45 FC mov dword ptr [ebp-4],eax
  18:
  19: d = sumExampled(2.3, 2.5);
  004010F1 68 00 00 04 40 push 40040000h
  004010F6 6A 00 push 0
  004010F8 68 66 66 02 40 push 40026666h
  004010FD 68 66 66 66 66 push 66666666h
  00401102 E8 FE FE FF FF call @ILT+0(@sumExampled@16) (00401005)
  00401107 DD 5D F4 fstp qword ptr [ebp-0Ch]


了解了调用约定之后来通过一个普通程序的反汇编看下,参数是如何传递的。VC++ 6.0查看反汇编窗口是在调试的时候 alt+8组合键。

9:    void main()10:   {00401060   push        ebp00401061   mov         ebp,esp00401063   sub         esp,48h00401066   push        ebx00401067   push        esi00401068   push        edi00401069   lea         edi,[ebp-48h]0040106C   mov         ecx,12h00401071   mov         eax,0CCCCCCCCh00401076   rep stos    dword ptr [edi]11:       int a=1,b=2;00401078   mov         dword ptr [ebp-4],10040107F   mov         dword ptr [ebp-8],2
12:       foo(a,b);
00401086   mov         eax,dword ptr [ebp-8]//这儿就是传递参数的过程,把b的值复制到eax中,然后在foo函数中所做的处理就完全和b是没有关系的了。因此说是通过复制传值的
00401089   push        eax
0040108A   mov         ecx,dword ptr [ebp-4]
0040108D   push        ecx
0040108E   call        @ILT+5(foo) (0040100a)
00401093   add         esp,8
13:       printf("%d\n%d",a,b);
00401096   mov         edx,dword ptr [ebp-8]
00401099   push        edx
0040109A   mov         eax,dword ptr [ebp-4]
0040109D   push        eax
0040109E   push        offset string "%d\n%d" (0042201c)
004010A3   call        printf (00401170)
004010A8   add         esp,0Ch

#include <stdio.h>#include <windows.h>void foo(int a,int b){a=a+b;a=a*b;}void main(){int a=1,b=2;foo(a,b);printf("%d\n%d",a,b);}
函数传递指针地址
#include <stdio.h>#include <windows.h>void foo(int *a,int *b){*a=*a+*b;*a=(*a)*(*b);}void main(){int a=1,b=2;foo(&a,&b);printf("%d\n%d",a,b);}11:       int a=1,b=2;00401078   mov         dword ptr [ebp-4],10040107F   mov         dword ptr [ebp-8],212:       foo(&a,&b);00401086   lea         eax,[ebp-8]//复制a,b的地址的值。只不过在函数中由于对地址取值进行修改。因此a,b的值会改变。但是a,b的地址任然没有改变00401089   push        eax0040108A   lea         ecx,[ebp-4]0040108D   push        ecx0040108E   call        @ILT+10(foo) (0040100f)00401093   add         esp,8
3、引用传值

12:       foo(a,b);00401086   lea         eax,[ebp-8]00401089   push        eax0040108A   lea         ecx,[ebp-4]0040108D   push        ecx0040108E   call        @ILT+15(foo) (00401014)00401093   add         esp,8


深入理解函数传递参数的过程。


原创粉丝点击