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 _VALIST
typedef 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)