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差不多,有時間可以試試。