函数调用约定
数学术语
函数调用约定,是指当一个函数被调用时,函数的参数会被传递给被调用的函数和返回值会被返回给调用函数。函数的调用约定就是描述参数是怎么传递和由谁平衡堆栈的,当然还有返回值。
几种类型
__stdcall__cdecl__fastcall,__thiscall,__nakedcall,__pascal,__vectorcall
约束事件
参数传递顺序
1.从右到左依次入栈:__stdcall__cdecl,__thiscall,__fastcall
2.从左到右依次入栈:__pascal
调用堆栈清理
1.调用者清除栈。
2.被调用函数返回后清除栈。
常用描述
__cdecl
1、参数是从右向左传递的,也是放在堆栈中。
2、堆栈平衡是由调用函数来执行的(在call B,之后会有add esp x,x表示参数的字节数)。
3、函数的前面会加一个前缀_(_sumExample)
下面来看看具体的反汇编代码,这是从VC反汇编的代码截取的一部分代码。
从上面的两个push操作我们就可以知道参数是从右向左传递的了。另外这里也回答前面的问题为什么参数会被扩展为4个字节,因为堆栈的操作都是对一个字进行操作的,所以参数都是4个字节的。
这里就是调用函数操作了。在进行call操作之后,会自动将call的下一条语句作为函数的返回地址保存在栈中,也就是下面的(00401098)。在地址00401014处我们可以看到这样的一小段代码
这里就可以知道程序在编译之后会在函数前面加上前缀_
这里就是平衡堆栈操作了。可以看出是在调用者进行的。
保存值是由eax寄存器返回的,从这里就可以看出了。
这上面的一段代码就是函数的开端了。也就是function prolog。通过将一些寄存器来对它们进行保存,也就像中断发生后,需要保护现场一样。
这里就是函数收尾,也就是function epilog
经过上面的分析,相信你对__cdecl调用约定有了比较清晰的认识了。但是这里我们应该想想为什么不在被调函数内进行堆栈平衡呢?在这里我们应该要考虑类似于像scanfprintf这样的函数,这里我们应该明白这两个函数的参数都是可变的,如果参数不固定的话,在被调用函数内就无法知道参数究竟使用了多少个字节,所以为了实现可变参数,我们必须要在被调函数执行之后我们才知道参数究竟用了多少字节,所以我们在调用者来进行堆栈平衡操作。在后面我们将要对printf函数内部是怎么实现做一些探究。
__stdcall
Win32 API函数绝大部分都是采用__stdcall调用约定的。WINAPI其实也只是__stdcall的一个别名而已。
还是与上面一样,我们在函数的面前用__stdcall作为修饰符。此时函数将会采用__stdcall调用约定
__stdcall调用约定的主要特征是:
1、参数是从右往左传递的,也是放在堆栈中。
2、函数的堆栈平衡操作是由被调用函数执行的。
3、在函数名的前面用下划线修饰,在函数名的后面由@来修饰并加上需要的字节数的空间(_sumExample@8)。
这两个push可以说明函数的参数是由右向左传递的。
再来看看函数的代码。
函数的开端与__cdecl调用约定是相同的
函数的收尾也是和__cdecl调用约定是相同的
另外在最后面将对堆栈进行平衡操作。
ret 8 //两个4字节的参数
上面的是文章本来的说明,但在VC中却好像有点区别。
sumExample函数
因为栈的清理(堆栈平衡操作)是由被调用函数执行的。所以使用__stdcall调用约定生成的可执行文件要比__cdecl的要小,因为在每次的函数调用都要产生堆栈清理的代码。函数具有可变参数像我wsprintf这个函数,与前面的prinf一样,都必须使用__cdecl调用约定,因为只有调用者才知道参数的数量在每一次的函数调用,因此也只有调用者才能够执行堆栈清理操作。
__fastcall
__fastcall见名知其意,其特点就是快。__fastcall函数调用约定表明了参数应该放在寄存器中,而不是在栈中,VC编译器采用调用约定传递参数时,最左边的两个不大于4个字节(DWORD)的参数分别放在ecx和edx寄存器。当寄存器用完的时候,其余参数仍然从右到左的顺序压入堆栈。像浮点值、远指针和__int64类型总是通过堆栈来传递的。
下面来看看使用测试的源代码
参考资料
最新修订时间:2022-08-26 10:48
目录
概述
几种类型
约束事件
常用描述
参考资料