stdcall调用约定分析,以c语言为分析对象

来源:互联网 发布:隔离和防晒的顺序 知乎 编辑:程序博客网 时间:2024/05/14 02:57

 

                    stdcall调用约定分析,以c语言为分析对象

                          yckyck2001

stdcall调用约定分析,以c语言为分析对象(一)

-、编译平台

本文中C代码的编译平台为VC6.0,编译版本为Debug版本。

二、研究的C代码

int __stdcall stdcallFun(int Num1 , int Num2)

{

    if (Num1 > Num2)

        return Num1 ;

    else

      return Num2 ;

}

void main(int argc, char *argv[])

{

    int x1 , x2 , nBigger;

    x1 = 5 ;

    x2 = 6 ;

    nBigger = stdcallFun(x1,x2) ;

    return ;

}

 

三、利用VC调试时,查看Debug版本的汇编代码(如看不懂该部分代码可直接跳到第四点的分析部分)

stdcallFun函数

stdcallFun:

004010A0   push        ebp ;保存ebp                     

004010A1   mov        ebp,esp

004010A3   sub         esp,40h ; stdcallFun分配临时变量

004010A6   push        ebx

004010A7   push        esi

004010A8   push        edi ;保护现场

004010A9   lea         edi,[ebp-40h]

004010AC   mov         ecx,10h

004010B1   mov         eax,0CCCCCCCCh

004010B6   rep stos    dword ptr [edi]  ;初始化临时变量

004010B8   mov         eax,dword ptr [ebp+8] ;mov eax, Num1

004010BB   cmp         eax,dword ptr [ebp+0Ch] cpm Num1,Num2

004010BE   jle         stdcallFun+25h (004010c5)

004010C0   mov         eax,dword ptr [ebp+8]

004010C3   jmp         stdcallFun+28h (004010c8)

004010C5   mov         eax,dword ptr [ebp+0Ch]

004010C8   pop         edi

004010C9   pop         esi

004010CA   pop        ebx

004010CB   mov        esp,ebp

004010CD   pop         ebp

004010CE   ret         8

main函数

main:

004010EC   mov         ecx,13h

004010F1   mov         eax,0CCCCCCCCh

004010F6   rep stos       dword ptr [edi]

004010F8   mov         dword ptr [ebp-4],5   ;x1 = 5

004010FF   mov         dword ptr [ebp-8],6    ;x2 = 6

00401106   mov         eax,dword ptr [ebp-8] 

00401109   push         eax                 ;变量x2的值入栈

0040110A   mov         ecx,dword ptr [ebp-4]

0040110D   push         ecx               ;变更x1的值入栈

0040110E   call          @ILT+25(_stdcallFun@8) (0040101e)

00401113   mov         dword ptr [ebp-0Ch],eax

00401116   pop          edi

00401117   pop          esi

00401118   pop          ebx

00401119   add          esp,4Ch

0040111C   cmp         ebp,esp

0040111E   call        __chkesp (00401280) ;调试代码

00401123   mov         esp,ebp

00401125   pop         ebp

00401126   ret

 

四、阶段分析

1.理解stdcall参数传递规则的关键是C语言的变量内存分配规则。对于全局变量和static变量分配的内存空间为全局内存空间;对于函数的局部变量执行函数时分配,退出执行函数收回分配空间。变量的内存空间为栈(stack)。而程序执行中程序员动态审请的内存则位于全局的堆(heap)中。所以我们以下分析函数调用传递规则时,将结合栈的变化图分析。

 

2.阶段分析

a.main函数局部变量分配初始化

void main()

