在看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)
参考:http://bbs.csdn.net/topics/290044491typedef __builtin_va_list __gnuc_va_list;
网上说它是个指针,额。。。最开始我以为linux下同样是这个样子,因为最初只是想了解下原理不想知道细节,可是好奇用gdp跟踪了下,结果是个结构体(难不成windows下是pointer?呵呵,手头没环境不晓得诶Σ( ° △ °|||)︴):va_list: 该变量保存了可变参数列表的一些信息,va_arg和va_end都要用到它
用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从而指向下个返回对象
参考:https://msdn.microsoft.com/en-us/library/kb57fad8.aspxva_end:该宏通常起一个返回(结束)作用,它会“释放”ap使其指向NULL从而使该变量失效除非再次调用va_start初始化(可能又是windows下?)。va_end应该放于参数列表全部被va_arg读取完之后使用,调用失败可能会导致一些未定义的错误。
操作演示
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 前va_start后,gp_offset的值就变为8啦(gdb调试信息是连着上面的)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_arg之后(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}}
之前有说过reg_save_area记录的是栈顶的位置,gp_offset记录的是参数的偏移位置,下面来看一下reg_save_area指向的内存地址情况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}}
很明显就能看到,参数变量在栈里面是自高向下分配的,栈顶地址为0x7fffffffe4f0,每8个字节一个参数。(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
网上说va_end会让ap指向NULL,可是在linux下调试发现调用该宏之后aprp依旧可以访问,可能是一个小的安全隐患吧,可以看看windows下是什么样的情况。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"
说明
本文和网上其他 文章有两点出入:
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