一.可变参数函数的原型声明:
type VAFunction(type arg1, type arg2, … );
参数可以分为两部分:个数确定的固定参数和个数可变的可选参数。函数至少需要一个固定参数,固定参数的声明和普通函数一样;可选参数由于个数不确定,声明时用"..."表示。固定参数和可选参数共同构成一个函数的参数列表。
二.具体分析
下面是分析c库中的printf函数,但完全适用与内核printk的分析
三个关键宏:
void va_start ( va_list arg_ptr, prev_param ); /* ANSI version */ type va_arg ( va_list arg_ptr, type ); void va_end ( va_list arg_ptr );
在这些宏中,va就是 variable argument (可变参数)的意思;
arg_ptr 是指向可变参数表的指针;
prev_param 指可变参数表的前一个固定参数;
type为可变参数的类型。
va_list也是一个宏,其定义为typedef char * va_list,实质上是一char型指针。
char型指针的特点: ++ 跟 -- 操作对其作用的结果是增1和减1(因为sizeof(char) == 1)。
<1>va_start宏
(1)定义:
#define va_start(ap,v) (ap =(va_list)&v + _INTSIZEOF(v))
_INTSIZEOF宏定义为:
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) -1) & ~(sizeof(int)-1))
_INTSIZEOF(v) 可以得到 第一个变参对v的偏移地址.因为在32bit系统下栈中数据总是4字节对齐的,所以宏定义写为上述形式。
(2)作用:
根据v取得可变参数表的首指针并赋值给ap,方法:最后一个固定参数v的地址+第一个变参对v的偏移地址,然后赋值给ap,这样ap就是可变参数表的首地址。
(3)举例:
如果有一va函数的声明是void va_test(char a, char b,char c, …),则它的固定参数依次是a,b,c,最后一个固定参数 argN 为c,因此就是va_start(arg_ptr,c)。
<2>va_arg宏
(1)定义:
#define va_arg(list, mode) ((mode *)(list = (char*) ((((int)list + (__builtin_alignof(mode)<=4?3:7)) & (__builtin_alignof(mode)<=4?-4:-8))+sizeof(mode))))[-1]
(2)作用:
指取出当前 arg_ptr 所指的可变参数并将ap指针指向下一可变参数
<3>va_end宏
(1)定义为:
#defineva_end (list)
(2)作用:
结束可变参数的获取。va_end( list )实际上被定义为空,没有任何真实对应的代码,用于代码对称,与va_start对应。
三.实践
<1>怎样得到可变参数个数?归纳起来有三种办法:
(1)函数的第一个参数,指定后续的参数个数,如func(intnum,...)
(2)根据隐含参数,判断参数个数,如printf系列的,通过字符串中%的个数判断
(3)特殊情况下(如参数都是不大于0xFFFF的int),
可以一直向低处访问堆栈,直到返回地址。
<2>举例说明三种情况:
(1)情况1
#include<stdio.h> #include<stdarg.h> void VariableFunc(int prev_param, ...) { va_list arg_ptr; //可变参数表的首指针 va_start(arg_ptr,prev_param); //取得可变参数表的首地址并赋给arg_ptr for(int i=0;i<prev_param;i++) { int ParamValue; ParamValue=va_arg(arg_ptr,int); //取出当前arg_ptr所指的可变参数并将ap指针指向下一可变参数 printf("这是第%d个可变参数,值:%d,类型:int\n",i+1,ParamValue); } va_end(arg_ptr);//执行清理工作 }
(2)情况2
#include<stdio.h> #include<stdarg.h> //包含些头文件 #include<string> using namespace std; //模仿printf函数,写一个printk函数 void printk(char* prev_param, ...) { int j = 0; va_list arg_ptr; //可变参数表的首指针 va_start(arg_ptr ,prev_param); //取得可变参数表的首地址 string FormatStr(prev_param); //保存格式化的字符串 int InsertPos; //当在固定参数中找到%符号: while(-1!=(InsertPos=FormatStr.find("%"))) { //根据%后面的字符分别进行处理 if(FormatStr[InsertPos+1]=='d') //%号后是'd'就转为字符再插入 { char buf[15]; int IntValud = va_arg(arg_ptr ,int); //从可变参数列表中获得数据 itoa(IntValud,buf,10); //Int 转string并保存在buf FormatStr.erase(InsertPos,2);//擦除两个字符%d FormatStr.insert(InsertPos,buf); //插入Int值到FormatStr } else if(FormatStr[InsertPos+1]=='s') ////%号后是's'就直接将字符串插入FormatStr { FormatStr.erase(InsertPos,2); FormatStr.insert(InsertPos,va_arg(arg_ptr,char*)); } } printf("%s\n",FormatStr.c_str()); //打印出处理后的FormatStr va_end(arg_ptr);//执行清理工作 } void main() { printk("show you how %s %s work %d","printf","function",88); }
(3)情况3
#include <stdio.h> #include <stdarg.h> struct T_Progs{ int x; int y; }; void func(T_Progs *tProgs,...) { int total = 0; va_list ap; T_Progs *p; va_start(ap, tProgs); p = tProgs; printf("x[%d]=%d\n",total,p->x); printf("y[%d]=%d\n",total,p->y); total++; while (p = (va_arg(ap,T_Progs*))) { printf("x[%d]=%d\n",total,p->x); printf("y[%d]=%d\n",total,p->y); total ++; } va_end(ap); printf("参数个数:%d\n",total); } void main(void) { T_Progs test1,test2; test1.x = 1;test2.x = 3; test1.y = 2;test2.y = 4; func(&test1,&test2,NULL); }
四.最简单的移植步骤
<1>我们许多选择:
(1)移植linux的printf,版本越新越难移植,但是功能也越强大
(2)移植uboot的printf,实际uboot也是移植到内核的
(3)完全自己编写,但是功能比较弱
在保证整个裸机其他代码部分没有任何问题,且编译器也没有任何问题的情况下,上述三种方法都是可行的。
下面我们只是直接采用韦东山老师移植好的printf相关的库文件,他的办法是移植2.4内核版本的printf功能
<2>拷贝附件里相关库文件到裸机代码根目录
<3>修改makefile如附件所示,必须严格按照makefile里的相关设置
<4>make 编译并测试
测试代码如下:
void test_printf(void) { char *p="this is %s test"; char c='H'; int d=-256; int k=0; printf("testing printf\n"); printf("test string ::: %s\n test char ::: %c\n test digit ::: %d\n test X ::: %x\n test unsigned ::: %u\n test zero ::: %d\n",p,c,d,d,d,k); }
原文链接:
http://www.arm9home.net/read.php?tid=20549&page=1#231834
可以到github上clone 下来移植好的printf 工程项目。