printf 函数的实现原理

  1 /*
  2 * =====================================================================================
  3 *
  4 *       Filename:  printf.c
  5 *
  6 *    Description:  printf 函数的实现
  7 *
  8 *        Version:  1.0
  9 *        Created:  2010年12月12日 14时48分18秒
 10 *       Revision:  none
 11 *       Compiler:  gcc
 12 *
 13 *         Author:  Yang Shao Kun (), [email protected]
 14 *        Company:  College of Information Engineering of CDUT
 15 *
 16 * =====================================================================================
 17 */
 18 要了解变参函数的实现,首先我们的弄清楚几个问题:
 19 1:该函数有几个参数。
 20 2:该函数增样去访问这些参数。
 21 3:在访问完成后,如何从堆栈中释放这些参数。
 22 对于c语言,它的调用规则遵循_cdedl调用规则。
 23 在_cdedl规则中:1.参数从右到左依次入栈
 24                 2.调用者负责清理堆栈
 25                 3.参数的数量类型不会导致编译阶段的错误
 26 要弄清楚变参函数的原理,我们需要解决上述的3个问题,其中的第三个问题,根据调
 27 用原则,那我们现在可以不管。
 28 要处理变参函数,需要用到 va_list 类型,和 va_start,va_end,va_arg 宏定义。我
 29 看网上的许多资料说这些参数都是定义在stdarg.h这个头文件中,但是在我的linux机
 30 器上,我的版本是fedorea 14,用vim访问的时候,确是在 acenv.h这个头文件中,估
 31 计是内核的版本不一样的原因吧!!!
 32 上面的这几个宏和其中的类型,在内核中是这样来实现的:
 33 #ifndef _VALIST
 34 #define _VALIST
 35 typedef char *va_list;
 36 #endif                /* _VALIST */
 37 /*
 38 * Storage alignment properties
 39 */
 40 #define  _AUPBND                (sizeof (acpi_native_int) - 1)
 41 #define  _ADNBND                (sizeof (acpi_native_int) - 1)
 42 /*
 43 * Variable argument list macro definitions
 44 */
 45 #define _bnd(X, bnd)            (((sizeof (X)) + (bnd)) & (~(bnd)))
 46 #define va_arg(ap, T)           (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
 47 #define va_end(ap)              (void) 0
 48 #define va_start(ap, A)         (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))
 49 #endif                /* va_arg */
 50 首先来看 va_list 类型,其实这是一个字符指针。
 51 va_start,是使ap指针指向变参函数中的下一个参数。
 52 我们现在来看_bnd 宏的实现:
 53 首先:
 54 typedef s32 acpi_native_int;
 55 typedef int            s32;
 56 看出来,acpi_native_int 其实就是 int 类型,那么,
 57 #define  _AUPBND                (sizeof (acpi_native_int) - 1)
 58 #define  _ADNBND                (sizeof (acpi_native_int) - 1)
 59 这两个值就应该是相等的,都-等于:3==0x00000003,按位取反后的结果就是:0xfffff
 60 ffc,因此,
 61 _bnd(x,bnd)宏在32位机下就是
 62 (((sizeof (X)) + (3)) & (0xfffffffc)),那么作用就很明显是取4的整数,就相当与
 63 整数除法后取ceiling--向上取整。
 64 回过头来看 va_start(ap,A),初始化参数指针ap,将函数参数A右边右边第一个参数地
 65 址赋值给ap,A必须是一个参数的指针,所以,此种类型函数至少要有一个普通的参数
 66 ,从而提供给va_start ,这样va_start才能找到可变参数在栈上的位置。
 67 va_arg(ap,T),获得ap指向参数的值,同时使ap指向下一个参数,T用来指名当前参数类
 68 型。
 69 va_end 在有些简单的实现中不起任何作用,在有些实现中可能会把ap改成无效值,这
 70 里,是把ap指针指向了 NULL。
 71 c标准要求在同一个函数中va_start 和va_end 要配对的出现。
 72 那么到现在,处理多参数函数的步骤就是
 73 1:首先是要保证该函数至少有一个参数,同时用...参数申明函数是变参函数。
 74 2:在函数内部以va_start(ap,A)宏初始化参数指针。
 75 3:用va_arg(ap,T)从左到右逐个取参数值。
 76 printf()格式转换的一般形式如下:
 77 %[flags][width][.prec][type]
 78 prec有一下几种情况:
 79                     正整数的最小位数
 80                     在浮点数中表示的小数位数
 81                     %g格式表示有效为的最大值
 82                     %s格式表示字符串的最大长度
 83                     若为*符号表示下个参数值为最大长度
 84 width:为输出的最小长度,如果这个输出参数并非数值,而是*符号,则表示以下一个参数当做输出长度。
 85 现在来看看我们的printf函数的实现,在内核中printf函数被封装成下面的代码:
 86 static char sprint_buf[1024];
 87 int printf(const char *fmt, ...)
 88 {
 89     va_list args;
 90     int n;
 91     va_start(args, fmt);//初始化参数指针
 92     n = vsprintf(sprint_buf, fmt, args);/*函数放回已经处理的字符串长度*/
 93     va_end(args);//与va_start 配对出现,处理ap指针
 94     if (console_ops.write)
 95         console_ops.write(sprint_buf, n);/*调用控制台的结构中的write函数,将sprintf_buf中的内容输出n个字节到设备*/
 96     return n;
 97 }
 98 vs_printf函数的实现代码是:
 99 int vsprintf(char *buf, const char *fmt, va_list args)
