C/C++中用va_start/va_arg/va_end实现可变参数函数的原理

本文深入解析C/C++中使用va_start/va_arg/va_end实现可变参数函数的原理,涵盖Linux/Windows平台实现细节,附带实例说明。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

C/C++中用va_start/va_arg/va_end实现可变参数函数的原理与实例详解

        在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中,宏定义的相关代码如下:

    #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()函数的调用路径:

// 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集成的编译器中,对这三个函数有很明确的实现代码,摘出如下。

/* 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中:

/* 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. 程序实例
        经过上面对可变参数函数实现机制的分析,很容易实现一个带可变参数的函数。程序实例如下:

#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;
}

 

        上面的代码很简单,旨在抛砖引玉,只要真正搞清楚了可变参数函数的原理,相信各位会写出更加复杂精细的可变参函数。

分析以下代码: uint32_t FAST_FUNC getopt32(char **argv, const char *applet_opts, ...) { int argc; unsigned flags = 0; unsigned requires = 0; t_complementary complementary[33]; /* last stays zero-filled */ char first_char; int c; const unsigned char *s; t_complementary *on_off; va_list p; #if ENABLE_LONG_OPTS || ENABLE_FEATURE_GETOPT_LONG const struct option *l_o; struct option *long_options = (struct option *) &bb_null_long_options; #endif unsigned trigger; char **pargv; int min_arg = 0; int max_arg = -1; #define SHOW_USAGE_IF_ERROR 1 #define ALL_ARGV_IS_OPTS 2 #define FIRST_ARGV_IS_OPT 4 int spec_flgs = 0; /* skip 0: some applets cheat: they do not actually HAVE argv[0] */ argc = 1; while (argv[argc]) argc++; va_start(p, applet_opts); c = 0; on_off = complementary; memset(on_off, 0, sizeof(complementary)); /* skip bbox extension */ first_char = applet_opts[0]; if (first_char == '!') applet_opts++; /* skip GNU extension */ s = (const unsigned char *)applet_opts; if (*s == '+' || *s == '-') s++; while (*s) { if (c >= 32) break; on_off->opt_char = *s; on_off->switch_on = (1 << c); if (*++s == ':') { on_off->optarg = va_arg(p, void **); while (*++s == ':') continue; } on_off++; c++; } #if ENABLE_LONG_OPTS || ENABLE_FEATURE_GETOPT_LONG if (applet_long_options) { const char *optstr; unsigned i, count; count = 1; optstr = applet_long_options; while (optstr[0]) { optstr += strlen(optstr) + 3; /* skip NUL, has_arg, val */ count++; } /* count == no. of longopts + 1 */ long_options = alloca(count * sizeof(*long_options)); memset(long_options, 0, count * sizeof(*long_options)); i = 0; optstr = applet_long_options; while (--count) { long_options[i].name = optstr; optstr += strlen(optstr) + 1; long_options[i].has_arg = (unsigned char)(*optstr++); /* long_options[i].flag = NULL; */ long_options[i].val = (unsigned char)(*optstr++); i++; } for (l_o = long_options; l_o->name; l_o++) { if (l_o->flag) continue; for (on_off = complementary; on_off->opt_char; on_off++) if (on_off->opt_char == l_o->val) goto next_long; if (c >= 32) break; on_off->opt_char = l_o->val; on_off->switch_on = (1 << c); if (l_o->has_arg != no_argument) on_off->optarg = va_arg(p, void **); c++; next_long: ; } /* Make it unnecessary to clear applet_long_options * by hand after each call to getopt32 */ applet_long_options = NULL; } #endif /* ENABLE_LONG_OPTS || ENABLE_FEATURE_GETOPT_LONG */ for (s = (const unsigned char *)opt_complementary; s && *s; s++) { t_complementary *pair; unsigned *pair_switch; if (*s == ':') continue; c = s[1]; if (*s == '?') { if (c < '0' || c > '9') { spec_flgs |= SHOW_USAGE_IF_ERROR; } else { max_arg = c - '0'; s++; } continue; } if (*s == '-') { if (c < '0' || c > '9') { if (c == '-') { spec_flgs |= FIRST_ARGV_IS_OPT; s++; } else spec_flgs |= ALL_ARGV_IS_OPTS; } else { min_arg = c - '0'; s++; } continue; } if (*s == '=') { min_arg = max_arg = c - '0'; s++; continue; } for (on_off = complementary; on_off->opt_char; on_off++) if (on_off->opt_char == *s) goto found_opt; /* Without this, diagnostic of such bugs is not easy */ bb_error_msg_and_die("NO OPT %c!", *s); found_opt: if (c == ':' && s[2] == ':') { on_off->param_type = PARAM_LIST; continue; } if (c == '+' && (s[2] == ':' || s[2] == '\0')) { on_off->param_type = PARAM_INT; s++; continue; } if (c == ':' || c == '\0') { requires |= on_off->switch_on; continue; } if (c == '-' && (s[2] == ':' || s[2] == '\0')) { flags |= on_off->switch_on; on_off->incongruously |= on_off->switch_on; s++; continue; } if (c == *s) { on_off->counter = va_arg(p, int *); s++; } pair = on_off; pair_switch = &pair->switch_on; for (s++; *s && *s != ':'; s++) { if (*s == '?') { pair_switch = &pair->requires; } else if (*s == '-') { if (pair_switch == &pair->switch_off) pair_switch = &pair->incongruously; else pair_switch = &pair->switch_off; } else { for (on_off = complementary; on_off->opt_char; on_off++) if (on_off->opt_char == *s) { *pair_switch |= on_off->switch_on; break; } } } s--; } opt_complementary = NULL; va_end(p); if (spec_flgs & (FIRST_ARGV_IS_OPT | ALL_ARGV_IS_OPTS)) { pargv = argv + 1; while (*pargv) { if (pargv[0][0] != '-' && pargv[0][0] != '\0') { /* Can't use alloca: opts with params will * return pointers to stack! * NB: we leak these allocations... */ char *pp = xmalloc(strlen(*pargv) + 2); *pp = '-'; strcpy(pp + 1, *pargv); *pargv = pp; } if (!(spec_flgs & ALL_ARGV_IS_OPTS)) break; pargv++; } } /* In case getopt32 was already called: * reset the libc getopt() function, which keeps internal state. * run_nofork_applet() does this, but we might end up here * also via gunzip_main() -> gzip_main(). Play safe. */ #ifdef __GLIBC__ optind = 0; #else /* BSD style */ optind = 1; /* optreset = 1; */ #endif /* optarg = NULL; opterr = 0; optopt = 0; - do we need this?? */ /* Note: just "getopt() <= 0" will not work well for * "fake" short options, like this one: * wget $'-\203' "Test: test" http://kernel.org/ * (supposed to act as --header, but doesn't) */ #if ENABLE_LONG_OPTS || ENABLE_FEATURE_GETOPT_LONG while ((c = getopt_long(argc, argv, applet_opts, long_options, NULL)) != -1) { #else while ((c = getopt(argc, argv, applet_opts)) != -1) { #endif /* getopt prints "option requires an argument -- X" * and returns '?' if an option has no arg, but one is reqd */ c &= 0xff; /* fight libc's sign extension */ for (on_off = complementary; on_off->opt_char != c; on_off++) { /* c can be NUL if long opt has non-NULL ->flag, * but we construct long opts so that flag * is always NULL (see above) */ if (on_off->opt_char == '\0' /* && c != '\0' */) { /* c is probably '?' - "bad option" */ goto error; } } if (flags & on_off->incongruously) goto error; trigger = on_off->switch_on & on_off->switch_off; flags &= ~(on_off->switch_off ^ trigger); flags |= on_off->switch_on ^ trigger; flags ^= trigger; if (on_off->counter) (*(on_off->counter))++; if (optarg) { if (on_off->param_type == PARAM_LIST) { llist_add_to_end((llist_t **)(on_off->optarg), optarg); } else if (on_off->param_type == PARAM_INT) { //TODO: xatoi_positive indirectly pulls in printf machinery *(unsigned*)(on_off->optarg) = xatoi_positive(optarg); } else if (on_off->optarg) { *(char **)(on_off->optarg) = optarg; } } } /* check depending requires for given options */ for (on_off = complementary; on_off->opt_char; on_off++) { if (on_off->requires && (flags & on_off->switch_on) && (flags & on_off->requires) == 0 ) { goto error; } } if (requires && (flags & requires) == 0) goto error; argc -= optind; if (argc < min_arg || (max_arg >= 0 && argc > max_arg)) goto error; option_mask32 = flags; return flags; error: if (first_char != '!') bb_show_usage(); return (int32_t)-1; }
最新发布
08-15
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值