调用惯例 |
清理方 |
参数传递顺序 |
函数编译后修饰规则 |
cdecl |
函数调用方 |
从右到左的顺序压栈 |
下划线+函数名 |
stdcall |
函数本身即被调用方 |
从右到左的顺序压栈 |
下划线+函数名+@+参数占用字节数 |
fastcall |
函数本身 |
函数的头两个 DWORD(4Byte)类型或者更少字节的参数被放入寄存器,其他剩下的参数依旧按照从右到左的顺序压栈 |
@+函数名+@+参数的字节数 |
pacall |
函数本身 |
从左到右的顺序压栈 |
较为复杂,可单独了解 |
通过上述宏定义和宏实现的过程,显示了变长参数“顺藤摸瓜”的遍历过程。而谈到这种变长参数“顺藤摸瓜”的效果,则不得不说函数调用规范。正是调用惯例“cdecl”的存在才使得变长参数这种“调用时方能确定详细使用情况”的机制才能得以存在。默认情况下使用的是cdecl.
在进行函数调用之前需要将本次调用的参数放置在栈中,而后才能正式启动本次调用,子函数使用完成后,本轮调用使用的参数显然是需要被弹出栈的,但本着“谁使用,谁处理”的原则,一般的,参数出栈这种清理工作是需要被调用函数主动清理的。被调用方主动清理参数出栈在大多数情况下是合理的,但是如果面对一些事先无法确认调用情况,如参数个数,显然被调用方是无法事先知道弹栈的范围的,这种情况下便是需要cdecl调用惯例,交由调用者来处理出栈。
可借由使用cdecl和stdcall两修饰符的函数的反汇编代码的末端来查看这两种调用惯例的不同之处。
/1.cdecl调用规范:被调用方不负责弹出参数,由主动调用者负责为后栈中参数的弹出清理
mov esp, ebp //恢复到esp此前的栈顶位置,回到调用方的栈顶
pop ebp //恢复到ebp此前的栈底位置,回到调用方的栈底
ret //将ebp+4处保留的return address装载近eip寄存器
/2.stdcall调用规范:和cdecl调用惯例最大的不同是末端ret变成了ret X
mov esp, ebp //恢复到esp此前的栈顶位置,回到调用方的栈顶
pop ebp //恢复到ebp此前的栈底位置,回到调用方的栈底
ret X //将ebp+4处保留的return address装载近eip寄存器,
//并指示CPU自动弹出栈中X字节的空间,X等于本轮调 用时传递的参数占用字节总量
可以看到X的个数在调用前就应该固定下来的,否则也不会被汇编进最后的汇编代码中,调用者也要严格按照API声明的参数个数和类型来进行传递,不能多,不能少,顺序不能乱。看起来呆板不够灵活,但是可靠性强,一旦发生调用不规范可及时示警。这也是Windows API都采用stdcall规范(宏WINAPI的定义)的原因,因为不同的编译器产生栈的方式不尽相同,调用者不一定能正常完成清除工作,如果使用stdcall则函数调用者就可以主动解决参数清理工作,所以涉及跨平台API时,如果能确定函数的行参情况,则应该尽量不使用变长行参,函数使用stdcall调用惯例修饰。但如果遇到可变行参的情况,如printf,则只能使用cdecl。
C 语言允许定义参数数量可变的函数,这称为可变参数函数(variadic function)。这种函数需要固定数量的强制参数(mandatory argument),后面是数量可变的可选参数(optional argument)。
这种函数必须至少有一个强制参数。可选参数的类型可以变化。可选参数的数量由强制参数的值决定,或由用来定义可选参数列表的特殊值决定。
C 语言中最常用的可变参数函数例子是 printf()和 scanf()。这两个函数都有一个强制参数,即格式化字符串。格式化字符串中的转换修饰符决定了可选参数的数量和类型。
对于每一个强制参数来说,函数头部都会显示一个适当的参数,像普通函数声明一样。参数列表的格式是强制性参数在前,后面跟着一个逗号和省略号(...),这个省略号代表可选参数。
可变参数函数要获取可选参数时,必须通过一个类型为 va_list 的对象,它包含了参数信息。这种类型的对象也称为参数指针(argument pointer),它包含了栈中至少一个参数的位置。可以使用这个参数指针从一个可选参数移动到下一个可选参数,由此,函数就可以获取所有的可选参数。va_list 类型被定义在头文件 stdarg.h 中。
当编写支持参数数量可变的函数时,必须用 va_list 类型定义参数指针,以获取可选参数。在下面的讨论中,va_list 对象被命名为 argptr。可以用 4 个宏来处理该参数指针,这些宏都定义在头文件 stdarg.h 中:void va_start(va_list argptr, lastparam);
宏 va_start 使用第一个可选参数的位置来初始化 argptr 参数指针。该宏的第二个参数必须是该函数最后一个有名称参数的名称。必须先调用该宏,才可以开始使用可选参数。
type va_arg(va_list argptr, type);
展开宏 va_arg 会得到当前 argptr 所引用的可选参数,也会将 argptr 移动到列表中的下一个参数。宏 va_arg 的第二个参数是刚刚被读入的参数的类型。
void va_end(va_list argptr);
当不再需要使用参数指针时,必须调用宏 va_end。如果想使用宏 va_start 或者宏 va_copy 来重新初始化一个之前用过的参数指针,也必须先调用宏 va_end。
void va_copy(va_list dest, va_list src);
宏 va_copy 使用当前的 src 值来初始化参数指针 dest。然后就可以使用 dest 中的备份获取可选参数列表,从 src 所引用的位置开始。
/*
如此的声明表明,printf函数处理第一参数类型为const char*之外,
其后可以追加任意数量、任意类型的参数。
*/
int printf(const char* format, ⋯);va_list ap;
//va_list类型实际是一指针,因为类型不明,因此以void* 和char*
//初始化为最佳。该变量以后将会依次指向各个可变参数。/*
在函数的实现部分,可以使用stdarg.h里的多个宏来访问各个额外的参数:假设
lastarg是变长参数函数的最后一个具名参数(例如printf里的format).
ap必须用va_start初始化一次,其中lastarg必须是函数的最后一个具名的参数
*/
va_start(ap, lasrtarg); //将va_list类型指针va指向第一个不定参数/*
此后,可以通过va_arg宏来获得下一个不定参数(假设已知其类型为type):
*/
type next = va_arg(ap, type); //va_arg获取当前不定参数的类型和值,并将指针移动到下一个参数/*
marco implementation part...
*/#define va_list char*
#define va_start(ap,arg) ( ap = (va_list)&arg + sizeof(arg) )
#define va_arg(ap, type) ( *(type*)( (ap+=sizeof(type))-sizeof(type)) ) //ap被移向下一个可变参数的起点位置,并且va_arg将返回当前可变参数的值,当前被遍历到的参数的类型type都是format中规定的%d这类字符串格式传递进来的。
#define va_end(ap) ( ap=(va_list)0 ) //在函数结束前,还必须调用宏
//va_end来清理现场,将指针ap置为空指针,防止野指针误用
printf.c
int fputc(int c, FILE* stream)
{
if( fwrite(&c, 1, 1, stream) != 1)
return EOF;
else
return c;
}
int fputs(const char* str, FILE* stream)
{
int len = strlen(str);
if (fwrite(str,1,len, stream) != len)
return EOF;
else
return len;
}
#ifndef WIN32
#define va_list char*
#define va_start(ap,arg) ( ap = (va_list)&arg + sizeof(arg))
#define va_arg(ap, t) ( *(t*) ( (ap+=sizeof(t)) - sizeof(t) ) )
#define va_end(ap) ( ap = (va_list) 0)
#else
#include "windows.h"
#endif
//Mini CRT 中并不支持特殊的格式操作,仅支持%d和%s两种简单的转换
int vfprintf(FILE* stream, const char* format, va_list arglist )
{
int translating = 0;
int ret = 0; //记录最终输出的字符个数
const char* p = 0;
//char temp[10];
//strcpy(temp, "evilsama");
//fputs(temp, stream);
//fputs(*(const char**)arglist, stream);
//fputs(itoa(123, temp, 10), stream);
//fputs("entry the vfprintf\n",stream);
for (p = format; *p != '\0'; ++p)
{
switch (*p)
{
case '%':
//fputs("\n we truly enter the %-part \n", stream);
if (! translating)
{
translating = 1; //translating置为1,代表后面的字符需要解析
char temp[10];
//itoa( translating, temp, 10);
//fputs("translating = ", stream);
//fputs(temp, stream);
}
else
{
if (fputc('%', stream) < 0)
return EOF;
++ret;
translating = 0;
}
break;
case 'd':
if (translating) //%d
{
char buf[16];
translating = 0;
itoa( va_arg(arglist, int), buf, 10);
if (fputs(buf, stream) < 0)
return EOF;
ret += strlen(buf);
}
else if (fputc('d', stream) < 0)
return EOF;
else
++ret;
break;
case 's':
if (translating) //%s
{
const char* str = va_arg(arglist, const char*);
//fputs("\n we truly enter the s-part \n", stream);
translating = 0;
if (fputs(str, stream) < 0)
return EOF;
ret += strlen(str);
}
else if (fputc ('s' , stream) < 0)
return EOF;
else
++ret;
break;
default:
if (translating)
translating = 0;
if ( fputc(*p, stream) < 0 )
return EOF;
else
++ret;
break;
}
}
return ret;
}
int printf(const char* format, ...)
{
va_list(arglist);
va_start(arglist, format);
return vfprintf(stdout, format, arglist);
}
int fprintf(FILE* stream, const char* format, ...)
{
va_list(arglist);
va_start(arglist, format);
return vfprintf(stream, format, arglist);
}
示例:
示例:
// 函数add() 计算可选参数之和
// 参数:第一个强制参数指定了可选参数的数量,可选参数为double类型
// 返回值:和值,double类型
double add( int n, ... )
{
int i = 0;
double sum = 0.0;
va_list argptr;
va_start( argptr, n ); // 初始化argptr
for ( i = 0; i < n; ++i ) // 对每个可选参数,读取类型为double的参数,
sum += va_arg( argptr, double ); // 然后累加到sum中
va_end( argptr );
return sum;
}