1.基本原理
因为程序目前处于裸板阶段,只能输出字符串,没有C语言的printf函数可以调用。但是在调试程序时,想像C语言一样调用printf来调试,因此只能自己来实现了。
C语言中,printf函数的原型为:
int printf(const char *format, ...);
1.1 可变参数"…"
参数有2个, “const char *format” 和 “…”,这个“…”就是可变参数,下面先讲解一下如何识别这个可变参数。
参数的传递会顺序的放到栈里面,而printf函数可获取的参数只有 “const char *format” 和 “…”,不过format刚好指向了第1个参数的首地址,因此可以通过format地址的移动来找到后续的几个参数。
先通过1个简单的程序了解如何通过format这个参数找到后续的参数。请结合程序下面的图片来理解,必须理解这个程序才可继续往后看,否则会懵逼。
程序是实现自己的printf函数,把传进去的字符串、整数、结构体、字符、浮点数依次打印出来
01_printf.c
#include struct person{ char *name; int age; char score; int id; }; int my_printf(const char *format, ...) { /* 指针p指向format的地址 */ char *p = (char *)&format; int arg2 = 0; struct person arg3; char arg4 = 0; double arg5;// must be duoble /* format指向了第1个参数的首地址 所以直接输出format就可以"abcd" */ printf("arg1:%sn", format); /* 第1参数是字符串 "abcd", p的值加上sizeof(char *)就可以移动到第2个参数 */ p += sizeof(char *); arg2 = *((int *)p); printf("arg2:%dn", arg2); /* 第2个参数是整数 123, p的值加上sizeof(int)就可以移到第3个参数 */ p += sizeof(int); arg3 = *((struct person *)p); printf("arg3:name:%s,age:%d,score:%c, id:%dn", arg3.name, arg3.age, arg3.score, arg3.id); /* 第3个参数是结构体, p的值加上sizeof(struce person)就可以移到第4个参数 */ p += sizeof(struct person); arg4 = *((char *)p); printf("arg4:%cn", arg4); /* 第4个参数是字符 C, 这里需要4对齐,所以加上((sizeof(char) + 3) & ~3) 就可以移到第5个参数 */ p += ((sizeof(char) + 3) & ~3); /* 这里需要转化成double, 否则数据会出错 */ arg5 = *((double *)p); printf("arg5:%lfn", arg5); return 0; } int main(int argc, char *argv) { struct person per = {"Black", 26, 'A', 53}; //字符串 int 结构体 char double my_printf("abcd", 123, per, 'C', 4.321); return 0; } 1.2 获取地址的替代函数 01_printf.c 中,获取参数的都分成2个小步骤: 1)通过sizeof来偏移地址,如 p += sizeof(char *); 2)通过指针强制转换来取值, 如arg2 = *((int *)p); 是否有办法将2个步骤合二为一,这里可以通过va_start, va_end, va_arg来实现,所需要的头文件为#include void va_start(va_list ap, last); type va_arg(va_list ap, type); void va_end(va_list ap); 01_printf.c 中,一开始我们用char *p = (char *)&format; 来获得format的首地址。这里可以通过 va_start 来代替。 1)先定义va_list 变量:va_list p; 2)将format和定义好的va_list p带入va_start: va_start(p, format); char *p = (char *)&format 就等价于 va_start(p, format); 不过va_start事实上还会把p的值指向下一个参数,所以va_start等于做了2个步骤 1)char *p = (char *)&format 2)p += sizeof(char *); 获取第2个参数的2个小步骤可由va_arg来代替: 1)p += sizeof(char *); 2)arg2 = *((int *)p); 等价于 arg2= vs_arg(p, int); 不过对于第2个参数来说,地址的偏移是由va_start(p, format)完成了,所以并不需要再做p += sizeof(char *)了,而是把地址偏移到下个参数。所以vs_arg(p, int) 实际等价于: 1)arg2 = *((int *)p); 2)p += sizeof(int); va_star和va_arg的函数可能有点绕,但总结起来就是2个功能: 1)获取当前参数的值 2)把地址偏移到下1个参数 调用va_start(p, format)时,p已经指向到了第2个参数的位置(第1个参数就是format) 调用arg2= vs_arg(p, int)时,p已经指向到了第3个参数的位置 下面贴出整体替换的代码,自己好好分析一下 03_printf.c #include #include struct person{ char *name; int age; char score; int id; }; //use va_list va_start va_arg va_end int my_printf(const char *format, ...) { va_list p; int arg2 = 0; struct person arg3; char arg4 = 0; double arg5;// must be duoble //arg1 printf("arg1:%sn", format); //char *p = (char *)&format; //p += sizeof(char *); va_start(p, format); //arg2 //arg2 = *((int *)p); //p += sizeof(int); arg2 = va_arg(p, int); printf("arg2:%dn", arg2); //arg3 //arg3 = *((struct person *)p); //p += sizeof(struct person); arg3 = va_arg(p, struct person); printf("arg3:name:%s,age:%d,score:%c, id:%dn", arg3.name, arg3.age, arg3.score, arg3.id); //arg4 //arg4 = *((char *)p); //p += ((sizeof(char) + 3) & ~3); arg4 = va_arg(p, int); //must be int, because alignment is 4 printf("arg4:%cn", arg4); //arg5 //arg5 = *((double *)p); arg5 = va_arg(p, double); printf("arg5:%lfn", arg5); va_end(p); return 0; } int main(int argc, char *argv) { struct person per = {"Black", 26, 'A', 53}; my_printf("abcd", 123, per, 'C', 4.321); return 0; } 1.3 arg_xx的函数原型分析 由于跑裸板程序时,并没有C库可以调用,所以va_star, va_arg, va_end这3个函数需要自己实现。在网上收到这3个函数的原型,原来全是宏,下面对这些宏进行分析说明 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_end(ap) ( ap = (va_list)0 ) #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ): 为了实现sizeof(int)对齐也就是4对齐,所以才有(sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) 这一串操作。就等价于实现了4对齐的一个sizeof宏。 #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ): 如同前面说的,va_start实现了把ap偏移到下1个参数的功能。 这里先讲1个前提知识,请看下面test.c的代码,最终A的值是2。如果是#define A (1, 2, 3) 那么最终A的值会是3,C语言就有这么一个小规则。 test.c #include #define A (1,2) int main(int argc, char *argv) { printf("A=%dn", A); return 0; } #define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ): 这个宏实现了2个功能,第1就是参数强制转换成对应的类型,第2就是将地址偏移到下1个参数。 *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t))可以拆开成2个步骤: ap = ap + _INTSIZEOF(t) *(t *)(ap - _INTSIZEOF(t)) 将这2个步骤带到va_arg(ap,t)中 #define va_arg(ap,t) (ap = ap + _INTSIZEOF(t), *(t *)(ap - _INTSIZEOF(t))) ap = ap + _INTSIZEOF(t) 把ap地址偏移到了下1个参数 *(t *)(ap - _INTSIZEOF(t)) 把ap地址减去_INTSIZEOF(t)再强制转换,此时ap还是偏移在下1个参数不过 *(t *)(ap - _INTSIZEOF(t)) 整体作为了宏的值 还可以写成这样 #define va_arg(ap,t) (*(t *)(ap = ap + _INTSIZEOF(t), ap - _INTSIZEOF(t))) 2.源码 __out_putchar©函数就是就是putchar,也就是前篇文章实现的函数 https://blog.csdn.net/lian494362816/article/details/85083263 最后printf函数的实现我就不讲解了,自己看看源码分析分析。 #define __out_putchar putchar int putchar(int c) { while (!(UTRSTAT0 & 0x4)) { //nothing } UTXH0 = (unsigned char )c; } my_printf.c #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 ) static 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 != '