好记性不如铅笔头

编程

C语言中可变参数函数实现原理

参考链接

【 http://www.cnblogs.com/cpoint/p/3368993.html

CONTENTS

C函数调用的栈结构

可变参数函数的实现与函数调用的栈结构密切相关,正常情况下C的函数参数入栈规则为__stdcall, 它是从右到左的,即函数中的最右边的参数最先入栈。例如,对于函数:
void fun(int a, int b, int c)
{
    int d;
    …
}
其栈结构为
0x1ffc–>d
0x2000–>a
0x2004–>b
0x2008–>c
对于在32位系统的多数编译器,每个栈单元的大小都是sizeof(int), 而函数的每个参数都至少要占一个栈单元大小,如函数 void fun1(char a, int b, double c, short d) 对一个32的系统其栈的结构就是
0x1ffc–>a  (4字节)(为了字节对齐)
0x2000–>b  (4字节)
0x2004–>c  (8字节)
0x200c–>d  (4字节)
因此,函数的所有参数是存储在线性连续的栈空间中的,基于这种存储结构,这样就可以从可变参数函数中必须有的第一个普通参数来寻址后续的所有可变参数的类型及其值。

先看看固定参数列表函数:
void fixed_args_func(int a, double b, char *c)
{
    printf(“a = 0x%p\n”, &a);
    printf(“b = 0x%p\n”, &b);
    printf(“c = 0x%p\n”, &c);
}
对于固定参数列表的函数,每个参数的名称、类型都是直接可见的,他们的地址也都是可以直接得到的,比如:通过&a我们可以得到a的地址,并通过函数原型声明了解到a是int类型的。
但是对于变长参数的函数,我们就没有这么顺利了。还好,按照C标准的说明,支持变长参数的函数在原型声明中,必须有至少一个最左固定参数(这一点与传统C有区别,传统C允许不带任何固定参数的纯变长参数函数),这样我们可以得到其中固定参数的地址,但是依然无法从声明中得到其他变长参数的地址,比如:
void var_args_func(const char * fmt, …) 
{
    … … 
}
这里我们只能得到fmt这固定参数的地址,仅从函数原型我们是无法确定”…”中有几个参数、参数都是什么类型的。回想一下函数传参的过程,无论”…”中有多少个参数、每个参数是什么类型的,它们都和固定参数的传参过程是一样的,简单来讲都是栈操作,而栈这个东西对我们是开放的。这样一来,一旦我们知道某函数帧的栈上的一个固定参数的位置,我们完全有可能推导出其他变长参数的位置。
我们先用上面的那个fixed_args_func函数确定一下入栈顺序。
int main() 
{
    fixed_args_func(17, 5.40, “hello world”);
    return 0;
}
a = 0x0022FF50
b = 0x0022FF54
c = 0x0022FF5C
从这个结果来看,显然参数是从右到左,逐一压入栈中的(栈的延伸方向是从高地址到低地址,栈底的占领着最高内存地址,先入栈的参数,其地理位置也就最高了)。

stdarg.h头文件源代码分析

参考链接【 https://www.cnblogs.com/cpoint/p/3374994.html
谈到C语言中可变参数函数的实现(参见C语言中可变参数函数实现原理),有一个头文件不得不谈,那就是stdarg.h
本文从minix源码中的stdarg.h头文件入手进行分析:

#ifndef _STDARG_H
#define _STDARG_H


#ifdef __GNUC__
/* The GNU C-compiler uses its own, but similar varargs mechanism. */

typedef char *va_list;

/* Amount of space required in an argument list for an arg of type TYPE.
 * TYPE may alternatively be an expression whose type is used.
 */

#define __va_rounded_size(TYPE)  \
  (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))

#if __GNUC__ < 2

#ifndef __sparc__
#define va_start(AP, LASTARG)                                           \
 (AP = ((char *) &(LASTARG) + __va_rounded_size (LASTARG)))
#else
#define va_start(AP, LASTARG)                                           \
 (__builtin_saveregs (),                                                \
  AP = ((char *) &(LASTARG) + __va_rounded_size (LASTARG)))
#endif

void va_end (va_list);          /* Defined in gnulib */
#define va_end(AP)

#define va_arg(AP, TYPE)                                                \
 (AP += __va_rounded_size (TYPE),                                       \
  *((TYPE *) (AP - __va_rounded_size (TYPE))))

#else    /* __GNUC__ >= 2 */

#ifndef __sparc__
#define va_start(AP, LASTARG)                         \
 (AP = ((char *) __builtin_next_arg ()))
#else
#define va_start(AP, LASTARG)                    \
  (__builtin_saveregs (), AP = ((char *) __builtin_next_arg ()))
#endif

void va_end (va_list);        /* Defined in libgcc.a */
#define va_end(AP)

#define va_arg(AP, TYPE)                        \
 (AP = ((char *) (AP)) += __va_rounded_size (TYPE),            \
  *((TYPE *) ((char *) (AP) - __va_rounded_size (TYPE))))

#endif    /* __GNUC__ >= 2 */

#else    /* not __GNUC__ */


typedef char *va_list;

#define __vasz(x)        ((sizeof(x)+sizeof(int)-1) & ~(sizeof(int) -1))

#define va_start(ap, parmN)    ((ap) = (va_list)&parmN + __vasz(parmN))
#define va_arg(ap, type)      \
  (*((type *)((va_list)((ap) = (void *)((va_list)(ap) + __vasz(type))) \
                            - __vasz(type))))
