您的位置:首页 > 其它

3、C函数可变参数实现细节的一些思考

2012-10-10 16:04 393 查看
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差不多,有时间可以试试。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: