天天看点

va_start/va_arg/va_end/va_list

在看UEP的时候,作者自定义了几个error_handle函数

#include <stdarg.h>     /* ISO C variable aruments */
void    err_dump(const char *, ...);        /* {App misc_source} */
void    err_msg(const char *, ...);
void    err_quit(const char *, ...);
void    err_exit(int, const char *, ...);
void    err_ret(const char *, ...);
void    err_sys(const char *, ...);
           

其中好几个地方都有用到

va_start

va_arg

va_end

这几个宏函数,查了一些资料之后将总结记录下来。

首先声明:

测试环境是 GCC:4.8.2
          GDB:7.7.1
          kernel:3.13.0-36-generic
           

这几个宏函数主要是针对C语言里的可变参数列表做处理。

参考(还是en好理解):

vsnprintf()

write formatted output to a character array, up to a maximum number of character (varags)

Synopsis:

#include <stdarg.h>
        #include <stdio.h>
        int vsnprintf( char*   buf,
                       size_t   count,
                       const char* format,
                       va_list  arg );
           

Arguments:

buf
        A pointer to the buffer where you want to function to store the formatted string.
    count
        The maximum number of characters to store in the buffer, including a terminating null character.
    format
        A string that specifies the format of the output. The formatting string determines what additional arguments you need to provide. For more information, see printf().
    arg
        A variable-argument list of the additional arguments, which you must have initialized with the va_start() macro.
           

Library:

libc
           

Description:

The vsnprintf() function formats data under control of the format control string and stores the result in buf. The maximum number of characters to store, including a terminating null character, is specified by count.
    The vsnprintf() function is a “varargs” version of snprintf().
           

Returns:

The number of characters that would have been written into the array, not counting the terminating null character, had count been large enough. It does this even if count is zero; in this case buf can be NULL. If an error occurred, vsnprintf() returns a negative value and sets errno.
           

Examples:

Use vsnprintf() in a general error message routine:

    #include <stdio.h>
    #include <stdarg.h>
    #include <string.h>
    char msgbuf[80];
    char *fmtmsg( char *format, ... )
    {
            va_list arglist;
            va_start( arglist, format );
            strcpy( msgbuf, "Error: " );
            vsnprintf( &msgbuf[7], 80-7, format, arglist );
            va_end( arglist );
            return( msgbuf );
    }
    int main( void )
    {
        char *msg;
        msg = fmtmsg( "%s %d %s", "Failed", 100, "times" );
        printf( "%s\n", msg );
        return 0;
    }
           

下面再说说va_*的几个宏函数

va_start/va_arg/va_list

#include <stdarg.h>
  <- prototype ->
  void va_start(va_list ap, lastfix);
  type va_arg(va_list ap, type);
  void va_end(va_list ap);
           

在头文件stdarg.h中声明了一种类型(va_list)和三个宏(va_start/va_arg/va_end)

