一、可变参数函数实现原理
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); }
void var_args_func(const char * fmt, ...) { ... ... }
我们先用上面的那个fixed_args_func函数确定一下入栈顺序。
int main() { fixed_args_func(17, 5.40, "hello world"); return 0; } a = 0x0022FF50 b = 0x0022FF54 c = 0x0022FF5C
我们基本可以得出这样一个结论:
c.addr = b.addr + x_sizeof(b); /*注意: x_sizeof !=sizeof */ b.addr = a.addr + x_sizeof(a);
有了以上的"等式",我们似乎可以推导出 void var_args_func(const char * fmt, ... ) 函数中,可变参数的位置了。起码第一个可变参数的位置应该是:first_vararg.addr = fmt.addr + x_sizeof(fmt); 根据这一结论我们试着实现一个支持可变参数的函数:
期待输出结果:
4
5
hello world
改正后的代码如下:
var_args_func只是为了演示,并未根据fmt消息中的格式字符串来判断变参的个数和类型,而是直接在实现中写死了。
为了满足代码的可移植性,C标准库在stdarg.h中提供了诸多便利以供实现变长长度参数时使用。这里也列出一个简单的例子,看看利用标准库是如何支持变长参数的:
下面我们来探讨如何写一个简单的可变参数的C 函数.
使用可变参数应该有以下步骤:
1)首先在函数里定义一个va_list型的变量,这里是arg_ptr,这个变量是指向参数的指针.
2)然后用va_start宏初始化变量arg_ptr,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数.
3)然后用va_arg返回可变的参数,并赋值给整数j. va_arg的第二个参数是你要返回的参数的类型,这里是int型.
4)最后用va_end宏结束可变参数的获取.然后你就可以在函数里使用第二个参数了.如果函数有多个可变参数的,依次调用va_arg获取各个参数.
在《C程序设计语言》中,Ritchie提供了一个简易版printf函数:
二、stdarg.h头文件源代码分析
谈到C语言中可变参数函数的实现,有一个头文件不得不谈,那就是stdarg.h
接下来从minix源码中的stdarg.h头文件入手进行分析:
1 #ifndef _STDARG_H 2 #define _STDARG_H 3 4 5 #ifdef __GNUC__ 6 /* The GNU C-compiler uses its own, but similar varargs mechanism. */ 7 8 typedef char *va_list; 9 10 /* Amount of space required in an argument list for an arg of type TYPE. 11 * TYPE may alternatively be an expression whose type is used. 12 */ 13 14 #define __va_rounded_size(TYPE) \ 15 (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int)) 16 17 #if __GNUC__ < 2 18 19 #ifndef __sparc__ 20 #define va_start(AP, LASTARG) \ 21 (AP = ((char *) &(LASTARG) + __va_rounded_size (LASTARG))) 22 #else 23 #define va_start(AP, LASTARG) \ 24 (__builtin_saveregs (), \ 25 AP = ((char *) &(LASTARG) + __va_rounded_size (LASTARG))) 26 #endif 27 28 void va_end (va_list); /* Defined in gnulib */ 29 #define va_end(AP) 30 31 #define va_arg(AP, TYPE) \ 32 (AP += __va_rounded_size (TYPE), \ 33 *((TYPE *) (AP - __va_rounded_size (TYPE)))) 34 35 #else /* __GNUC__ >= 2 */ 36 37 #ifndef __sparc__ 38 #define va_start(AP, LASTARG) \ 39 (AP = ((char *) __builtin_next_arg ())) 40 #else 41 #define va_start(AP, LASTARG) \ 42 (__builtin_saveregs (), AP = ((char *) __builtin_next_arg ())) 43 #endif 44 45 void va_end (va_list); /* Defined in libgcc.a */ 46 #define va_end(AP) 47 48 #define va_arg(AP, TYPE) \ 49 (AP = ((char *) (AP)) += __va_rounded_size (TYPE), \ 50 *((TYPE *) ((char *) (AP) - __va_rounded_size (TYPE)))) 51 52 #endif /* __GNUC__ >= 2 */ 53 54 #else /* not __GNUC__ */ 55 56 57 typedef char *va_list; 58 59 #define __vasz(x) ((sizeof(x)+sizeof(int)-1) & ~(sizeof(int) -1)) 60 61 #define va_start(ap, parmN) ((ap) = (va_list)&parmN + __vasz(parmN)) 62 #define va_arg(ap, type) \ 63 (*((type *)((va_list)((ap) = (void *)((va_list)(ap) + __vasz(type))) \ - __vasz(type)))) 65 #define va_end(ap) 66 67 68 #endif /* __GNUC__ */ 69 70 #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行:定义了用于编译器的内存对齐宏,参见:(),
#define __va_rounded_size(TYPE) \ (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))
第17行:#if __GNUC__ < 2,进行GCC的版本判断,看当前版本是否大于2
第19行:#ifndef __sparc__ 可扩充处理器架构宏(以后再深入研究)
#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语言scanf函数的实现
上文从理论上详细介绍了C语言中可变参数函数的实现,接下来从minix内核源码中的scanf函数入手,学习C语言经典可变参数函数的实现过程
在scanf.c文件中,可以看到scanf函数,代码如下:
#include <stdio.h> #include <stdarg.h> #include "loc_incl.h" int scanf(const char *format, ...) { va_list ap; int retval; va_start(ap, format); retval = _doscan(stdin, format, ap); va_end(ap); return retval; }
对于va_list、va_start、va_end等在stdarg.h头文件中定义的宏,都已经在上文中介绍过。
在上述代码中我们可以看到有一个_doscan函数,而这一函数在头文件loc_incl.h中定义,函数声明如下:
int _doscan(FILE * stream, const char *format, va_list ap);
_doscan函数的实现源代码如下:
1 int 2 _doscan(register FILE *stream, const char *format, va_list ap) 3 { 4 int done = 0; /* number of items done */ 5 int nrchars = 0; /* number of characters read */ 6 int conv = 0; /* # of conversions */ 7 int base; /* conversion base */ 8 unsigned long val; /* an integer value */ 9 register char *str; /* temporary pointer */ 10 char *tmp_string; /* ditto */ 11 unsigned width = 0; /* width of field */ 12 int flags; /* some flags */ 13 int reverse; /* reverse the checking in [...] */ 14 int kind; 15 register int ic = EOF; /* the input character */ 16 #ifndef NOFLOAT 17 long double ld_val; 18 #endif 19 20 if (!*format) return 0; 21 22 while (1) { 23 if (isspace(*format)) { 24 while (isspace(*format)) 25 format++; /* skip whitespace */ 26 ic = getc(stream); 27 nrchars++; 28 while (isspace (ic)) { 29 ic = getc(stream); 30 nrchars++; 31 } 32 if (ic != EOF) ungetc(ic,stream); 33 nrchars--; 34 } 35 if (!*format) break; /* end of format */ 36 37 if (*format != '%') { 38 ic = getc(stream); 39 nrchars++; 40 if (ic != *format++) break; /* error */ 41 continue; 42 } 43 format++; 44 if (*format == '%') { 45 ic = getc(stream); 46 nrchars++; 47 if (ic == '%') { 48 format++; 49 continue; 50 } 51 else break; 52 } 53 flags = 0; 54 if (*format == '*') { 55 format++; 56 flags |= FL_NOASSIGN; 57 } 58 if (isdigit (*format)) { 59 flags |= FL_WIDTHSPEC; 60 for (width = 0; isdigit (*format);) 61 width = width * 10 + *format++ - '0'; 62 } 63 switch (*format) { 65 case 'h': flags |= FL_SHORT; format++; break; 66 case 'l': flags |= FL_LONG; format++; break; 67 case 'L': flags |= FL_LONGDOUBLE; format++; break; 68 } 69 kind = *format; 70 if ((kind != 'c') && (kind != '[') && (kind != 'n')) { 71 do { 72 ic = getc(stream); 73 nrchars++; 74 } while (isspace(ic)); 75 if (ic == EOF) break; /* outer while */ 76 } else if (kind != 'n') { /* %c or %[ */ 77 ic = getc(stream); 78 if (ic == EOF) break; /* outer while */ 79 nrchars++; 80 } 81 switch (kind) { 82 default: 83 /* not recognized, like %q */ 84 return conv || (ic != EOF) ? done : EOF; 85 break; 86 case 'n': 87 if (!(flags & FL_NOASSIGN)) { /* silly, though */ 88 if (flags & FL_SHORT) *va_arg(ap, short *) = (short) nrchars; 90 else if (flags & FL_LONG) 91 *va_arg(ap, long *) = (long) nrchars; 92 else 93 *va_arg(ap, int *) = (int) nrchars; 94 } 95 break; 96 case 'p': /* pointer */ 97 set_pointer(flags); 98 /* fallthrough */ 99 case 'b': /* binary */ 100 case 'd': /* decimal */ 101 case 'i': /* general integer */ 102 case 'o': /* octal */ 103 case 'u': /* unsigned */ 104 case 'x': /* hexadecimal */ 105 case 'X': /* ditto */ 106 if (!(flags & FL_WIDTHSPEC) || width > NUMLEN) 107 width = NUMLEN; 108 if (!width) return done; 109 110 str = o_collect(ic, stream, kind, width, &base); 111 if (str < inp_buf 112 || (str == inp_buf 113 && (*str == '-' 114 || *str == '+'))) return done; 115 116 /* 117 * Although the length of the number is str-inp_buf+1 118 * we don't add the 1 since we counted it already 119 */ 120 nrchars += str - inp_buf; 121 122 if (!(flags & FL_NOASSIGN)) { 123 if (kind == 'd' || kind == 'i') 124 val = strtol(inp_buf, &tmp_string, base); 125 else 126 val = strtoul(inp_buf, &tmp_string, base); 127 if (flags & FL_LONG) 128 *va_arg(ap, unsigned long *) = (unsigned long) val; 129 else if (flags & FL_SHORT) 130 *va_arg(ap, unsigned short *) = (unsigned short) val; 131 else 132 *va_arg(ap, unsigned *) = (unsigned) val; 133 } 134 break; 135 case 'c': 136 if (!(flags & FL_WIDTHSPEC)) 137 width = 1; 138 if (!(flags & FL_NOASSIGN)) 139 str = va_arg(ap, char *); 140 if (!width) return done; 141 142 while (width && ic != EOF) { 143 if (!(flags & FL_NOASSIGN)) 144 *str++ = (char) ic; 145 if (--width) { 146 ic = getc(stream); 147 nrchars++; 148 } 149 } 150 151 if (width) { 152 if (ic != EOF) ungetc(ic,stream); 153 nrchars--; 154 } 155 break; 156 case 's': 157 if (!(flags & FL_WIDTHSPEC)) 158 width = 0xffff; 159 if (!(flags & FL_NOASSIGN)) 160 str = va_arg(ap, char *); 161 if (!width) return done; 162 163 while (width && ic != EOF && !isspace(ic)) { 1 if (!(flags & FL_NOASSIGN)) 165 *str++ = (char) ic; 166 if (--width) { 167 ic = getc(stream); 168 nrchars++; 169 } 170 } 171 /* terminate the string */ 172 if (!(flags & FL_NOASSIGN)) 173 *str = '\0'; 174 if (width) { 175 if (ic != EOF) ungetc(ic,stream); 176 nrchars--; 177 } 178 break; 179 case '[': 180 if (!(flags & FL_WIDTHSPEC)) 181 width = 0xffff; 182 if (!width) return done; 183 184 if ( *++format == '^' ) { 185 reverse = 1; 186 format++; 187 } else 188 reverse = 0; 1 190 for (str = Xtable; str < &Xtable[NR_CHARS] 191 ; str++) 192 *str = 0; 193 194 if (*format == ']') Xtable[*format++] = 1; 195 196 while (*format && *format != ']') { 197 Xtable[*format++] = 1; 198 if (*format == '-') { 199 format++; 200 if (*format 201 && *format != ']' 202 && *(format) >= *(format -2)) { 203 int c; 204 205 for( c = *(format -2) + 1 206 ; c <= *format ; c++) 207 Xtable[c] = 1; 208 format++; 209 } 210 else Xtable['-'] = 1; 211 } 212 } 213 if (!*format) return done; 214 215 if (!(Xtable[ic] ^ reverse)) { 216 /* MAT 8/9/96 no match must return character */ 217 ungetc(ic, stream); 218 return done; 219 } 220 221 if (!(flags & FL_NOASSIGN)) 222 str = va_arg(ap, char *); 223 224 do { 225 if (!(flags & FL_NOASSIGN)) 226 *str++ = (char) ic; 227 if (--width) { 228 ic = getc(stream); 229 nrchars++; 230 } 231 } while (width && ic != EOF && (Xtable[ic] ^ reverse)); 232 233 if (width) { 234 if (ic != EOF) ungetc(ic, stream); 235 nrchars--; 236 } 237 if (!(flags & FL_NOASSIGN)) { /* terminate string */ 238 *str = '\0'; 239 } 240 break; 241 #ifndef NOFLOAT 242 case 'e': 243 case 'E': 244 case 'f': 245 case 'g': 246 case 'G': 247 if (!(flags & FL_WIDTHSPEC) || width > NUMLEN) 248 width = NUMLEN; 249 250 if (!width) return done; 251 str = f_collect(ic, stream, width); 252 253 if (str < inp_buf 254 || (str == inp_buf 255 && (*str == '-' 256 || *str == '+'))) return done; 257 258 /* 259 * Although the length of the number is str-inp_buf+1 260 * we don't add the 1 since we counted it already 261 */ 262 nrchars += str - inp_buf; 263 2 if (!(flags & FL_NOASSIGN)) { 265 ld_val = strtod(inp_buf, &tmp_string); 266 if (flags & FL_LONGDOUBLE) 267 *va_arg(ap, long double *) = (long double) ld_val; 268 else 269 if (flags & FL_LONG) 270 *va_arg(ap, double *) = (double) ld_val; 271 else 272 *va_arg(ap, float *) = (float) ld_val; 273 } 274 break; 275 #endif 276 } /* end switch */ 277 conv++; 278 if (!(flags & FL_NOASSIGN) && kind != 'n') done++; 279 format++; 280 } 281 return conv || (ic != EOF) ? done : EOF; 282 }
在上面的源代码中,值得注意的是第26行的getc宏,定义代码如下:
#define getc(p) (--(p)->_count >= 0 ? (int) (*(p)->_ptr++) : \ __fillbuf(p))
getc的调用形式:ch=getc(fp); 功能是从文件指针指向的文件读入一个字符,并把它作为函数值返回给int型变量ch。
第4行~第17行,定义一些后面需要用到的变量
第23行~第34行,跳过format格式串中的空格,并且跳过输入流中的空格
第37行~第42行,输入流stream与format格式串中的空白符(空白符可以是空格(space)、制表符(tab)和新行符(newline))保持一致
第44行~第52行,在format中的字符为'%'的前提下,stream中的字符也为'%',则继续
第54行~第57行,format当前字符为'*',表示读指定类型的数据但不保存
第58行~第62行,指定说明最大域宽。 在百分号(%)与格式码之间的整数用于从对应域读入的最大字符数于宽度
第行~第282行,switch语句,用于格式修饰符,这些修饰符包括: h、l、L、c、p、b、d、i、o、u……,还有基于扫描集的'['修饰符
对scanf函数的源码分析,需要在scanf函数的语法格式详细的理解基础上进行,由于scanf函数实现十分复杂,需要仔细的品味,这里只是比较初步的分析,具体还有待后期不断的完善