在单片机开发中,我们借助于vsprintf函数,可以自己实现一个printf函数,但是,那是IDE帮我们做了一些事情。
刚开始在ARM9裸机上自己写printf的实现的时候,包含对应头文件也会提示vsprintf函数找不到,查询很多资料之后,发现使用arm-linux-ld就是找不到对应的库函数,换成arm-linux-gcc 使用,
arm-linux-gcc -v -static -Wl,-Tsdram.lds,-Map,system.map -nostartfiles -o sdram.elf $^
这样之后,倒是可以找到vsprintf的定义了,可是编译之后的文件有400多k,下载进入开发板还是没能正常工作。后面放弃了这种方法,先自己实现了一个简易版本的printf函数,用来作为调试已经足够了,没有人会拿ARM9以上的芯片只跑裸机,等之后上linux操作系统之后,我们可以有很多调试方式,只是传统IDE开发,帮我们做了很多我们并不知道的事情。
现在,我们开始实现自己的简易版本printf函数。
要实现printf函数,首先不得不说的就是可变参数了。
printf函数原型:
int printf(const char *format, ...);
一个参数是一个const的char指针,作为格式标志,
另一个参数是可变参数,用3个点表示。
实现依据:
X86和我们s3c2440的堆栈增长方向,默认是一样的,都是从高地址向低地址增长,函数调用,是依靠于堆栈实现的,在我们的默认模式下,先入栈的参数,保存在高地址。
用代码来说明这个问题:
这个是要说明什么问题呢?
参数传递的时候,在栈生长方向是高地址往地址这种方式下。先入栈的,存放在高地址,通过上面的打印可以看出,最右边的参数明显先入栈,所以才会先打印b,再打印a,如果C语言基础比较好的,应该是知道原因的。为什么C语言选择参数从右往左入栈?先说结论,要是C语言不支持可变参数,那么从左到右和从右到左的的顺序都是可以的,但是为了满足可变参数的语法,那么C语言的参数入栈顺序只能是从右向左。
解释原因:
假设参数入栈按照从左到右方式入栈,当遇到可变参数的时候,
func(p1,p2,...)
p1先入栈,p2再入栈,然后是可变参数入栈,由于可变参数的数目不可确定,那么就无法动态确定偏移,也就是不能求得可变参数,可变参数是根据确定参数然后地址偏移得到的,如果从右往左的方式,那么最后被入栈的就是最左边的那个确定参数,通过这个确定参数,然后偏移就能得到可变参数,而且,无论可变参数数目多少,都不会影响后面调用func函数,因为在最左边的最后一个参数入栈之后,下面一个地址就将进行函数调用,如果是按照从左往右的方式,最左边的确定参数一开始就背入栈了,那么无法动态确定可变参数的个数,如何通过偏移去调用func函数呢?这下你也应该明白,为什么C语言书上要告诉我们,可变参数前面,必须至少要有一个确定参数(当然,可变宏除外),而且,可变参数必须位于末尾,不能位于参数中间,位于参数中间,就会出现从左往右入栈的问题。
对于已经确定的参数,它在栈上的位置也必须是确定的。衡量参数在栈上的位置,就是离开确切的 函数调用点(call func)有多远。已经确定的参数,它在栈上的位置,不应该依 赖参数的具体数量,因为参数的数量是未知的!所以,选择只能是,已经确定的参 数,离函数调用点有确定的距离。满足这个条件,只有参数入栈遵从自右向左规则。
这道这个之后,我们可以开始编写我们的printf函数了,因为后面的实现,要使用这个特性。
对于具体的实现,我不想再赘述,只是说明一下里面的va_list等数据类型,以及他们的实现和原理。
按照ANSI(AmericanNationalStandardsInstitute)标准,不能对void指针进行算法操作,即下列操作都是不合法的:
void * pvoid;
pvoid ++;//ANSI:错误
pvoid += 1;//ANSI:错误
ANSI标准之所以这样认定,是因为它坚持:进行算法操作的指针必须是确定知道其指向数据类型大小的。例如:
int * pint;
pint ++;//ANSI:正确
pint++的结果是使其增大sizeof(int)。
但是大名鼎鼎的GNU(GNU’sNotUnix的缩写)则不这么认定,它指定void * 的算法操作与char * 一致。
因此下列语句在GNU编译器中皆正确:
pvoid ++;//GNU:正确
pvoid += 1;//GNU:正确
pvoid++的执行结果是其增大了1。
在实际的程序设计中,为迎合ANSI标准,并提高程序的可移植性,我们可以这样编写实现同样功能的代码:
void * pvoid;
(char*)pvoid ++;//ANSI:正确;GNU:正确
(char*)pvoid += 1;//ANSI:正确;GNU:正确
GNU和ANSI还有一些区别,总体而言,GNU较ANSI更“开放”,提供了对更多语法的支持。但是我们在真实设计时,还是应该尽可能地迎合ANSI标准。
在windows平台下,va_list是char *的别名,通过typedef声明而来,而在GNU上,
这个__ptr_t是va_list通过层层define之后最后的原型,可以看到,GNU中,va_list确实是定义为void *类型,但是上面的分析也可以看出,GNU中void *指针默认操作是char *.但是我们自己实现的printf函数中,还是采用char *这样的通用操作。
typedef char * va_list;
现在,应该说明va_start这个宏了,它的定义为:
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
这个_INTSIZEOF宏的作用就有讲究了,它要实现的功能是保证偏移是int类型大小的整数倍,比如你的类型所占字节是1或者2,或者4,通过这个宏之后,最后的结果都是4,如果你的类型为8,最后得出的也为8,读者可以自己计算验证.在我们的系统中,int为4个字节。为什么要这样?因为我们内存会有个对齐机制,关于这个机制我在以前的随笔中专门分析过。这个内存对齐机制,关系到指针的偏移情况,所以要确保偏移是编译器默认对齐字节的整数倍。一般情况,32位编译器,默认4字节对齐,64位编译器,有的为了兼容32位,采取4字节对齐,有的为了更高效,采取8字节对齐,这些默认对齐方式,是可以通过程序更改的。
为了保证内存的4字节对齐,GNU那帮大牛们实现了_INTSIZEOF宏,在我的arm-linux-gcc编译器上,默认是4字节对齐的。补充说明,直接写1.2345这样的小数,默认是double类型,而不是float类型,这个几乎在现代编译器上都是这样规定的。至于如何想到的这个偏移求解方式,就是基本功的累积和数学的累积了,我经常说自己怎么总是写if ,else这样的代码,别人也只是用了&和取反就实现了一个算法,既然已经有巨人存在了,我们就好好站在他们的肩膀上学习,争取以后自己慢慢也能成为这样的巨人。
回到可变参数,通过固定参数,然后指针偏移,然后取值。这样的步骤就可以得到可变参数了。
可以看出,va_arg这个宏,其实要执行两个操作,一个是取值,一个是移动指针,那么一个宏定义如何实现执行两步操作呢?答案是:逗号表达式。
va_end宏就比较简单了。
贴出编译器对这三个宏的定义。
vc6.0中的stdarg.h
typedef char * va_list;
//当sizeof(n)=1/2/4时,_INTSIZEOF(n)等于4
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )
主要说明va_arg,其实这个宏是通过逗号表达式化解而来的。我们知道要进行取值和移动指针操作,而且是先取值,再移动指针,那么逗号表达式就派上用场了。
#define va_arg(ap,t) (ap = ap + _INTSIZEOF(t), *(t *)(ap - _INTSIZEOF(t)))
这个逗号表达式,优先级低于赋值符 =,那么要执行取值,移动指针,首先ap保存了偏移ap + _INTSIZEOF(t),那么很显然,最后要实现取值,所以要减去偏移,然后解引用。把这个式子化解一下,就是下面的表达式。关于逗号表达式,我在之前的随笔中有讲过。
#define va_arg(ap,t) (*(t *)(ap = ap + _INTSIZEOF(t), ap - _INTSIZEOF(t)))
由于逗号表达式右边的才是最终结果,上式化解顺利成章,再化解,ap先偏移并保存,然后减去偏移不保存解引用,就成为了库函数头文件定义了:
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
源码(putchar是之前串口程序已经实现了的):
#include "my_printf.h"
//==================================================================================================
typedef char * va_list;
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
//#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_arg(ap,t) ( *(t *)( ap=ap + _INTSIZEOF(t), ap- _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )
//==================================================================================================
unsigned char hex_tab[]={'0','1','2','3','4','5','6','7',
'8','9','a','b','c','d','e','f'};
static int outc(int c)
{
__out_putchar(c);
return 0;
}
static int outs (const char *s)
{
while (*s != '