网上有好多讲stdarg.h在vc++ x86平台的宏定义(#define xxxx),但是linux下面貌似是编译器内嵌的”定义”所以并没有找到显式”定义”的地方(locate stdarg.h)

typedef __builtin_va_list __gnuc_va_list;
           
参考:http://bbs.csdn.net/topics/290044491
va_list: 该变量保存了可变参数列表的一些信息,va_arg和va_end都要用到它
           
网上说它是个指针,额。。。最开始我以为linux下同样是这个样子,因为最初只是想了解下原理不想知道细节,可是好奇用gdp跟踪了下,结果是个结构体(难不成windows下是pointer?呵呵,手头没环境不晓得诶Σ( ° △ °|||)︴):
用gdb调试,argp是一个va_list类型的变量
(gdb) ptype argp
type = struct __va_list_tag {
unsigned int gp_offset;
    unsigned int fp_offset;
    void *overflow_arg_area;
    void *reg_save_area;
} [1]
(gdb) display argp
3: argp = {{gp_offset = 8, fp_offset = 48, overflow_arg_area = 0x7fffffffe5b0, reg_save_area = 0x7fffffffe4f0}}
           

参考:http://blog.csdn.net/nanhaizhixin/article/details/6538585

上面的这个链接博客里有提到

reg_save_area

存放的是栈顶的位置(栈是从高地址向低地址增长的,同时函数接受参数的传递顺序是自右向左);

gp_offset

是参数距栈顶的偏移位置。文章后面的操作演示部分会有相关说明。
va_start:这个宏会使得ap"指"向可变参数列表的第一个参数的首地址(参数列表中第一个是确定参数,之后的才是可变参数),它必须在va_arg和va_end前先被使用,其中va_start有两个参数,ap含义如上,lastfix是传递给函数的最后一个明确(非可变)的参数值(函数传参是自右向左,最后一个其实就是正着数的第一个)
           
va_arg:这个宏的作用其实就是返回ap所"指"向的type类型(!的参数值
   重点1: type不能为char,unsigned char,float
   重点2:首次使用va_arg时,返回的是可变参数列表的第一个参数,每次调用成功都会依次返回参数列表中的下一个参数。它的实现依赖于ap的指向,根据type类型参数的长度来向后移动ap从而指向下个返回对象
           
va_end:该宏通常起一个返回(结束)作用,它会“释放”ap使其指向NULL从而使该变量失效除非再次调用va_start初始化(可能又是windows下?)。va_end应该放于参数列表全部被va_arg读取完之后使用,调用失败可能会导致一些未定义的错误。
           
参考:https://msdn.microsoft.com/en-us/library/kb57fad8.aspx

操作演示

Example1:

#include <stdio.h>
#include <stdarg.h>
/* 计算参数列表之和 */
void sum(char *msg, ...)
{
   int total = ;
   va_list ap;
   int arg;

   /* 下步操作之后ap就指向整型参数1所在的首地址了 */
   va_start(ap, msg);
   while ((arg = va_arg(ap,int)) != ) {
      /* 第一次调用va_arg返回的是1,同时ap向后“移”一个int的长度 */
      total += arg;
   }
   printf(msg, total);
   va_end(ap);
}
int main(void) 
{
   sum("The total of 1+2+3+4 is %d\n", ,,,,);
   return ;
}
           

运行结果:

Example2:

#include <stdio.h>
#include <stdarg.h>

void demo(int  arg1, ...)
{
        va_list argp;
        va_start(argp, arg1);

        int i;
        char* j = NULL;
        for (i = ; i < arg1 ; i++)
        {
            j = va_arg(argp, char*);
                printf("Argument:# %s\n", j);
        }
}

int main(int argc, char* argv[])
{
        demo(, "a", "b", "c", "d");
        return ;
}
           

运行结果:

Argument:#a
Argument:#b
Argument:#c
Argument:#d
           

Example3:(gdb调试)

同样使用例2的演示代码

va_start 前
40      demo(4, "a", "b", "c", "d");
(gdb) s
demo     (arg1=4) at va.c:7
7                     va_start(argp, arg1);
2:  j = 0x0
1: argp = {{gp_offset = 4160723472, fp_offset = 32767, overflow_arg_area = 0x1, reg_save_area = 0x0}}
           
va_start后,gp_offset的值就变为8啦(gdb调试信息是连着上面的)
(gdb) n
16              for (i = 0; i < arg1 ; i++)
2: j = 0x0
1: argp = {{gp_offset = 8, fp_offset = 48, overflow_arg_area = 0x7fffffffe5b0, reg_save_area = 0x7fffffffe4f0}}
           
连续多次va_arg之后
19            j = va_arg(argp, char*);
2:   j = 0x0
1:   argp = {{gp_offset = 8, fp_offset = 48, overflow_arg_area = 0x7fffffffe5b0, reg_save_area = 0x7fffffffe4f0}}
(gdb)n        /* 第一次va_arg,取得offset为8的"a",同时gp_offset加8
16            for (i = 0; i < arg1 ; i++)
2:   j = 0x4006aa "a"
1:   argp = {{gp_offset = 16, fp_offset = 48, overflow_arg_area = 0x7fffffffe5b0, reg_save_area = 0x7fffffffe4f0}}
(gdb)n 
19            j = va_arg(argp, char*);
2:   j = 0x4006aa "a"
1:   argp = {{gp_offset = 16, fp_offset = 48, overflow_arg_area = 0x7fffffffe5b0, reg_save_area = 0x7fffffffe4f0}}
(gdb)n        /* 第二次va_arg,取得offset为16的"b",同时gp_offset加8
16            for (i = 0; i < arg1 ; i++)
2:   j = 0x4006a8 "b"
1:   argp = {{gp_offset = 24, fp_offset = 48, overflow_arg_area = 0x7fffffffe5b0, reg_save_area = 0x7fffffffe4f0}}
(gdb)n 
19            j = va_arg(argp, char*);
2:   j = 0x4006a8 "b"
1:   argp = {{gp_offset = 24, fp_offset = 48, overflow_arg_area = 0x7fffffffe5b0, reg_save_area = 0x7fffffffe4f0}}
(gdb)n        /* 第三次va_arg,取得offset为24的"c",同时gp_offset加8
16            for (i = 0; i < arg1 ; i++)
2:   j = 0x4006a6 "c"
1:   argp = {{gp_offset = 32, fp_offset = 48, overflow_arg_area = 0x7fffffffe5b0, reg_save_area = 0x7fffffffe4f0}}
(gdb)n 
19             j = va_arg(argp, char*);
2:   j = 0x4006a6 "c"
1:   argp = {{gp_offset = 32, fp_offset = 48, overflow_arg_area = 0x7fffffffe5b0, reg_save_area = 0x7fffffffe4f0}}
(gdb)n         /* 第四次va_arg,取得offset为32的"c",同时gp_offset加8变为40
16             for (i = 0; i < arg1 ; i++)
2:   j = 0x4006a4 "d"
1:   argp = {{gp_offset = 40, fp_offset = 48, overflow_arg_area = 0x7fffffffe5b0, reg_save_area = 0x7fffffffe4f0}}
           
之前有说过reg_save_area记录的是栈顶的位置,gp_offset记录的是参数的偏移位置,下面来看一下reg_save_area指向的内存地址情况
(gdb) x/40ux 0x7fffffffe4f0
0x7fffffffe4f0: 0x00000001      0x00007fff     0x004006aa      0x00000000
0x7fffffffe500: 0x004006a8      0x00000000      0x004006a6      0x00000000
0x7fffffffe510: 0x004006a4      0x00000000      0xf7dea560      0x00007fff
0x7fffffffe520: 0x00000000      0x00000000      0xf7ffe520      0x00007fff
0x7fffffffe530: 0xffffe560      0x00007fff      0xffffe550      0x00007fff
0x7fffffffe540: 0xf63d4e2e      0x00000000      0x0040030b      0x00000000
0x7fffffffe550: 0xffffffff      0x00000000      0xffffe6b8      0x00007fff
0x7fffffffe560: 0xf7a251a8      0x00007fff      0xf7ff94c0      0x00007fff
0x7fffffffe570: 0xf7ffe1c8      0x00007fff      0x00000000      0x00000000
0x7fffffffe580: 0x00000001      0x00000000      0x0040066d      0x00000000
           
很明显就能看到,参数变量在栈里面是自高向下分配的,栈顶地址为0x7fffffffe4f0,每8个字节一个参数。
address          indirect-add     offset     var
    0x7fffffffe4f8:   0x004006aa    offset:8     "a"
    0x7fffffffe500:   0x004006a8    offset:16    "b"
    0x7fffffffe508:   0x004006a6    offset:24    "c"
    0x7fffffffe510:   0x004006a4    offset:32    "d"
           
网上说va_end会让ap指向NULL,可是在linux下调试发现调用该宏之后aprp依旧可以访问,可能是一个小的安全隐患吧,可以看看windows下是什么样的情况。

说明

本文和网上其他 文章有两点出入:

1. linux下的stdarg.h里并没有va_*的几个显式声明/定义,编译器内置好了的

2. va_list在linux下是结构体,不是的一个指针

3. 调用va_end之后,va_list变量仍然可以访问,结构体信息仍在

如有疑问和出错的地方,欢迎指正!

参考文章:

http://blog.chinaunix.net/uid-20598149-id-20818.html

http://www.opensource.apple.com/source/xnu/xnu-1456.1.26/EXTERNAL_HEADERS/stdarg.hhttp://www.opensource.apple.com/source/xnu/xnu-1456.1.26/EXTERNAL_HEADERS/stdarg.h

http://www.educity.cn/wenda/264788.html

http://www.linuxsir.org/bbs/thread55249.html?pageon=1

http://blog.csdn.net/holandstone/article/details/6947119

http://blog.csdn.net/solox1983/article/details/6697111