100 {
101     int len;
102     unsigned long long num;
103     int i, base;
104     char * str;
105     const char *s;/*s所指向的内存单元不可改写,但是s可以改写*/
106     int flags;        /* flags to number() */
107     int field_width;    /* width of output field */
108     int precision;        /* min. # of digits for integers; max
109                    number of chars for from string */
110     int qualifier;        /* ‘h‘, ‘l‘, or ‘L‘ for integer fields */
111                             /* ‘z‘ support added 23/7/1999 S.H.    */
112                 /* ‘z‘ changed to ‘Z‘ --davidm 1/25/99 */
113     for (str=buf ; *fmt ; ++fmt)
114     {
115         if (*fmt != ‘%‘) /*使指针指向格式控制符‘%,以方便以后处理flags‘*/
116         {
117             *str++ = *fmt;
118             continue;
119         }
120         /* process flags */
121         flags = 0;
122         repeat:
123             ++fmt;        /* this also skips first ‘%‘--跳过格式控制符‘%‘ */
124             switch (*fmt)
125             {
126                 case ‘-‘: flags |= LEFT; goto repeat;/*左对齐-left justify*/
127                 case ‘+‘: flags |= PLUS; goto repeat;/*p plus with ’+‘*/
128                 case ‘ ‘: flags |= SPACE; goto repeat;/*p with space*/
129                 case ‘#‘: flags |= SPECIAL; goto repeat;/*根据其后的转义字符的不同而有不同含义*/
130                 case ‘0‘: flags |= ZEROPAD; goto repeat;/*当有指定参数时,无数字的参数将补上0*/
131             }
132 //#define ZEROPAD    1        /* pad with zero */
133 //#define SIGN    2        /* unsigned/signed long */
134 //#define PLUS    4        /* show plus */
135 //#define SPACE    8        /* space if plus */
136 //#define LEFT    16        /* left justified */
137 //#define SPECIAL    32        /* 0x */
138 //#define LARGE    64        /* use ‘ABCDEF‘ instead of ‘abcdef‘ */
139         /* get field width ----deal 域宽 取当前参数字段宽度域值,放入field_width 变量中。如果宽度域中是数值则直接取其为宽度值。 如果宽度域中是字符‘*‘,表示下一个参数指定宽度。因此调用va_arg 取宽度值。若此时宽度值小于0,则该负数表示其带有标志域‘-‘标志(左靠齐),因此还需在标志变量中添入该标志,并将字段宽度值取为其绝对值。  */
140         field_width = -1;
141         if (‘0‘ <= *fmt && *fmt <= ‘9‘)
142             field_width = skip_atoi(&fmt);
143         else if (*fmt == ‘*‘)
144         {
145             ++fmt;/*skip ‘*‘ */
146             /* it‘s the next argument */
147             field_width = va_arg(args, int);
148             if (field_width < 0) {
149                 field_width = -field_width;
150                 flags |= LEFT;
151             }
152         }
153         /* get the precision-----即是处理.pre 有效位 */
154         precision = -1;
155         if (*fmt == ‘.‘)
156         {
157             ++fmt;
158             if (‘0‘ <= *fmt && *fmt <= ‘9‘)
159                 precision = skip_atoi(&fmt);
160             else if (*fmt == ‘*‘) /*如果精度域中是字符‘*‘,表示下一个参数指定精度。因此调用va_arg 取精度值。若此时宽度值小于0,则将字段精度值取为0。*/
161             {
162                 ++fmt;
163                 /* it‘s the next argument */
164                 precision = va_arg(args, int);
165             }
166             if (precision < 0)
167                 precision = 0;
168         }
169         /* get the conversion qualifier 分析长度修饰符,并将其存入qualifer 变量*/
170         qualifier = -1;
171         if (*fmt == ‘l‘ && *(fmt + 1) == ‘l‘)
172         {
173             qualifier = ‘q‘;
174             fmt += 2;
175         }
176         else if (*fmt == ‘h‘ || *fmt == ‘l‘ || *fmt == ‘L‘|| *fmt == ‘Z‘)
177         {
178             qualifier = *fmt;
179             ++fmt;
180         }
181         /* default base */
182         base = 10;
183         /*处理type部分*/
184         switch (*fmt)
185         {
186             case ‘c‘:
187                 if (!(flags & LEFT))/*没有左对齐标志,那么填充field_width-1个空格*/
188                     while (--field_width > 0)
189                     *str++ = ‘ ‘;
190                     *str++ = (unsigned char) va_arg(args, int);
191                     while (--field_width > 0)/*不是左对齐*/
192                     *str++ = ‘ ‘;/*在参数后输出field_width-1个空格*/
193                     continue;
194             /*如果转换参数是s,则,表示对应的参数是字符串,首先取参数字符串的长度,如果超过了精度域值,则取精度域值为最大长度*/
195             case ‘s‘:
196                 s = va_arg(args, char *);
197                 if (!s)
198                     s = "";
199                     len = strnlen(s, precision);/*字符串的长度,最大为precision*/
200                     if (!(flags & LEFT))
201                     while (len < field_width--)/*如果不是左对齐,则左侧补空格=field_width-len个空格*/
202                     *str++ = ‘ ‘;
203                     for (i = 0; i < len; ++i)
204                     *str++ = *s++;
205                     while (len < field_width--)/*如果是左对齐,则右侧补空格数=field_width-len*/
206                     *str++ = ‘ ‘;
207                     continue;
208 /*如果格式转换符是‘p‘,表示对应参数的一个指针类型。此时若该参数没有设置宽度域,则默认宽度为8,并且需要添零。然后调用number()*/
209             case ‘p‘:
210                 if (field_width == -1)
211                 {
212                     field_width = 2*sizeof(void *);
213                     flags |= ZEROPAD;
214                 }
215                 str = number(str,(unsigned long) va_arg(args, void *), 16,
216                         field_width, precision, flags);
217                 continue;
218         // 若格式转换指示符是‘n‘,则表示要把到目前为止转换输出的字符数保存到对应参数指针指定的位置中。
219         // 首先利用va_arg()得该参数指针,然后将已经转换好的字符数存入该指针所指的位置
220             case ‘n‘:
221             if (qualifier == ‘l‘)
222             {
223                 long * ip = va_arg(args, long *);
224                 *ip = (str - buf);
225             }
226             else if (qualifier == ‘Z‘)
227             {
228                 size_t * ip = va_arg(args, size_t *);
229                 *ip = (str - buf);
230             }
231             else
232             {
233                 int * ip = va_arg(args, int *);
234                 *ip = (str - buf);
235             }
236             continue;
237     //若格式转换符不是‘%‘,则表示格式字符串有错,直接将一个‘%‘写入输出串中。
238     // 如果格式转换符的位置处还有字符,则也直接将该字符写入输出串中,并返回到继续处理
239     //格式字符串。
240             case ‘%‘:
241             *str++ = ‘%‘;
242             continue;
243             /* integer number formats - set up the flags and "break" */
244             case ‘o‘:
245             base = 8;
246             break;
247             case ‘X‘:
248             flags |= LARGE;
249             case ‘x‘:
250             base = 16;
251             break;
252 // 如果格式转换字符是‘d‘,‘i‘或‘u‘,则表示对应参数是整数,‘d‘, ‘i‘代表符号整数,因此需要加上
253 // 带符号标志。‘u‘代表无符号整数
254             case ‘d‘:
255             case ‘i‘:
256             flags |= SIGN;
257             case ‘u‘:
258             break;
259             default:
260             *str++ = ‘%‘;
261             if (*fmt)
262                 *str++ = *fmt;
263             else
264                 --fmt;
265             continue;
266         }
267         /*处理字符的修饰符,同时如果flags有符号位的话,将参数转变成有符号的数*/
268         if (qualifier == ‘l‘)
269         {
270             num = va_arg(args, unsigned long);
271             if (flags & SIGN)
272                 num = (signed long) num;
273         }
274         else if (qualifier == ‘q‘)
275         {
276             num = va_arg(args, unsigned long long);
277             if (flags & SIGN)
278                 num = (signed long long) num;
279         }
280         else if (qualifier == ‘Z‘)
281         {
282             num = va_arg(args, size_t);
283         }
284         else if (qualifier == ‘h‘)
285         {
286             num = (unsigned short) va_arg(args, int);
287             if (flags & SIGN)
288                 num = (signed short) num;
289         }
290         else
291         {
292             num = va_arg(args, unsigned int);
293             if (flags & SIGN)
294                 num = (signed int) num;
295         }
296         str = number(str, num, base, field_width, precision, flags);
297     }
298     *str = ‘/0‘;/*最后在转换好的字符串上加上NULL*/
299     return str-buf;/*返回转换好的字符串的长度值*/
300 }