#define va_end(ap)


#endif /* __GNUC__ */

#endif /* _STDARG_H */

从代码中可以看到,里面编译器的版本以及相关的大量宏定义

第5行: #ifdef __GNUC__
作用是条件编译,__GNUC__为GCC中定义的宏。GCC的版本,为一个整型值。如果你需要知道自己的程序是否被GCC编译,可以简单的测试一下__GNUC__,假如你代码需要运行在GCC某个特定的版本下,那么你就要小心了,因为GCC的主要版本在增加,如果你想定义宏的方式直接实现控制,你可以写如下的代码(参见伯克利大学网站):

/* 测试 GCC > 3.2.0 ? */
#if __GNUC__ > 3 || \
    (__GNUC__ == 3 && (__GNUC_MINOR__ > 2 || \
                       (__GNUC_MINOR__ == 2 && \
                        __GNUC_PATCHLEVEL__ > 0))
你还可以使用下面一个类似的方法:

#define GCC_VERSION (__GNUC__ * 10000 \
                     + __GNUC_MINOR__ * 100 \
                     + __GNUC_PATCHLEVEL__)

/*测试 GCC > 3.2.0 ?*/
#if GCC_VERSION > 30200
第8行: 使用typedef进行了一个声明:typedef char *va_list;

第14行:定义了用于编译器的内存对齐宏(参见C语言内存对齐详解(3)):

#define __va_rounded_size(TYPE)  \
     (((sizeof (TYPE) + sizeof (int) – 1) / sizeof (int)) * sizeof (int))
第17行:#if __GNUC__ < 2,进行GCC的版本判断,看当前版本是否大于2

第19行:#ifndef __sparc__ 可扩充处理器架构宏(以后再深入研究)

第20行:使得ap指向函数中的第一个无名参数的首地址的宏:

#define va_start(AP, LASTARG)                                           \
   (AP = ((char *) &(LASTARG) + __va_rounded_size (LASTARG)))
第31行:

#define va_arg(AP, TYPE)                                                \
   (AP += __va_rounded_size (TYPE),                                       \
    *((TYPE *) (AP – __va_rounded_size (TYPE))))
va_arg宏使得ap指向下一个参数,已经处理了内存对齐,其中参数的类型为TYPE

第48行:
void va_end (va_list);          /* Defined in gnulib */
定义在gnulib中,va_end 与va_start成对使用.在有些代码中定义为:
#define va_end(ap)      ( ap = (va_list)0 )

C stdarg.h的使用

https://blog.csdn.net/u012005313/article/details/52122077

参考:
stdarg.h:https://zh.wikipedia.org/wiki/Stdarg.h
stdarg.h:http://baike.baidu.com/view/3373010.htm
linux环境下可以使用man手册:man stdarg
C语言也存在可变参数的概念
最常见的就是scanf和printf函数:
int scanf(const char * restrict format,…);
int printf(const char *fmt, …);
你可以输入任意类型的任意个参数,但是必须在格式化字符串中确定输入参数的个数和类型。
那么我们如何自定义可变参数函数呢?
就需要使用stdarg.h头文件了。stdarg的全称就是standard arguments(标准参数),主要目的就是为了让函数能够接收可变参数。
它为用户定义了4个标准宏:
/* Define the standard macros for the user,
   if this invocation was from the user program.  */
#ifdef _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 || defined(__GXX_EXPERIMENTAL_CXX0X__)
#define va_copy(d,s)    __builtin_va_copy(d,s)
#endif

同时它定义了一个类型va_list
注意:如果想要使用stdarg.h中的宏定义和类型对象,必须显示定义头文件#include <stdarg.h>
接下来先介绍4个宏定义:
void va_start(va_list ap, last);
va_start函数初始化了va_list对象ap,为之后的va_arg和va_end函数作准备,所以必须首先调用。
参数last指的是变量参数列表之前的参数名,也就是调用函数中最后一个已知参数类型的参数。比如,printf函数中的fmt
因为last参数的地址会在va_start函数中使用,所以last不应该是一个寄存器变量,函数或者数组类型。
type va_arg(va_list ap, type);
va_arg函数返回ap当前指向的参数的值。
参数ap就是va_start初始化的va_list对象;
参数type是一个类型名,比如“char”,“int”等,表示当前ap指向的参数的类型
每次调用va_arg后,ap就会指向下一个参数。但如果已经遍历完参数列表,或者参数type并不是当前参数的实际类型名,此时调用va_arg函数将会发生随机错误。
ap被参数va_arg函数使用过后,将无法回到最开始的位置
void va_end(va_list ap);
va_end函数和va_start相对应。在同一个函数中,调用过va_start之后就必须调用va_end。
使用va_end以后,变量ap将重置为空,并释放内存。
void va_copy(va_list dest, va_list src);
C99标准。如果想要多次使用参数列表,那么可以使用va_copy函数。
每次调用过va_copy函数后,必须相应的在同一个函数中调用va_end函数,比如:
va_list aq;
va_copy(aq, ap);

va_end(aq)
有些情况下,va_copy函数已经在其他地方有所定义,所以使用相同功能的函数__va_copy。
注意:va_start/va_arg/va_end函数符合C89标准。而va_copy是C99定义的。

发表评论

19 − 8 =

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据