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

2024-01-09 21:58

本文主要是介绍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;
}

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

【参考资料】
1. linux man : va_start
2. wikipedia - x86 calling conventions
3. VS2008头文件:stdarg.h和vadefs.h的源码

================== EOF =================


这篇关于C/C++中用va_start/va_arg/va_end实现可变参数函数的原理与实例详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/588560

相关文章

C#实现系统信息监控与获取功能

《C#实现系统信息监控与获取功能》在C#开发的众多应用场景中,获取系统信息以及监控用户操作有着广泛的用途,比如在系统性能优化工具中,需要实时读取CPU、GPU资源信息,本文将详细介绍如何使用C#来实现... 目录前言一、C# 监控键盘1. 原理与实现思路2. 代码实现二、读取 CPU、GPU 资源信息1.

SpringBoot实现动态插拔的AOP的完整案例

《SpringBoot实现动态插拔的AOP的完整案例》在现代软件开发中,面向切面编程(AOP)是一种非常重要的技术,能够有效实现日志记录、安全控制、性能监控等横切关注点的分离,在传统的AOP实现中,切... 目录引言一、AOP 概述1.1 什么是 AOP1.2 AOP 的典型应用场景1.3 为什么需要动态插

详解如何在React中执行条件渲染

《详解如何在React中执行条件渲染》在现代Web开发中,React作为一种流行的JavaScript库,为开发者提供了一种高效构建用户界面的方式,条件渲染是React中的一个关键概念,本文将深入探讨... 目录引言什么是条件渲染?基础示例使用逻辑与运算符(&&)使用条件语句列表中的条件渲染总结引言在现代

Python调用另一个py文件并传递参数常见的方法及其应用场景

《Python调用另一个py文件并传递参数常见的方法及其应用场景》:本文主要介绍在Python中调用另一个py文件并传递参数的几种常见方法,包括使用import语句、exec函数、subproce... 目录前言1. 使用import语句1.1 基本用法1.2 导入特定函数1.3 处理文件路径2. 使用ex

详解Vue如何使用xlsx库导出Excel文件

《详解Vue如何使用xlsx库导出Excel文件》第三方库xlsx提供了强大的功能来处理Excel文件,它可以简化导出Excel文件这个过程,本文将为大家详细介绍一下它的具体使用,需要的小伙伴可以了解... 目录1. 安装依赖2. 创建vue组件3. 解释代码在Vue.js项目中导出Excel文件,使用第三

SQL注入漏洞扫描之sqlmap详解

《SQL注入漏洞扫描之sqlmap详解》SQLMap是一款自动执行SQL注入的审计工具,支持多种SQL注入技术,包括布尔型盲注、时间型盲注、报错型注入、联合查询注入和堆叠查询注入... 目录what支持类型how---less-1为例1.检测网站是否存在sql注入漏洞的注入点2.列举可用数据库3.列举数据库

Linux之软件包管理器yum详解

《Linux之软件包管理器yum详解》文章介绍了现代类Unix操作系统中软件包管理和包存储库的工作原理,以及如何使用包管理器如yum来安装、更新和卸载软件,文章还介绍了如何配置yum源,更新系统软件包... 目录软件包yumyum语法yum常用命令yum源配置文件介绍更新yum源查看已经安装软件的方法总结软

Oracle查询优化之高效实现仅查询前10条记录的方法与实践

《Oracle查询优化之高效实现仅查询前10条记录的方法与实践》:本文主要介绍Oracle查询优化之高效实现仅查询前10条记录的相关资料,包括使用ROWNUM、ROW_NUMBER()函数、FET... 目录1. 使用 ROWNUM 查询2. 使用 ROW_NUMBER() 函数3. 使用 FETCH FI

Python脚本实现自动删除C盘临时文件夹

《Python脚本实现自动删除C盘临时文件夹》在日常使用电脑的过程中,临时文件夹往往会积累大量的无用数据,占用宝贵的磁盘空间,下面我们就来看看Python如何通过脚本实现自动删除C盘临时文件夹吧... 目录一、准备工作二、python脚本编写三、脚本解析四、运行脚本五、案例演示六、注意事项七、总结在日常使用

Java实现Excel与HTML互转

《Java实现Excel与HTML互转》Excel是一种电子表格格式,而HTM则是一种用于创建网页的标记语言,虽然两者在用途上存在差异,但有时我们需要将数据从一种格式转换为另一种格式,下面我们就来看看... Excel是一种电子表格格式,广泛用于数据处理和分析,而HTM则是一种用于创建网页的标记语言。虽然两