参看该资料:

C中的可变参数研究

一. 何谓可变参数
int printf( const char* format, ...); 
这是使用过C语言的人所再熟悉不过的printf函数原型,它的参数中就有固定参数format和可变参数(用”…”表示). 而我们又可以用各种方式来调用printf,如:
printf("%d",value); 
printf("%s",str); 
printf("the number is %d ,string is:%s", value, str);

二.实现原理
C语言用宏来处理这些可变参数。这些宏看起来很复杂,其实原理挺简单,就是根据参数入栈的特点从最靠近第一个可变参数的固定参数开始,依次获取每个可变参数的地址。下面我们来分析这些宏。在VC中的stdarg.h头文件中,针对不同平台有不同的宏定义,我们选取X86平台下的宏定义:

typedef char *va_list; 
/*把va_list被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。而在有的机器上va_list是被定义成void*的*/
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
/*_INTSIZEOF(n)宏是为了考虑那些内存地址需要对齐的系统,从宏的名字来应该是跟sizeof(int)对齐。一般的sizeof(int)=4,也就是参数在内存中的地址都为4的倍数。比如,如果sizeof(n)在1-4之间,那么_INTSIZEOF(n)=4;如果sizeof(n)在5-8之间,那么_INTSIZEOF(n)=8。*/
#define va_start(ap,v)( ap = (va_list)&v + _INTSIZEOF(v) )
/*va_start的定义为 &v+_INTSIZEOF(v) ,这里&v是最后一个固定参数的起始地址,再加上其实际占用大小后,就得到了第一个可变参数的起始内存地址。所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在的内存地址*/
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
/*这个宏做了两个事情,
①用用户输入的类型名对参数地址进行强制类型转换,得到用户所需要的值
②计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。*/
  #define va_end(ap) ( ap = (va_list)0 ) 
