3、C函數可變參數實現細節的一些思考

c函數可變參數很有意思,它和cpu有關係,所以這些參數都是庫提供的。 4個參數,va_listva_startva_argva_end ; 以前只會用,並不知道爲什麼可以這樣。

unixwindows系統針對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差不多,有時間可以試試。

發佈了19 篇原創文章 · 獲贊 1 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章