C# · 12月 19, 2021

为什么函数指针的入参可以不等于函数原形的入参?——谈谈栈平衡

【摘要】 最近在某产品推进再研版本和维护版本分支合一的事项,为有效的隔离新老特性,使用了Bridge模式,其间采用了函数指针,在编译/运行时通过注册不同的具体实现函数,确定特性的具体行为。

一、问题

先尝试看看,下列代码的编译结果是什么?

typedef void (*APP_FUNC)(unsigned , unsigned, unsigned);

void Called(int i1, char s2)

{

      printf(“%d,%c”,i1,s2);

}

void Callee()

{

      int i1 = -1024;

      char s2 = ‘B’;

      char s3 = ‘C’;

      APP_FUNC fp = (APP_FUNC) Called;

      fp(i1,s2,s3);

}

欢迎加入学习群【892643663】,获取全套免费C/C++企业实战级课程资源(素材+源码+视频)和编译大礼包

A. 不会出错,因为s3在实际函数Called中未使用。

B. 出错,因为入参数类型不匹配,函数指针入参为unsigned,实际执行函数Called入参为int,char。

C. 出错,因为入参个数不匹配,调用函数指针的入参为3个,实际执行函数Called入参为2个。

二、分析

我们知道C/C++中默认函数参数入栈顺序为从右至左,函数执行前参数入栈,函数完成后参数出栈;也就是说调用函数指针fp(i1,s3)时,参数入栈顺序为s3-> s2-> s1;那么在实际执行函数Called中,因为没有使用到s3,在默认字节对齐的情况下使用s1,s2变量时,unsigned占用的存储空间>=实际入参的存储空间,所以函数功能应该不会受影响。

问题是——Called执行结束时参数出栈,此种情况究竟应该出多少个呢?入栈出栈是否平衡呢?

    2.1 函数的调用约定

打开VC,我们可以看到有__cdecl、 __fastcall、 __stdcall 三种调用约定。通常

__cdecl   :C/C++默认的函数调用协议;

__stdcall :Windows API默认的函数调用协议;

__fastcall:适用于对性能要求较高的场合,从左开始不大于4字节的参数放入cpu的ECX和EDX寄存器,其余参数从右向左入栈。

使用者可显式的标记函数的调用约定。

    2.2 三种调用方式的汇编展示    

对比__cdecl和__stdcall方式下的汇编,Callee调用Called函数前有明显的参数入栈动作(蓝色部分);但是在Called函数结束时,__cdecl直接ret并在调用函数Callee中add esp,0ch恢复栈顶,__stdcall则在函数内部ret 8(红色部分)恢复栈顶,也就是说__stdcall入栈12个字节,出栈8个字节,栈数据残留,后续运行结果未知。

在__fastcall方式下,Callee调用Called函数,从左向右i1,s2未入栈,通过寄存器ecx,edx传递,其余参数(s3)从右向左入栈后,调用Called函数;然后在Called函数中使用ret n恢复栈顶。本例中被调函数Called使用2个寄存器传递进来的参数,ret 0恢复栈顶,也就是说__fastcall入栈4个字节,出栈0个字节,栈数据残留。

栈内数据清除方式简单描述如下:

__cdecl   :函数调用结束后由函数调用者清除栈内数据。

__stdcall :函数调用结束后由被调用函数清除栈内数据。

__fastcall:函数调用结束后由被调用函数清除栈内数据。

三、标准遵从

在ISO/IEC 9899:1999 (E)中有如下描述:

A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the pointed-to type,the behavior is undefined.

四、总结

在标准中没有明确定义;虽然在当前既成事实的__cdecl方式下,函数指针和实现函数入参不同,我们定义的接口函数似乎能正确简洁的运行,但将来的事情谁说的准呢?有追求的程序员不会为未来埋坑。

作者|伍小川

欢迎加入学习群【892643663】,获取全套免费C/C++企业实战级课程资源(素材+源码+视频)和编译大礼包