/*x86平台定义为ap=(char*)0;使ap不再 指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的. 在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型. */

以下再用图来表示:

在VC等绝大多数C编译器中,默认情况下,参数进栈的顺序是由右向左的,因此,参数进栈以后的内存模型如下图所示:最后一个固定参数的地址位于第一个可变参数之下,并且是连续存储的。
|——————————————————————————|
|最后一个可变参数 | ->高内存地址处
|——————————————————————————|
...................
|——————————————————————————|
|第N个可变参数 | ->va_arg(arg_ptr,int)后arg_ptr所指的地方,
| | 即第N个可变参数的地址。
|——————————————— | 
………………………….
|——————————————————————————|
|第一个可变参数 | ->va_start(arg_ptr,start)后arg_ptr所指的地方
| | 即第一个可变参数的地址
|——————————————— | 
|———————————————————————— ——|
| |
|最后一个固定参数 | -> start的起始地址
|—————————————— —| .................
|—————————————————————————— |
| |
|——————————————— |-> 低内存地址处

三.printf研究

下面是一个简单的printf函数的实现,参考了中的156页的例子,读者可以结合书上的代码与本文参照。
#include "stdio.h"
#include "stdlib.h"
void myprintf(char* fmt, ...) //一个简单的类似于printf的实现,//参数必须都是int 类型

