在C/C++中,我們經常會用到可變參數的函數(比如PRintf/snprintf等),本篇筆記旨在講解編譯器借助va_start/va_arg/va_end這簇宏來實現可變參數函數的原理,并在文末給出簡單的實例。
備注:本文的分析適用于linux/Windows,其它操作系統平臺的可變參數函數的實現原理大體相似。1. 基礎知識 如果想要真正理解可變參數函數背后的運行機制,建議先理解兩部分基礎內容: 1)函數調用棧 2)函數調用約定 關于這兩個基礎知識點,我之前的筆記有詳細介紹,感興趣的童鞋可以移步這里:棧與函數調用慣例—上篇 和棧與函數調用慣例—下篇
2. 三個宏:va_start/va_arg/va_end 由man va_start可知,這簇宏定義在stdarg.h中,在我的測試機器上,該頭文件路徑為:/usr/lib/gcc/x86_64-redhat-Linux/3.4.5/include/stdarg.h,在gcc源碼中,其路徑為:gcc/include/stdarg.h。 在stdarg.h中,宏定義的相關代碼如下:
[cpp] view plain copy#define va_start(v,l) __builtin_va_start(v,l) #define va_end(v) __builtin_va_end(v) #define va_arg(v,l) __builtin_va_arg(v,l) #if !defined(__STRICT_ANSI__) || __STDC_VERSION__ + 0 >= 199900L #define va_copy(d,s) __builtin_va_copy(d,s) #endif #define __va_copy(d,s) __builtin_va_copy(d,s) 其中,前3行就是我們所關心的va_start & var_arg & var_end的定義(至于va_copy,man中有所提及,但通常不會用到,想了解的同學可man查看之)。可見,gcc將它們定義為一組builtin函數。 關于這組builtin函數的實現代碼,我曾試圖在gcc源碼中沿著調用路徑往下探索,無奈gcc為實現這組builtin函數引入了很多自定義的數據結構和宏,對非編譯器研究者的我來說,實在有點晦澀,最終探索過程無疾而終。在這里,我列出目前跟蹤到的調用路徑,以便有興趣的童鞋能繼續探索下去或指出我的不足,先在此謝過。 __builtin_va_start()函數的調用路徑:[cpp] view plain copy// file: gcc/builtins.c /* The "standard" implementation of va_start: just assign `nextarg' to the variable. */ void std_expand_builtin_va_start (tree valist, rtx nextarg) { rtx va_r = expand_expr (valist, NULL_RTX, VOIDmode, EXPAND_WRITE); convert_move (va_r, nextarg, 0); // definition is in gcc/expr.c } // 上述代碼中調用了expand_expr()來展開表達式,我猜測該函數調用完后,va_list指向了可變參數list前的最后一個已知類型參數 // file: gcc/expr.h /* Generate code for computing expression EXP. An rtx for the computed value is returned. The value is never null. In the case of a void EXP, const0_rtx is returned. */ static inline rtx expand_expr (tree exp, rtx target, enum machine_mode mode,enum expand_modifier modifier) { return expand_expr_real (exp, target, mode, modifier, NULL); } 3. Windows系統VS內置編譯器對va_start/va_arg/va_end的實現 如前所述,我沒能在gcc源碼中找出va_startva_arg/va_end這3個宏的實現代碼(⊙﹏⊙b汗),所幸的是,Windows平臺VS2008集成的編譯器中,對這三個函數有很明確的實現代碼,摘出如下。[cpp] view plain copy/* file path: Microsoft Visual Studio 9.0/VC/include/stdarg.h */ #include <vadefs.h> #define va_start _crt_va_start #define va_arg _crt_va_arg #define va_end _crt_va_end 可見,Windows系統下,仍然將va_start/va_arg/va_end定義為一組宏。他們對應的實現在vadefs.h中:[cpp] view plain copy/* file path: Microsoft Visual Studio 9.0/VC/include/vadefs.h */ #ifdef __cplusplus #define _ADDRESSOF(v) ( &reinterpret_cast<const char &>(v) ) #else #define _ADDRESSOF(v) ( &(v) ) #endif #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) #define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) ) #define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) #define _crt_va_end(ap) ( ap = (va_list)0 ) 備注:在VS2008提供的vadefs.h文件中,定義了若干組宏以支持不同的操作系統平臺,上面摘出的代碼片段是針對IA x86_32的實現。 下面對上面的代碼做個解釋: a. 宏_ADDRESSOF(v)作用:取參數v的地址。 b. 宏_INTSIZEOF(n)作用:返回參數n的size并保證4字節對齊(32-bits平臺)。這個宏應用了一個小技巧來實現字節對齊:~(sizeof(int) - 1)的值對應的2進制值的低k位一定是0,其中sizeof(int) = 2^k,因此,在IA x86_32下,k=2。理解了這一點,那么(sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1)的作用就很直觀了,它保證了sizeof(n)的值按sizeof(int)的值做對齊,例如在32-bits平臺下,就是按4字節對齊;在64-bits平臺下,按8字節對齊。至于為什么要保證對齊,與編譯器的底層實現有關,這里不再展開。 c. _crt_va_start(ap,v)作用:通過v的內存地址來計算ap的起始地址,其中,v是可變參數函數的參數中,最后一個類型已知的參數,執行的結果是ap指向可變參數列表的第1個參數。以int snprintf(char *str, size_t size, const char *format, ...)為例,其函數參數列表中最后一個已知類型的參數是const char *format,因此,參數format對應的就是_crt_va_start(ap, v)中的v, 而ap則指向傳入的第1個可變參數。 特別需要理解的是:為什么ap = address(v) + sizeof(v),這與函數棧從高地址向低地址的增長方向 及函數調用時參數從右向左的壓棧順序有關,這里默認大家已經搞清楚了這些基礎知識,不再展開詳述。 d. _crt_va_arg(ap,t)作用:更新指針ap后,取類型為t的變量的值并返回該值。 e. _crt_va_end(ap)作用:指針ap置0,防止野指針。 概括來說,可變參數函數的實現原理是: 1)根據函數參數列表中最后一個已知類型的參數地址,得到可變參數列表的第一個可變參數 2)根據程序員指定的每個可變參數的類型,通過地址及參數類型的size獲取該參數值 3)遍歷,直到訪問完所有的可變參數 從上面的實現過程可以注意到,可變參數的函數實現嚴重依賴于函數棧及函數調用約定(主要是參數壓棧順序),同時,依賴于程序員指定的可變參數類型。因此,若指定的參數類型與實際提供的參數類型不符時,程序出core簡直就是一定的。4. 程序實例 經過上面對可變參數函數實現機制的分析,很容易實現一個帶可變參數的函數。程序實例如下:
[cpp] view plain copy#include <stdio.h> #include <stdarg.h> void foo(char *fmt, ...) { va_list ap; int d; char c, *p, *s; va_start(ap, fmt); while (*fmt) { if('%' == *fmt) { switch(*(++fmt)) { case 's': /* string */ s = va_arg(ap, char *); printf("%s", s); break; case 'd': /* int */ d = va_arg(ap, int); printf("%d", d); break; case 'c': /* char */ /* need a cast here since va_arg only takes fully promoted types */ c = (char) va_arg(ap, int); printf("%c", c); break; default: c = *fmt; printf("%c", c); } // end of switch } else { c = *fmt; printf("%c", c); } ++fmt; } va_end(ap); } int main(int argc, char * argv[]) { foo("sdccds%%, string=%s, int=%d, char=%c/n", "hello world", 211, 'k'); return 0; }【參考資料】1. linux man : va_start2. wikipedia - x86 calling conventions3. VS2008頭文件:stdarg.h和vadefs.h的源碼
================== EOF =================
新聞熱點
疑難解答