c函数可变参数很有意思,它和cpu有关系,所以这些参数都是库提供的。 4个参数,va_list、va_start、va_arg、va_end ; 以前只会用,并不知道为什么可以这样。
unix和windows系统针对X86平台是这样的:
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(UINTN) - 1) & ~(sizeof(UINTN) - 1) )
typedef CHAR8 * va_list;
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )
看起来晕吧?哈哈。没有啦,。第一个宏定义是要求4字节对齐,不信可以拿几个数据算算咯。。假设sizeof(UINTN)为4, (sizeof(n)为5 ,结果为8。
下面就很清楚了,变量定位哈。
单片机的就不同了,因为单片机没有对齐的问题。
typedef char *va_list;
#define va_start(ap,v) ap = (va_list)&v + sizeof(v)
#define va_arg(ap,t) *(((t *)ap)++)
#define va_end(ap) ( ap = (va_list)0 )
这个比较明显哈。
以X86为例:
从汇编的角度来看,函数的参数会入栈,__cdecl是c语言函数默认的调用方式。它会先让最后一个参数入栈,然后堆栈指针sp会根据参数的大小下移(并且按规定的字节数对齐),第一个参数后入栈后,才会让函数内变量入栈。
所以,我们只要确定第一个参数的地址就可以了,后面的参数,可以根据参数的大小确定其地址,然后转换成参数的指针咯。
我写了个测试的函数,感觉挺好玩的。
typedef struct test
{
char a;
int b ;
char c;
}test;
void test_f(char *fmt, ...)
{
printf("the arg &fmt = %u, string is %s, &fmt + 4 = %u\n, ((test *)(&fmt + 4))->dd = %d ,the char is %c ", (unsigned int)(&fmt),*(char **)(&fmt), (unsigned int)(&fmt) + 4, ((test *)((unsigned int) (&fmt) + 4)) ->b, *(char *)((unsigned int)(&fmt) + 16) );
}
void *memset(void *d, int c, size_t size )
{
while( size-- )
{
*(char *)d = c ;
d = (char *)d + 1;
}
return (d) ;
}
int main(int argc, char *argv[])
{
test mytest;
memset(&mytest, 0, sizeof(mytest) );
mytest.b = 1;
test_f("es", mytest, 'c');
return 0;
}
运行结果:
the arg &fmt = 3213711168, string is es, &fmt + 4 = 3213711172
, ((test *)(&fmt + 4))->b = 1 ,the char is c
这个结果很有意思,哈哈。结果还说明了,c函数确确实实是按值传递的。前面的函数参数入栈分析没有问题。
下面稍微改一下:
void test_f(char *fmt, ...)
{
printf("the arg &fmt = %u, string is %s, &fmt + 4 = %u\n, the char is %c , ((test *)(&fmt + 4))->b = %d ", (unsigned int)(&fmt),*(char **)(&fmt), (unsigned int)(&fmt) + 4, *(char *)((unsigned int)(&fmt) + 4), ((test *)((unsigned int) (&fmt) + 8)) ->b );
}
int main(int argc, char *argv[])
{
test mytest;
memset(&mytest, 0, sizeof(mytest) );
mytest.b = 1;
test_f("es", 'c', mytest);
return 0;
}
运行结果:
the arg &fmt = 3214004096, string is es, &fmt + 4 = 3214004100
, the char is c , ((test *)(&fmt + 4))->b = 1
这个结果说明 x86平台 c语言可变参数函数的参数入栈确实是 4字节对齐的。
最后一个疑问:函数指针是否可以作为c语言可变参数函数的参数呢?
c standard貌似讲了这个方面, 结构和函数指针是不允许的,不过c99做了一些改变。 用函数名试试就知道了。
typedef void (*pf)(void);
void f_test(void)
{
printf("f_test called ! \n" );
}
void test_f(char *fmt, ...)
{
printf("the arg &fmt = %u, string is %s, &fmt + 4 = %u\n, the char is %c , ((test *)(&fmt + 4))->b = %d ", (unsigned int)(&fmt),*(char **)(&fmt), (unsigned int)(&fmt) + 4, *(char *)((unsigned int)(&fmt) + 4) , ((test *)((unsigned int) (&fmt) + 8)) ->b );
( *(pf)((unsigned int)(&fmt) + 20))( );
}
主函数只改一个地方:
test_f("es", 'c', mytest, f_test );
运行结果:
the arg &fmt = 3214106528, string is es, &fmt + 4 = 3214106532
Command terminated
唉,躺着中枪啊! 从汇编上来说,这么做没问题哦。 更奇怪的是,调试时用类似的方法结果也是莫名其妙,我靠!
(gdb) p (f_test)((unsigned int)(&fmt) + 20 )
, the char is c , ((test *)(&fmt + 4))->b = 1 f_test called !
$1 = void
(gdb) p (f_test)((unsigned int)(&fmt) + 20 ) ()
f_test called !
Invalid data type for function to be called.
(gdb) p (f_test)((unsigned int)(&fmt) + 20 ) (void)
A syntax error in expression, near `)'.
总结: 感觉挺好玩的,哈哈。很可惜现在没有办法验证arm平台的情况,板子不在身边。 应该和X86差不多,有时间可以试试。