char* pArg=NULL; //等价于原来的va_list 
char c;

pArg = (char*) &fmt; //注意不要写成p = fmt !!因为这里要对//参数取址,而不是取值
pArg += sizeof(fmt); //等价于原来的va_start

do
{
c =*fmt;
if (c != ‘%‘)
{
putchar(c); //照原样输出字符
}
else
{
//按格式字符输出数据
switch(*++fmt) 
{
case ‘d‘:
printf("%d",*((int*)pArg)); 
break;
case ‘x‘:
printf("%#x",*((int*)pArg));
break;
default:
break;

pArg += sizeof(int); //等价于原来的va_arg
}
++fmt;
}while (*fmt != ‘\0‘); 
pArg = NULL; //等价于va_end
return; 
}

int main(int argc, char* argv[])
{
int i = 1234;
int j = 5678;

myprintf("the first test:i=%d",i,j); 
myprintf("the secend test:i=%d; %x;j=%d;",i,0xabcd,j); 
system("pause");
return 0;
}

在intel+win2k+vc6的机器执行结果如下:
the first test:i=1234
the secend test:i=1234; 0xabcd;j=5678;

四.应用
求最大值:
#include //不定数目参数需要的宏
int max(int n,int num,...)
{
va_list x;//说明变量x
va_start(x,num);//x被初始化为指向num后的第一个参数
int m=num;
for(int i=1;i {
//将变量x所指向的int类型的值赋给y,同时使x指向下一个参数
int y=va_arg(x,int);
if(y>m)m=y;
}
va_end(x);//清除变量x
return m;
}

int main()
{
printf("%d,%d",max(3,5,56),max(6,0,4,32,45,533));
return 0;
}

时间: 2024-10-14 16:07:29

printf 函数的实现原理的相关文章

C语言printf()函数具体解释和安全隐患

一.问题描写叙述 二.进一步说明 请细致注意看,有例如以下奇怪的现象 int a=5; floatx=a; //这里转换是没有问题的.%f打印x是 5.000000 printf("%d\n",a); printf("%f\n",a); //输出为什么是0.000000? -----问题1 printf("%f\n",x); printf("%d\n",x); //输出为什么是0? -----问题2 printf("

C语言printf()函数详解和安全隐患

一.问题描述 二.进一步说明 请仔细注意看,有如下奇怪的现象 int a=5; floatx=a; //这里转换是没有问题的,%f打印x是 5.000000 printf("%d\n",a); printf("%f\n",a); //输出为什么是0.000000? -----问题1 printf("%f\n",x); printf("%d\n",x); //输出为什么是0? -----问题2 printf("%f,%

几个常见字符串处理函数的实现原理

字符串是一种常见的数据结构,对字符串的处理又可以十分灵活,所以在实际开发,尤其是非数值处理中,字符串的应用非常广泛.虽然很多字符串操作都封装在了函数库里,应用程序可以直接通过调用库函数来实现字符串处理,然而对于开发者而言,若能了解其底层实现原理,对于应用编程而言还是大有裨益的. 这篇文章主要介绍几种常用的字符串处理函数的实现原理. 一.strlen函数 strlen函数:计算字符串的实际长度,不包括'\0'. 算法:从第一个字符开始扫描,直到遇见第一个'\0',停止扫描,返回字符串长度. 代码如

【C语言】浅谈可变参数与printf函数

一.何谓可变参数 int printf( const char* format, ...); 这是使用过C语言的人所再熟悉不过的printf函数原型,它的参数中就有固定参数format和可变参数(用"-"表示). 而我们又可以用各种方式来调用printf,如: printf( "%d ",value); printf( "%s ",str); printf( "the number is %d ,string is:%s ",

C语言笔记之printf()函数

先看例子: printf("This line has only controlling strings.\n"); `printf("I am %d years old and %f inch tall", 21, 132); 参数 printf()的参数分为两大部分,分别是控制字符串和参数列表,二者用逗号隔开,而且参数之间也要用逗号隔开.其中控制字符串是字符串常量,如果不需要其他参数的话,双引号里面的文字将被原原本本的打印到屏幕.通俗的讲,双引号里面是什么,输出

rand函数的实现原理

rand函数的实现原理 rand函数产生的是伪随机数,也就是说它不是一个真实的随机数. 那么伪随机数是怎么实现的呢?原理大概如下: 如果约定:a1=f(seed),an+1=f(an)那你可以行到一个序列:a1,a2,a3...an,那么要制作一个伪随机函数rand,只需要让它每调用一次就返回序列的下一个元素就行. 就相当于第1次调用rand返回a1,第2次返回a2,-,第n次返回an,这样每次调rand都能拿到一个不同的数,只要整个序列的规律不明显,整个函数看起来就是随机的. 现在计算机上的r

C利用可变参数列表统计一组数的平均值,利用函数形式参数栈原理实现指针运算

//描述:利用可变参数列表统计一组数的平均值 #include <stdarg.h> #include <stdio.h> float average(int num, ...);//函数原型:即声明 float average2(int num, ...);//num个数 void add(int num, int x, int y, int z); int main(void){ int a=10; int b=20; printf("a地址:%p b地址:%p\n&

[转组第7天] | 函数的工作原理

2018-05-04 <C++反汇编和逆向技术>第六章 函数的工作原理 读书笔记 debug版本的函数调用: call func func: push ebp ;保存ebp mov ebp,esp sub esp,40h ;抬高esp,开辟栈空间 push ... ;保存寄存器 ... pop ... ;还原寄存器 add esp,40h ;降低esp,释放局部变量空间 cmp ebp,esp ;检测栈平衡 call __chkesp ;进入栈平衡错误检测函数 mov esp,ebp ;还原e

printf函数重定向

printf函数底层会调用fputc函数 /*重定向c库函数printf到USART1*/ int fputc(int ch, FILE *f) { /*发送一个字节数据USART1 */ USART_SendData(DEBUG_USART, (uint8_t) ch); /* 等待发送完毕 */ while (USART_GetFlagStatus(DEBUG_USART, USART_FLAG_TXE) == RESET); return (ch); }