为什么函数指针的入参可以不等于函数原形的入参?——谈谈栈平衡
一、问题
先尝试看看,下列代码的编译结果是什么?
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);
}
A. 不会出错,因为s3在实际函数Called中未使用。
B. 出错,因为入参数类型不匹配,函数指针入参为unsigned,实际执行函数Called入参为int,char。
C. 出错,因为入参个数不匹配,调用函数指针的入参为3个,实际执行函数Called入参为2个。
二、分析
我们知道C/C++中默认函数参数入栈顺序为从右至左,函数执行前参数入栈,函数完成后参数出栈;也就是说调用函数指针fp(i1, s2, 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方式下,函数指针和实现函数入参不同,我们定义的接口函数似乎能正确简洁的运行,但将来的事情谁说的准呢?有追求的程序员不会为未来埋坑。
作者|伍小川
- 点赞
- 收藏
- 关注作者
评论(0)