{

    int x1 , x2 , nBigger;

程序执行到此处

对应汇编代码为:

00401060   push       ebp

00401061   mov        ebp,esp

00401063   sub        esp,4Ch  ;main函数分配局部变量在stack里边分配

00401066   push       ebx

00401067   push       esi

00401068   push       edi ;保护现场

00401069   lea         edi,[ebp-4Ch]

0040106C   mov       ecx,13h

00401071   mov       eax,0CCCCCCCCh 

00401076   rep stos    dword ptr [edi] ;用缺省值0CCCCCCCH初始化局部变量,

局部变量的分配是通过,改变栈指针ESP的值实现的,上述代码实现后,栈空间分配如下

 

……

ESP

0CCCCCCCH

 

……

 

0CCCCCCCH

 

nBigger = 0CCCCCCCH

 

x2 = 0CCCCCCCH

 

x1 = 0CCCCCCCH

EBP

……

 

 

13h个临时变量,占4Ch个字节空间

每个函数调用都有当时具体的EBP值,函数通过EBP和偏移值访问该函数的局部变量,如该main函数,通过当前main函数的EBP, [ebp-4]访问main函数的局部变量x1[ebp-8]访问main函数的局部变量x2, [ebp-0Ch]访问main函数的局部变量nBigger。为了便于描述,我们记现在main函数此时的ebp值为 Value_ebp_main

stdcall调用约定分析,以c语言为分析对象(二)

 

b.代码继续执行,x1,x2赋值

    x1 = 5 ;

    x2 = 6 ;  

对应汇编代码

004010F8   mov         dword ptr [ebp-4],5   ;x1 = 5

004010FF   mov         dword ptr [ebp-8],6    ;x2 = 6

对应栈图

 

……

ESP

0CCCCCCCH

 

……

 

0CCCCCCCH

 

nBigger = 0CCCCCCCH

 

x2 = 6

 

x1 = 5

EBP

……

 

 

 

                                             

c.调用stdcallFun函数

nBigger = stdcallFun(x1,x2) ;

对应汇编代码

00401106   mov         eax,dword ptr [ebp-8] 

00401109   push         eax                 ;变量x2的值入栈

0040110A   mov         ecx,dword ptr [ebp-4]

0040110D   push         ecx               ;变更x1的值入栈

0040110E   call          @ILT+25(_stdcallFun@8) (0040101e)

对应栈空间中

 

……

ESP

EIP

 

Num1=5

 

         Num2 =6

 

0CCCCCCCH

 

……..

 

0CCCCCCCH

 

nBigger = 0CCCCCCCH

 

x2 = 6

 

x1 = 5

EBP

……..

我们可以看到main函数中的局部变量的值,x1的值,x2的值通过栈传递给了函数stdcallFun,注意以上我们的用词,局部变量的,也就是说stdcall参数传递规则下的C参数传递是值传送,也就是说我们调用stdcallFun(x1,x2),无论函数怎么操作也不会改变到main函数局部变量x1,x2的,调用stdcallFun(x1,x2)如同调用stdcallFun(5,6)。从以上栈变化图也可以看出stdcall的局部变量Num1,Num2通过main函数的push操作,内存空间得到审请,并赋值。也就是说以上栈图红色部分(Num1,Num2)的空间是属于子函数而不属于main函数的,到时要合理的释放。局部变量值的入栈顺序顺序为自右至左(先x2,x1)。

d.我们继续单步进去执行stdcallFun函数

stdcallFun:

004010A0   push        ebp ;                      

004010A1   mov        ebp,esp

对应栈空间图:

 

……

EBP,ESP

Value_ebp_main

 

ECS:EIP

 

Num1=5

 

         Num2 =6

 

0CCCCCCCH

 

……

 

0CCCCCCCH

 

nBigger = 0CCCCCCCH

 

x2 = 6

 

x1 = 5

 

……

我们以上讨论到函数通过EBP和偏移值访问该函数的局部变量,每个函数执行时都有其特定的EBP值,进入stdcallFun后,我们要更改EBP值使其更方便的操作stdcallFun的局部变量,退出stdcallFun函数调用时,我们要恢复调用函数mainEBP值,所以改变stdcallFunEBP值前必须把main函数的EBPValue_ebp_main入栈,然后把当前的ESP值赋于EBP

e.stdcallFun分配并初始化局部变量空间和保护现场

004010A3   sub         esp,40h ; stdcallFun分配临时变量

004010A6   push        ebx

004010A7   push        esi

004010A8   push        edi ;保护现场

004010A9   lea         edi,[ebp-40h]

004010AC   mov         ecx,10h

004010B1   mov         eax,0CCCCCCCCh

004010B6   rep stos    dword ptr [edi]  ;初始化临时变量 

对应栈空间图 

 

……

ESP

ESI

 

EBI

 

EBX

 

0CCCCCCCH

 

……

 

0CCCCCCCH

EBP

Value_ebp_main

 

ECS:EIP

 

Num1=5

 

         Num2 =6

 

0CCCCCCCH

 

……

 

0CCCCCCCH

 

nBigger = 0CCCCCCCH

 

x2 = 6

 

x1 = 5

 

……

 

 

 10h个临时变量,占40h个字节空间

 

 

 

 

 

 

 

  

stdcall调用约定分析,以c语言为分析对象(三)

f.执行stdcallFun函数里边的代码

    if (Num1 > Num2)

        return Num1 ;

    else

        return Num2 ;

对应汇编代码

004010B8   mov         eax,dword ptr [ebp+8]   ;mov eax, Num1

004010BB   cmp         eax,dword ptr [ebp+0Ch]  ;cmp Num1,Num2

004010BE   jle         stdcallFun+25h (004010c5)  ;if Num1<=Num2

004010C0   mov         eax,dword ptr [ebp+8]   ;返回值Num1保存在eax

004010C3   jmp         stdcallFun+28h (004010c8)

004010C5   mov         eax,dword ptr [ebp+0Ch]  ;返回值Num2保存在eax

我们可以看到stdcallFun,对于传进来的参数Num1,Num2的访问,通过当前EBP的值(属于stdcallFun函数)偏移来访问,而在函数内部定义的局部变量则是通过函数执行时当前的EBP值和偏移来访问的,如main函数的对其x1,x2,nBigger 局部变量的访问。而stdcallFun的返回值存储在eax中。

g.stdcallFun恢复现场,释放局部变量空间

004010C8   pop         edi

004010C9   pop         esi

004010CA   pop        ebx

004010CB   mov        esp,ebp

004010CD   pop         ebp

以上代码中mov esp,ebp ,执行后,esp值加40H,也就是说在stdcallFun内部审请的10c个临时局部变量40H个字节的空间释放。pop ebp,恢复调用函数main执行时的ebp值。此时栈空间图如下:

 

……

 

Value_ebp_main

ESP

ECS:EIP

 

Num1=5

 

         Num2 =6

 

0CCCCCCCH

 

……

 

0CCCCCCCH

 

nBigger = 0CCCCCCCH

 

x2 = 6

 

x1 = 5

EBP

……

 

以上为无效值
  

紧接着stdcallFun返回调用者main,

004010CE   ret         8

我们注意到 ret 后面还带了一个8,该语句不单单把cs,ip指针出栈,而且还把当初main函数(调用函数)传递参数值进来审请的空间释放了。此时栈空间图如下  

 

……

 

Value_ebp_main

 

ECS:EIP

 

Num1=5

 

         Num2 =6

ESP

0CCCCCCCH

 

……..

 

0CCCCCCCH

 

nBigger = 0CCCCCCCH

 

x2 = 6

 

x1 = 5

EBP

……..

 

 

 

 以上为无效值


 

h.返回main函数继续执行

00401113   mov         dword ptr [ebp-0Ch],eax ;返回值赋于nBigger

00401116   pop          edi

00401117   pop          esi

00401118   pop          ebx

00401119   add          esp,4Ch

0040111C   cmp         ebp,esp

0040111E   call        __chkesp (00401280) ;调试代码

00401123   mov         esp,ebp

00401125   pop         ebp

00401126   ret

3.阶段分析总结

由以上分析可知,stdcall参数传递约定,传进调用函数的参数空间由主调函数push操作的方式审请,并同时传进了参数值,传参数时遵循从右到左的的传递参数的顺序。被调用函数负责释放调用函数审请的空间,在返回调用处时释放。由于在返回调用时释放的汇编代码对于具体函数来说是特定的,(如本例中,由于stdcallFun有两个int 型的参数,故其返回代码为ret 8),所以stdcall参数传递约定不能用来实现参数个数可变的函数,如printf等函数。

stdcall参数传递约定,规定被调用函数的返回值存取在eax中。

五、其他

我们可以看到main函数中有两个参数传进,但main函数返回调用处的汇编代码却是

ret ,由以上的分析可知main函数不是stdcall参数传递规则的函数。如果我们把main函数写成:

void __stdcall main(int argc, char *argv[])

{

    int x1 , x2 , nBigger;

    x1 = 5 ;

    x2 = 6 ;

    nBigger = stdcallFun(x1,x2) ;

    return ;

}

发现编译可通过,链接时提示

LIBCD.lib(crt0.obj) : error LNK2001: unresolved external symbol _main

事实上main函数的参数传递规则是__cdecl,VC6.0创建工程的缺省参数传递规则为__cdecl,

LIBCD.lib是已经编译好的库,它是按调用__cdecl参数方式的main函数来编译。而__cdecl__stdcall函数名编译解析的名字是不同的

__stdcall:

An underscore (_) is prefixed to the name. The name is followed by the at sign (@) followed by the number of bytes (in decimal) in the argument list. Therefore, the function declared as int func( int a, double b ) is decorated as follows: _func@12

__cdecl:

Underscore character (_) is prefixed to names

所以以上才会出现提示找不到_main函数的错误,void __stdcall main(int argc, char *argv[])

解析为_main@8,void __cdecl main(int argc, char *argv[])解析为_main,LIBCD.lib调用的是_main