Printf这个函数让大家又爱又恨,第一次接触c语言编程,基本都是调用printf打印“Hellow World!”,但当真正深入使用编程后,才发现printf并不是一个简单的函数。尤其是从事嵌入式软件工作的开发人员,会经常接触printf挂接驱动、printf重入的问题。
本文详细解释printf函数的工作原理,希望对大家有所帮助。
一、函数栈
分析printf之前首先了解函数的工作机制,程序运行前需要分配好内存空间,如图1所示(本文给出一个简图,实际编译器分配的会更加细致):
图1
代码、全局变量、常量内存位置固定,堆可以用于分配动态内存,而栈区则用于程序的运行。函数调用时将形参从右向左压入栈,等函数运行完成,通过出栈,将形参的存储空间释放。不同的编译器对函数入栈、出栈的内容会有所区别,但是对于c语言,形参的格式遵循_cdedl调用规则,有以下特点:
函数形参入栈顺序是从右向左
函数形参存储空间为连续存储,且参数按照固定字节对齐;编译器根据程序运行平台的字长进行对齐,32位字长平台按照4字节对齐,64位的会按照8字节对齐。
二、printf函数栈
printf 函数原型为int printf(const char *fmt, …),使用了可变参数的模式,我们通过图2例子来分析函数栈。
图2
fmt:“%d,%c,%c,%f\n”为常量字符串,存储在内存的常量字段,fmt为该字符串首地址;
可变形参1:与变量a类型和数值一致,为int类型;
可变形参2:与变量b类型和数值一致, 为char类型;
可变形参3:与变量c类型和数值一致,为char类型;
可变形参4:与变量d类型和数值一致,float的可变形参会被编译器强制转换为double类型;
假设该代码运行在32位字长的平台,且栈底->栈顶为“高地址->低地址”,函数栈中所有参数的存储地址按照4字节对齐存储,设fmt存储地址为0x30000000;则其函数栈如下图:
图3
三、printf代码解析
printf代码框架
printf代码及注释如下所示:
注:本例为32位平台,所以参数出入栈地址均为4字节对齐。
#ifndef _VALIST#define _VALISTtypedef char *va_list;#endif/* _VALIST */typedef int acpi_native_int;#define_AUPBND(sizeof (acpi_native_int) - 1) // 入栈4字节对齐#define_ADNBND(sizeof (acpi_native_int) - 1) // 出栈4字节对齐#define _bnd(X, bnd)(((sizeof (X)) + (bnd)) & (~(bnd)))// 4字节对齐#define va_arg(ap, T) (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND)))) // 按照4字节对齐取下一个可变参数,并且更新参数指针#define va_end(ap)(void) 0 // 与va_start成对,避免有些编译器告警#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND)))) // 第一个可变形参指针#endif/* va_arg */static char sprint_buf[2408];int printf(const char *fmt, ...){va_list args;int n;// 第一个可变形参指针va_start(args, fmt);// 根据字符串fmt,将对应形参转换为字符串,并组合成新的字符串存储在sprint_buf[]缓存中,返回字符个数。n = vsprintf(sprint_buf, fmt, args);//c标准要求在同一个函数中va_start 和va_end 要配对的出现。va_end(args);// 调用相关驱动接口,将将sprintf_buf中的内容输出n个字节到设备,// 此处可以是串口、控制台、Telnet等,在嵌入式开发中可以灵活挂接if (console_ops.write)console_ops.write(sprint_buf, n);return n;}
vsprintf解析模式详解
vsprintf采用%[flags][width][.prec][length][type]模式对各个参数进行解析各标志解析如下表:
标志(flags)
标志(flags)用于规定输出样式,含义如下:
flags(标志) | 字符名称 | 描述 |
– | 减号 | 在给定的字段宽度内左对齐,右边填充空格(默认右对齐) |
+ | 加号 | 强制在结果之前显示加号或减号(+ 或 -),即正数前面会显示 + 号;默认情况下,只有负数前面会显示一个 – 号 |
(空格) | 空格 | 输出值为正时加上空格,为负时加上负号 |
# | 井号 | specifier 是 o、x、X 时,增加前缀 0、0x、0X; specifier 是 e、E、f、g、G 时,一定使用小数点; specifier 是 g、G 时,尾部的 0 保留 |
0 | 数字零 | 对于所有的数字格式,使用前导零填充字段宽度(如果出现了减号标志或者指定了精度,则忽略该标志) |
最小宽度(width)
最小宽度(width)用于控制显示字段的宽度,即打印输出的总宽度,取值和含义如下:
width(最小宽度) | 字符名称 | 描述 |
digit(n) | 数字 | 字段宽度的最小值,如果输出的字段长度小于该数,结果会用前导空格填充;如果输出的字段长度大于该数,结果使用更宽的字段,不会截断输出 |
* | 星号 | 宽度在 format 字符串中规定位置未指定,使用星号标识附加参数,指示下一个参数是width |
精度(.prec)
精度(.precision)用于指定输出精度,即输出数据占用的宽度,取值和含义如下:
.pre(精度) | 字符名称 | 描述 |
.digit(n) | 点+数字 | 对于整数说明符(d、i、o、u、x、X):precision 指定了要打印的数字的最小位数。如果写入的值短于该数,结果会用前导零来填充。如果写入的值长于该数,结果不会被截断。精度为 0 意味着不写入任何字符; 对于 e、E 和 f 说明符:要在小数点后输出的小数位数; 对于 g 和 G 说明符:要输出的最大有效位数; 对于 s 说明符:要输出的最大字符数。默认情况下,所有字符都会被输出,直到遇到末尾的空字符; 对于 c 说明符:没有任何影响; 当未指定任何精度时,默认为 1。如果指定时只使用点而不带有一个显式值,则标识其后跟随一个 0。 |
.* | 点+星号 | 精度在 format 字符串中规定位置未指定,使用点+星号标识附加参数,指示下一个参数是精度 |
类型长度(length)
类型长度(length)用于控制待输出数据的数据类型长度,取值和含义如下:
length(类型长度) | 描述 |
h | 参数被解释为短整型或无符号短整型(仅适用于整数说明符:i、d、o、u、x 和 X) |
l | 参数被解释为长整型或无符号长整型,适用于整数说明符(i、d、o、u、x 和 X)及说明符 c(表示一个宽字符)和 s(表示宽字符字符串) |
ll | 参数被解释为超长整型或无符号超长长整型,适用于整数说明符(i、d、o、u、x 和 X)及说明符 c(表示一个宽字符)和 s(表示宽字符字符串) |
说明符(type)
说明符(type)用于规定输出数据的类型,含义如下:
说明符(specifier) | 对应数据类型 | 描述 |
d / i | int | 输出类型为有符号的十进制整数,i 是老式写法 |
o | unsigned int | 输出类型为无符号八进制整数(没有前导 0) |
u | unsigned int | 输出类型为无符号十进制整数 |
x / X | unsigned int | 输出类型为无符号十六进制整数,x 对应的是 abcdef,X 对应的是 ABCDEF(没有前导 0x 或者 0X) |
f / lf | double | 输出类型为十进制表示的浮点数,默认精度为6(lf 在 C99 开始加入标准,意思和 f 相同) |
e / E | double | 输出类型为科学计数法表示的数,此处 “e” 的大小写代表在输出时用的 “e” 的大小写,默认浮点数精度为6 |
g | double | 根据数值不同自动选择 %f 或 %e,%e 格式在指数小于-4或指数大于等于精度时用使用 [1] |
G | double | 根据数值不同自动选择 %f 或 %E,%E 格式在指数小于-4或指数大于等于精度时用使用 |
c | char | 输出类型为字符型。可以把输入的数字按照ASCII码相应转换为对应的字符 |
s | char * | 输出类型为字符串。输出字符串中的字符直至遇到字符串中的空字符(字符串以 ‘\0‘ 结尾,这个 ‘\0’ 即空字符)或者已打印了由精度指定的字符数 |
p | void * | 以16进制形式输出指针 |
q | long long | 输出类型为长整型有符号的十进制整数 |
% | 不转换参数 | 不进行转换,输出字符‘%’(百分号)本身 |
n | int * | 到此字符之前为止,一共输出的字符个数,不输出文本 [4] |
转义字符
转义序列在字符串中会被自动转换为相应的特殊字符。printf() 使用的常见转义字符如下:
转义序列 | 描述 | ASCII 编码 |
\’ | 单引号 | 0x27 |
\” | 双引号 | 0x22 |
\? | 问号 | 0x3f |
\\ | 反斜杠 | 0x5c |
\a | 铃声(提醒) | 0x07 |
\b | 退格 | 0x08 |
\f | 换页 | 0x0c |
\n | 换行 | 0x0a |
\r | 回车 | 0x0d |
\t | 水平制表符 | 0x09 |
\v | 垂直制表符 | 0x0b |
常见组合及输出结果
int main(void){int a = 10, b = 3;printf("%*.*d\n",a,b, -100); // 输出数字,右对齐,宽度从变量获取printf("%10.3d\n", -100);// 输出数字,右对齐printf("%-10.3d\n", -100); // 输出数字,左对齐printf("%+10.3d\n", 100);// 输出数字,正数带正号printf("%0.13d\n", 100); // 输出文本格式,如员工号printf("%#.8x\n", 0x30ff); // 输出8位16进制地址,小写字母printf("%#.8X\n", 0x30ff); // 输出8位16进制地址,大写字母printf("%.3f\n", 3.14159267892); // 保留浮点小数点后有效位数printf("%llu\n", 0xffffffffffffffff);// 输出64位长整型}
输出结果:
vsprintf流程图
vsprintf源码及注释
#define ZEROPAD1 /* 无数据位用0填充 */#define SIGN 2 /* 符号位 */#define PLUS 4 /* 符号位正数显示正号 */#define SPACE8 /* 符号位非负数显示空格 */#define LEFT16 /* 左对齐 */#define SPECIAL 32 /* 显示其他进制的前缀,比如16进制添加前缀0x */#define LARGE 64 /* 使用大写母 */#define is_digit(c) ((c) >= '0' && (c) <= '9')//浮点字符串缓存#define SZ_NUM_BUF32static char sprint_fe[SZ_NUM_BUF+1];static const char *digits = "0123456789abcdefghijklmnopqrstuvwxyz";static const char *upper_digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";// 字符串长度static size_t strnlen(const char *s, size_t count){const char *sc;for (sc = s; *sc != '\0' && count--; ++sc);return sc - s;}// 字符转10进制数static int skip_atoi(const char **s){int i = 0;while (is_digit(**s)) i = i*10 + *((*s)++) - '0';return i;}// static char *number(char *str, long num, int base, int size, int precision, int type){char c, sign, tmp[66];const char *dig = digits;int i;// if (type & LARGE)dig = upper_digits;// 左对齐,去掉补0if (type & LEFT) type &= ~ZEROPAD;// if (base 36) return 0;// 补0或补空格c = (type & ZEROPAD) " />= SZ_NUM_BUF) er = "OV";/* 最大缓存 */}else{/* E类型 x.xxxxxxe+xx*/if (val != 0){val += i10x(ilog10(val) - prec) / 2;/* 用于四舍五入,prec表示底数部分的小数位数*/e = ilog10(val);/*整数位数*/if (e > 99 || prec + 7 >= SZ_NUM_BUF)//指数范围,及最大缓存,{er = "OV";}else{if (e = -prec);if (fmt != 'f'){*buf++ = (char)fmt;if (e < 0) {e = 0 - e; *buf++ = '-';}else {*buf++ = '+';}*buf++ = (char)('0' + e / 10);*buf++ = (char)('0' + e % 10);}}}// 特殊值if (er){// 符号if (sign)*buf++ = sign;// 数据字符串do{*buf++ = *er++;} while (*er);}*buf = 0;/* 结束符 */}int vsprintf(char *buf, const char *fmt, va_list args){int len;unsigned long long num;int i, base;char * str;const char *s;/*s所指向的内存单元不可改写,但是s可以改写*/int flags;/* flags to number() */int field_width;/* width of output field */int precision;/* min. # of digits for integers; max number of chars for from string */int qualifier;/* 'h', 'l', or 'L' for integer fields *//* 'z' support added 23/7/1999 S.H.*//* 'z' changed to 'Z' --davidm 1/25/99 */for (str=buf ; *fmt ; ++fmt){if (*fmt != '%') /*使指针指向格式控制符'%,以方便以后处理flags'*/{*str++ = *fmt;continue;}/* flags */flags = 0;repeat:++fmt;switch (*fmt){case '-': flags |= LEFT; goto repeat;/*左对齐-left justify*/case '+': flags |= PLUS; goto repeat;/*p plus with ’+‘*/case ' ': flags |= SPACE; goto repeat;/*p with space*/case '#': flags |= SPECIAL; goto repeat;/*根据其后的转义字符的不同而有不同含义*/case '0': flags |= ZEROPAD; goto repeat;/*当有指定参数时,无数字的参数将补上0*/} field_width = -1;if ('0' <= *fmt && *fmt <= '9')field_width = skip_atoi(&fmt);else if (*fmt == '*'){++fmt;/*skip '*' *//* it's the next argument */field_width = va_arg(args, int);// 下个参数变量表述位宽if (field_width < 0) {field_width = -field_width;flags |= LEFT;}}/* get the precision-----即是处理.pre 有效位 */precision = -1;if (*fmt == '.'){++fmt;if ('0' <= *fmt && *fmt <= '9')precision = skip_atoi(&fmt);else if (*fmt == '*') /*如果精度域中是字符'*',表示下一个参数指定精度。因此调用va_arg 取精度值。若此时宽度值小于0,则将字段精度值取为0。*/{++fmt;/* it's the next argument */precision = va_arg(args, int);}if (precision 0) *str++ = ' ';/*非左对齐,左侧填充空格-End*/*str++ = (unsigned char) va_arg(args, int);/*左对齐,右侧填充空格-Satrt*/while (--field_width > 0)*str++ = ' ';/*左对齐,右侧填充空格-End*///continue在此处是跳出本次for循环continue;case 's':s = va_arg(args, char *);if (!s)s = "";len = strnlen(s, precision);/*取字符串的长度,最大为precision*//*非左对齐,左侧填充空格-Satrt*/if (!(flags & LEFT))while (len < field_width--)*str++ = ' ';/*非左对齐,左侧填充空格-End*/for (i = 0; i < len; ++i)*str++ = *s++;/*左对齐,右侧填充空格-Satrt*/while (len < field_width--)*str++ = ' ';/*左对齐,右侧填充空格-End*/continue;case 'p':/*没有设置宽度域,则默认宽度为指针变量长度,32位系统默认为8,且需要添0处理*/if (field_width == -1){field_width = 2*sizeof(void *);flags |= ZEROPAD;}str = number(str,(unsigned long) va_arg(args, void *), 16,field_width, precision, flags);continue;// 形参作为指针变量,向指针变量所指向的地址写入当前转换的字符长度case 'n':// ln长整型地址if (qualifier == 'l'){long * ip = va_arg(args, long *);*ip = (str - buf);}// zn 字节地址else if (qualifier == 'Z'){size_t * ip = va_arg(args, size_t *);*ip = (str - buf);}// n 整形地址else{int * ip = va_arg(args, int *);*ip = (str - buf);}continue;// %f %e %E %lf均使用double类型case 'f':/* Floating point (decimal) */case 'e':/* Floating point (e) */case 'E':/* Floating point (E) */// double数据转字符串ftoa(sprint_fe, va_arg(args, double), precision, *fmt);/* 浮点转字符串*/// 右对齐 左侧补充空格if (!(flags&LEFT)){for (j = strnlen(sprint_fe, SZ_NUM_BUF); j<field_width; j++)*str++= '/0';}// 数据主体i = 0;while(sprint_fe[i]) *str++ = sprint_fe[i++];/* 主体 */// 左对齐 右侧补充空格while (j++ < field_width) *str++ = '/0'; continue;// %%表示%case '%':*str++ = '%';continue;/* 设置进制*/case 'o':base = 8;break;/*大写*/case 'X':flags |= LARGE;case 'x':base = 16;break;/*有符号类型*/case 'd':case 'i':flags |= SIGN;/*无符号类型*/case 'u':break;default:/*非参数打印*/*str++ = '%';if (*fmt)*str++ = *fmt;else--fmt;continue;}/*同时如果flags有符号位的话,将参数转变成有符号的数*/if (qualifier == 'l'){num = va_arg(args, unsigned long);if (flags & SIGN)num = (signed long) num;}else if (qualifier == 'q'){num = va_arg(args, unsigned long long);if (flags & SIGN)num = (signed long long) num;}else if (qualifier == 'Z'){num = va_arg(args, size_t);}else if (qualifier == 'h'){num = (unsigned short) va_arg(args, int);if (flags & SIGN)num = (signed short) num;}else{num = va_arg(args, unsigned int);if (flags & SIGN)num = (signed int) num;}str = number(str, num, base, field_width, precision, flags);}*str = '/0';/*最后在转换好的字符串上加上NULL*/return str-buf;/*返回转换好的字符串的长度值*/}
四、printf不可重入
Printf函数是不可重入的,因为vsprintf函数调用了全局变量sprint_buf[],但是并且没有做任何边界保护,如果在多线程、或者中断程序中运行该函数,就难以避免资源重复抢占的问题。比如,线程A和线程B均调用了printf打印,线程A正在运行vsprintf函数,对sprint_buf正在操作中,此时被高优先级线程B抢占,B获取了sprint_buf的操作权,线程A生的数据就会被覆盖掉。
五、解决不可重入的方法
在嵌入式软件开发中经常会使用LogMsg打印,LogMsg可以自己编写,也可以借用操作系统的自带的,其根本的思想就是不同线程使用不同的sprint_buf缓存空间。主要方法有独立线程使用静态消息队列、申请动态内存、使用多组缓存空间等。