8.从零开始实现printf函数

目录

1.调用系统printf

         2.分析printf函数的来源

3.手动控制可变参数(变参函数使用)

4.自动控制可变参数(变参函数使用)

5.在x86平台实现自己的printf函数

6.在ARM平台上实现自己的打印函数

附录:源代码


 1.调用系统printf

我们在写c语言函数时,经常调用printf函数打印调试信息,现编写一个简单的程序,编译运行一下:

例子:

新建一个文件命名为:printf_test.c

#include <stdio.h>

void test(void)
{
	printf("yuan\n");
	printf("char             =%c,%c\n",'A','B');
	printf("string           =%s\n"   ,"hello yuan");
	printf("int number       =%d\n"   ,666666);
	printf("int -number      =%d\n"   ,-666666);
	printf("hex              =0x%x\n" ,0x12345678);
	
}
int main()
{
	test();
	return 0;

}

上传到Linux系统使用gcc编译器进行编译,运行,根据我们给的格式打印出信息:


2.分析printf函数的来源

2.1.printf函数的声明

int printf(const char *format, ...);
format   :固定参数
...           :可变参数(变参)

注意:其中可变参数是重点!

2.2.printf中的格式控制

2.3.修改int printf(const char *format, ...); 重新实现这个函数

修改这个函数的名字为int myprintf(const char *format, ...)

例子:

新建一个c语言文件,命名为:myprintf01.c ,内容如下:



#include <stdio.h>

//int printf(const char *format, ...)
int myprintf(const char *format, ...)
{

	printf("arg1 : %s\n",format);
	return 0;
}



int main()
{
	printf("==========================\n");
	printf("sizeof(char   )=%d\n", sizeof(char   ));
	printf("sizeof(int    )=%d\n", sizeof(int    ));
	printf("sizeof(char  *)=%d\n", sizeof(char  *));
	printf("sizeof(char **)=%d\n", sizeof(char **));
	printf("==========================\n");
	myprintf("aaaa");
	return 0;
	
 
}

使用Linux系统的gcc编译器进行编译:

使用命令:gcc -m32 -o  myprintf  myprintf.c

编译运行:

解释 :当调用了myprintf(”aaaa“),此时把aaaa传给了固定参数format,未使用可变参数

编译注意事项:

此处注意 Ubuntu16.4的系统是64位的系统,如果直接编译就是按64位系统编译出bin文件,那么这个程序只能在64位系统才能正确运行,我们ARM是32位的,使用gcc时可以使用选项(-m32)编译出32位的应用程序。

例如有一个mian.c文件在 64位Ubuntu系统,可以这样编译:

gcc -m32 -o main main.c 

如果遇到错误:

可以通过安装一些配置来解决:

sudo  apt-get  update                     --更新Ubuntu的软件库
sudo  apt-get  purge  libc6-dev     --安装libc6-dev软件
sudo  apt-get  install  libc6-dev
sudo  apt-get  install  libc6-dev-i386


3.手动控制可变参数(变参函数使用)

问题:int myprintf(const char *format, ...)   ,如何使用函数里的可变参数?

解决思路:

1.x86平台,函数调用时的参数传递使用的是堆栈的形式,如下简图所示:

如果调用函数myprintf("aaaa",123);  是这样存放的

如果知道format的地址,那么根据format所占的字节数,就可以知道存放123这块内存的首地址了。

定义一个指针变量  p ,其存放的是format的地址,即为: p = &format;

那么如何知道123存放的地址,只需知道format所占的字节数即可。

由函数原型:int myprintf(const char *format, ...) 可知,format为 (char *)类型,那么占用四个字节的地址。

例子1:

那么可以修改上边的程序,试着打印可变参数,123:



#include <stdio.h>

//int printf(const char *format, ...)
int myprintf(const char *format, ...)
{

    char *p=(char *)&format;
	int i;
	//打印第一个参数
	printf("arg1 : %s\n",format);
	/*打印第一个可变参数
	 *1.移动指针
	 *2.取值
	 *3打印参数
	 */
	p = p + sizeof(char *);
	i = *((int *)p);
	printf("arg2 : %d\n",i);
	return 0;
}



int main()
{
	printf("==========================\n");
	printf("sizeof(char   )=%d\n", sizeof(char   ));
	printf("sizeof(int    )=%d\n", sizeof(int    ));
	printf("sizeof(char  *)=%d\n", sizeof(char  *));
	printf("sizeof(char **)=%d\n", sizeof(char **));
	printf("==========================\n");
	myprintf("aaaa",123);
	return 0;
	
 
}

编译,运行:

例子2:

继续增加可变参数的数量,增加一个结构体,然后把结构体变量作为可变参数进行传递

定义一个结构体,然后进行初始赋值,然后把定义的结构体变量传递给myprintf

这时如何去修改 myprintf函数,将结构体变量的值打印出来呢?

堆栈是一块连续的控件,因此结构体在内存中这样表示:

修改myprintf代码如下:

整体代码如下:同时打印结构体所占的字节数



#include <stdio.h>
struct student
{
	char *name;
	int age;
	char score;
	int idnumber;
};

//int printf(const char *format, ...)
int myprintf(const char *format, ...)
{

    char *p=(char *)&format;
	int i;
	struct student st;
	//打印第一个参数
	printf("arg1 : %s\n",format);
	/*打印第一个可变参数
	 *1.移动指针
	 *2.取值
	 *3打印参数
	 */
	p = p + sizeof(char *);
	i = *((int *)p);
	printf("arg2 : %d\n",i);
	/*打印第二个可变参数
	 *1.移动指针
	 *2.取值
	 *3打印参数
	 */
	p = p + sizeof(int);
	st = *((struct student *)p);
	printf("arg3 : name = %s, age = %d , score = %c, id = %d\n" ,\
		           st.name,  st.age   , st.score   ,st.idnumber);
	return 0;
}



int main()
{
	struct student st={"yuan",20,'A',666};
	printf("==========================\n");
	printf("sizeof(char   )=%d\n", sizeof(char   ));
	printf("sizeof(int    )=%d\n", sizeof(int    ));
	printf("sizeof(char  *)=%d\n", sizeof(char  *));
	printf("sizeof(char **)=%d\n", sizeof(char **));
	printf("sizeof(struct student)=%d\n", sizeof(struct student));
	printf("==========================\n");
	myprintf("aaaa",123,st);
	return 0;
	
 
}

在程序还运行之前,计算一下结构体所占的内存大小:

编译运行程序:

解释:在x86(32位机器)平台下,GCC编译器默认按四字节对齐 

补充:字节对齐

在x86(32位机器)平台下,GCC编译器默认按四字节对齐

例子验证:

定义一个结构体,给其赋值,然后打印他们的值和地址,如下:

#include <stdio.h>

struct student
{
	char *name;
	int age;
	char score;
	int idnumber;
};


int main()
{
	struct student st={"yuan",20,'A',666};
	printf("==========================\n");
	printf("sizeof(char   )=%d\n", sizeof(char   ));
	printf("sizeof(int    )=%d\n", sizeof(int    ));
	printf("sizeof(char  *)=%d\n", sizeof(char  *));
	printf("sizeof(char **)=%d\n", sizeof(char **));
	printf("sizeof(struct student)=%d\n", sizeof(struct student));
	printf("==========================\n");
	
	/*打印各个成员的地址和值*/
	printf("&st.name    =%p,st.name    =%s\n",&st.name    ,st.name);
	printf("&st.age     =%p,st.age     =%d\n",&st.age     ,st.age);
	printf("&st.score   =%p,st.score   =%c\n",&st.score   ,st.score);
	printf("&st.idnumber=%p,st.idnumber=%d\n",&st.idnumber,st.idnumber);
	return 0;
	
 
}

编译运行:

其在内存中的位置简图如下: 

由于在x86(32位机器)平台下,GCC编译器默认按4字节对齐,

如:结构体4字节对齐,即结构体成员变量所在的内存地址是4的整数倍。

可以通过使用gcc中的__attribute__选项来设置指定的对齐大小。

1):
__attribute__ ((packed)),让所作用的结构体取消在编译过程中的优化对齐,按照实际占用字节数进行对齐。

2):
__attribute((aligned (n))),让所作用的结构体成员对齐在n字节边界上。如果结构体中有成员变量的字节长度大于n,则按照最大成员变量的字节长度来对齐。


 :例子2:

设置结构体的对齐方式,让所作用的结构体取消在编译过程中的优化对齐,按照实际占用字节数进行对齐,和四字节对齐

打印出来:

编译运行:

例子3:

调用函数:myprintf("aaaa",123,st,'A');

那么此时在内存中的表现就是这样子:

只要修改的是myprintf函数:

整体代码如下:



#include <stdio.h>
struct student
{
	char *name;
	int age;
	char score;
	int idnumber;
};

//int printf(const char *format, ...)
int myprintf(const char *format, ...)
{

    char *p=(char *)&format;
	int i;
	struct student st;
	char c;
	//打印第一个参数
	printf("arg1 : %s\n",format);
	/*打印第一个可变参数
	 *1.移动指针
	 *2.取值
	 *3打印参数
	 */
	p = p + sizeof(char *);
	i = *((int *)p);
	printf("arg2 : %d\n",i);
	/*打印第二个可变参数
	 *1.移动指针
	 *2.取值
	 *3打印参数
	 */
	p = p + sizeof(int);
	st = *((struct student *)p);
	printf("arg3 : name = %s, age = %d , score = %c, id = %d\n" ,\
		           st.name,  st.age   , st.score   ,st.idnumber);
	/*打印第三个可变参数
	 *1.移动指针
	 *2.取值
	 *3打印参数
	 */
	p = p + sizeof(struct student);
	c = *((char *)p);
 	printf("arg4 : %c\n",c);
 	return 0;
}



int main()
{
	struct student st={"yuan",20,'A',666};
	printf("==========================\n");
	printf("sizeof(char   )=%d\n", sizeof(char   ));
	printf("sizeof(int    )=%d\n", sizeof(int    ));
	printf("sizeof(char  *)=%d\n", sizeof(char  *));
	printf("sizeof(char **)=%d\n", sizeof(char **));
	printf("sizeof(struct student)=%d\n", sizeof(struct student));
	printf("==========================\n");
	myprintf("aaaa",123,st,'A');
	return 0;
	
 
}

编译运行:

例子4:

在上面的基础上再增加一个参数:

调用:myprintf("aaaa",123,st,'A','B');

然后直接修改myprintf函数,如下:

这样子修改可以吗?试着运行一下:

这也验证了四字节对齐,因为第四个参数是A,是char类型,只占一个字节,而GCC编译器默认按4字节对齐,所以存放 A字符,只需要一个字节的地址,其他三个应该都不存放有效值,如果需要寻找 B  的值,需要偏移3个地址,如下:

此时简图如下:

整体代码如下:



#include <stdio.h>
struct student
{
	char *name;
	int age;
	char score;
	int idnumber;
};

//int printf(const char *format, ...)
int myprintf(const char *format, ...)
{

    char *p=(char *)&format;
	int i;
	struct student st;
	char c,b;
	//打印第一个参数
	printf("arg1 : %s\n",format);
	/*打印第一个可变参数
	 *1.移动指针
	 *2.取值
	 *3打印参数
	 */
	p = p + sizeof(char *);
	i = *((int *)p);
	printf("arg2 : %d\n",i);
	/*打印第二个可变参数
	 *1.移动指针
	 *2.取值
	 *3打印参数
	 */
	p = p + sizeof(int);
	st = *((struct student *)p);
	printf("arg3 : name = %s, age = %d , score = %c, id = %d\n" ,\
		           st.name,  st.age   , st.score   ,st.idnumber);
	/*打印第三个可变参数
	 *1.移动指针
	 *2.取值
	 *3打印参数
	 */
	p = p + sizeof(struct student);
	c = *((char *)p);
 	printf("arg4 : %c\n",c);
	/*打印第四个可变参数
	 *1.移动指针
	 *2.取值
	 *3打印参数
	 */
	p = p + (sizeof(char)+3);
	b = *((char *)p);
 	printf("arg5 : %c\n",b);
 	return 0;
}



int main()
{
	struct student st={"yuan",20,'A',666};
	printf("==========================\n");
	printf("sizeof(char   )=%d\n", sizeof(char   ));
	printf("sizeof(int    )=%d\n", sizeof(int    ));
	printf("sizeof(char  *)=%d\n", sizeof(char  *));
	printf("sizeof(char **)=%d\n", sizeof(char **));
	printf("sizeof(struct student)=%d\n", sizeof(struct student));
	printf("==========================\n");
	myprintf("aaaa",123,st,'A','B');
	return 0;
	
 
}

编译运行:


查看规律:

我们可以把上面的例子简化成下面简图的方式,即为:两个步骤,取值和移动指针。

现如今程序修改如下,改变的只有myprintf里函数的顺序而已:

int myprintf(const char *format, ...)
{
    //p指向第一个可变参数,取值
    char *p=(char *)&format;
	int i;
	struct student st;
	char c,b;
	
	/*打印第一个参数format
	 *1.取值
	 *2.移动指针到下一次取值位置
	 *3打印参数
	 */
	//移动指针
	p = p + sizeof(char *);
	printf("arg1 : %s\n",format);
    /*============================*/
	/*打印第一个可变参数
	 *1.取值
	 *2.移动指针到下一次取值位置
	 *3打印参数
	 */
	//取值
	i = *((int *)p);
	//移动指针到下一次取值位置
	p = p + sizeof(int);
	printf("arg2 : %d\n",i);
	/*============================*/
	/*打印第二个可变参数
	 *1.取值
	 *2.移动指针到下一次取值位置
	 *3打印参数
	 */
	//取值
	st = *((struct student *)p);
	//移动指针到下一次取值位置
	p = p + sizeof(struct student);
	printf("arg3 : name = %s, age = %d , score = %c, id = %d\n" ,\
		           st.name,  st.age   , st.score   ,st.idnumber);
	/*============================*/
	/*打印第四个可变参数
	 *1.取值
	 *2.移动指针到下一次取值位置
	 *3打印参数
	 */
	//取值
	c = *((char *)p);
	//移动指针到下一次取值位置
	p = p + (sizeof(char)+3);
 	printf("arg4 : %c\n",c);
	/*打印第四个可变参数
	 *1.取值
	 *2.移动指针到下一次取值位置
	 *3打印参数
	 */
	//取值
	b = *((char *)p);
	p = p + (sizeof(char)+3);
 	printf("arg5 : %c\n",b);
 	return 0;
}

运行程序,还是和上面的程序,正常打印出可变参数的信息

虽然我们简化了这个程序的思路,但是我们还是需要手动静态的去修改需要移动的指针,而且移动的距离也不是固定的,那么有没有一个函数能够帮我们解决这个问题呢?

答案是可以的。


4.自动控制可变参数(变参函数使用)

4.1.可变参数控制函数

在库文件 <stdarg.h> 中有对可变参数函数的定义,如下:

可变参数函数定义
先后步骤 使用说明
int myprintf(const char *format,...)

format是固定参数,...是可变参数

函数调用:myprintf("aaaa",666,st,'A','B');则:

第一个变参是:666

第二个变参是:st结构体

第三个变参是:A

第四个变参是:B

va_list   p; 定义一个变参变量p,等价于char *p
va_start(p,format);

fotmat是固定参数,经过va_start(p,format)后,移动指针p到第一个变参变量

变量类型  var;

var = va_arg(p,变量类型)

在已知变量类型的情况下:

1.返回当前p所指向的变参变量的值传递给var,

2.然后移动指针p到下一个变参变量

va_end(p); 结束变参变量的使用,等价于 p=NULL; 避免野指针。

可以查看一下其在vc++6.0中的定义(函数原型):

②宏定义

宏定义解析:

简化上面的代码如下:



#include <stdio.h>
#include <stdarg.h>
struct student
{
	char *name;
	int age;
	char score;
	int idnumber;
};

//int printf(const char *format, ...)
int myprintf(const char *format, ...)
{
    
      
	int i;
	struct student st;
	char c,b;
	va_list p;            //等价于char *p;
	/*打印第一个参数format
	 *1.移动指针到下一次取值位置
	 *2打印参数
	 */
	//移动指针
	va_start(p,format);  //等价于p = p + sizeof(char *);
	printf("arg1 : %s\n",format);
    /*============================*/
	/*打印第一个可变参数
	 *1.取值
	 *2.移动指针到下一次取值位置
	 *3打印参数
	 */
                                //取值
                              	//i = *((int *)p);
       i = va_arg(p,int);       //移动指针到下一次取值位置
                              	//p = p + sizeof(int);
	printf("arg2 : %d\n",i);
	/*============================*/
	/*打印第二个可变参数
	 *1.取值
	 *2.移动指针到下一次取值位置
	 *3打印参数
	 */
								     //取值
								     //st = *((struct student *)p);
		st=va_arg(p,struct student); //移动指针到下一次取值位置
								     //p = p + sizeof(struct student);
	printf("arg3 : name = %s, age = %d , score = %c, id = %d\n" ,\
		           st.name,  st.age   , st.score   ,st.idnumber);
	/*============================*/
	/*打印第四个可变参数
	 *1.取值
	 *2.移动指针到下一次取值位置
	 *3打印参数
	 */
										//取值
										//c = *((char *)p);
	 c=va_arg(p,int);					//移动指针到下一次取值位置
										//p = p + (sizeof(char)+3);
 	printf("arg4 : %c\n",c);
	/*打印第四个可变参数
	 *1.取值
	 *2.移动指针到下一次取值位置
	 *3打印参数
	 */
										//取值
										//b = *((char *)p);
	b=va_arg(p,int);					//移动指针到下一次取值位置
										//p = p + (sizeof(char)+3);
 	printf("arg5 : %c\n",b);
    va_end(p); //p指向空,避免野指针
	
 	return 0;
}



int main()
{
	struct student st={"yuan",20,'A',666};
	printf("==========================\n");
	printf("sizeof(char   )=%d\n", sizeof(char   ));
	printf("sizeof(int    )=%d\n", sizeof(int    ));
	printf("sizeof(char  *)=%d\n", sizeof(char  *));
	printf("sizeof(char **)=%d\n", sizeof(char **));
	printf("sizeof(struct student)=%d\n", sizeof(struct student));
	printf("==========================\n");
	myprintf("aaaa",123,st,'A','B');
	return 0;
	
 
}

此处注意:


5.在x86平台实现自己的printf函数

5.1.创建两个文件,分别是:myprintf.c和myprintf.h

myprinf.h的内容如下:

#ifndef _MYPRINTF_H
#define _MYPRINTF_H


#define  MAX_NUMBER_BYTES  64

int myprintf(const char *fmt, ...);

#endif 

编写myprintf.c的内容:

①首先把第四节说明的几条宏定义复制过来,也就是下面这几个宏定义

②定义字符输出和字符串输出函数,如下,调用<stdio.h>的putchar函数

static int myputchar(int c) 
{
	putchar(c);
	return 0;
}

static int myputString(const char *s)
{
	while (*s != '\0')	
		putchar(*s++);
	return 0;
}

③其中比较重要的是,编写可变参数函数

//reference :  int printf(const char *format, ...); 
int myprintf(const char *format, ...) 
{
	va_list p;  //定义一个char * 类型的指针变量

	va_start(p, format);   //使p指向第一个可变参数
	my_vprintf(format, p);	
	va_end(p);             //避免野指针
	return 0;
}

其中比较重要的是my_vprintf(format,p函数),其中肯定涉及了,取值和移动指针的函数(va_arg(p,变量类型))

整体代码:

#include "myprintf.h"
#include <stdio.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_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 myputchar(int c) 
 {
	 putchar(c);
	 return 0;
 }
 
 static int myputString(const char *s)
 {
	 while (*s != '\0')  
		 putchar(*s++);
	 return 0;
 }

 //va_arg(ap, int),   10,	   lead,	maxwidth
 //   取值,移动指针 		10进制	前导码		 最大宽度
 static int out_num(long n, int base) 
 {
	 unsigned long m=0;
	 //把需要输出的东西进行组合,放进一个数组里面
	 //MAX_NUMBER_BYTES 最大为64个字节
	 char buf[MAX_NUMBER_BYTES];
	 char *s = buf + sizeof(buf);	//数组的末端地址
	 int count=0,i=0;
 
	 s	=s-1;
	 *s ='\0';	//在buf数组最后加上字符串结束符 ‘\0’
	 /*判断不定参数的值是否大于0,小于0则加上负号*/
	 if (n < 0){
		 m = -n;  
	 }
	 else{
		 m = n;
	 }
	 /*根据不定参数取出来的值进行进制的转换,比如100 转换成
	  *16进制,其值为64,如何转换呢?
	  *开始		  *(--s)=100%16=4
	  * 		   count++;  count=1   判断((m /= base) != 0)   100/16= 6 还不等于0,继续do里面的语句
	  * 		  *(--s)=6%10  =6
	  * 		   count++;  count=2   判断((m /= base) != 0)   6/16 =  0  则退出do循环
	  */
	 do{
		 *--s = hex_tab[m%base];
		 count++;
	 }while ((m /= base) != 0);
 
	 if (n < 0)
		 *--s = '-';
	 
	 return myputString(s);
 }

 /*reference :	 int vprintf(const char *format, va_list ap); */
 static int my_vprintf(const char *fmt, va_list ap) 
 {
	 
	 
	  for(; *fmt != '\0'; fmt++)   //此处是检测第一个参数,也就是format指向的地址,为不可变参数
	  {
			 //果没有碰到 % 符号,直接输出一个字符
			 if (*fmt != '%') {
				 myputchar(*fmt);
		     	 continue;
			 }
		 //format : %d,%u,%x,%f,%c,%s 
			 fmt++;   
		 //前面都只是输出 % 符号前面的字符而已,并没有做什么
		 switch (*fmt) {   //根据需要输出的格式,进行输出
 
		 //根据需要输出的字符,其中va_arg的功能是取值和移动指针
		 case 'd': out_num(va_arg(ap, int), 		 10); break;
		 case 'o': out_num(va_arg(ap, unsigned int),  8); break;				 
		 case 'u': out_num(va_arg(ap, unsigned int), 10); break;
		 case 'x': out_num(va_arg(ap, unsigned int), 16); break;
		 case 'c': myputchar(va_arg(ap, int   ));         break;	 
		 case 's': myputString(va_arg(ap, char *));       break;				 
		 default:  myputchar(*fmt);                       break;
			 }
	 }
	 return 0;
 }

 
 //reference :	int printf(const char *format, ...); 
 int myprintf(const char *format, ...) 
 {
	 va_list p;  //定义一个char * 类型的指针变量
 
	 va_start(p, format);	//使p指向第一个可变参数
	 my_vprintf(format, p);  
	 va_end(p); 			//避免野指针
	 
 }


5.2.新建一个main.c函数调用myprintf函数,进行格式打印:

内容如下:

注意:此处无需调用  <stdio.h>的库,因为我们printf打印函数是自己实现的,即为:myprintf


#include "myprintf.h"

int main()
{

	int a=100;	
	myprintf("char                =%c,%c\n\r", 'A','a');	
	myprintf("number1             =%d\n\r",    123456);
	myprintf("number2			  =%d\n\r",    -123456);	
	myprintf("hex number          =0x%x\n\r",  0x12345678);	
	myprintf("string              =%s\n\r",    "hello yuan!");	
	myprintf("num                 =%d\n\r",    0x64);
	myprintf("hello yuan          =0x%x,%d\n\r",    a,a);
	return 0;
}


5.3.上传到Linux系统进行编译:

使用命令:gcc -m32 -o out myprintf.c main.c

然后运行可执行文件 out ,正常打印输出,和调用系统的的打印函数是一样的,这样我们就在x86平台实现了打印的函数,可以进行调试:


6.在ARM平台上实现自己的打印函数

现在需要在arm平台实现这个打印函数,肯定是要考虑到硬件,和上面在x86实现唯一不同的就是,putchar函数。

1).在x86平台上,我们调用的是系统 <stdio.h> 库中的 putchar函数,输出一个字符,也是通过putchar函数来输出字符串的

2).所以在arm平台上,我们同样去实现这样一个底层函数,这个在上一节已经实现了,具体查看上一节 点我查看 。

解决了硬件问题了之后,剩下的就是软件的问题了。

把上一节实现能在arm平台实现打印的所有文件复制下来,再和上边实现的两个myprintf.c和myprintf.h文件放在同一个文件夹里面。如下:

 1.现在需要修改myprintf函数,把myprintf中的putchar函数,改成在ARM平台实现的putchar(也就是在uart.c中实现的putchar)

2.接着修改main.c中的函数,调用测试一下myprintf,看是否能成功在ARM平台上实现打印函数,修改如下:

#include "s3c2440_soc.h"
#include "uart.h"

void printf_test(void)
{
	int a=100;	
	myprintf("char                =%c,%c\n\r"   ,'A','a');	
	myprintf("number1             =%d\n\r"      ,123456);
	myprintf("number2             =%d\n\r"      ,-123456);	
	myprintf("hex number          =0x%x\n\r"    ,0x12345678);	
	myprintf("string              =%s\n\r"      ,"hello yuan!");	
	myprintf("num                 =%d\n\r"      ,0x64);
	myprintf("hello yuan          =0x%x,%d\n\r" ,a,a);

}

int main()
{
	unsigned char c;
	uart0_init();
	putString(
"Hello world!\r\n");
	printf_test();
	while(1)
	{
		c =getchar();
		if(c== '\r')
		{
			putString("\n");
		}
		putchar(c);
	}
	return 0;

}

3.修改Makefile文件,因为现在添加了几个文件同样需要编译,修改如下:

all:
	arm-linux-gcc -c -o uart.o uart.c
	arm-linux-gcc -c -o myprintf.o myprintf.c  
	arm-linux-gcc -c -o lib1funcs.o lib1funcs.S
	arm-linux-gcc -c -o main.o main.c
	arm-linux-gcc -c -o start.o start.S
	arm-linux-ld -Ttext 0 start.o  uart.o lib1funcs.o myprintf.o  main.o -o uart.elf
	arm-linux-objcopy -O binary -S uart.elf uart.bin	
	arm-linux-objdump -D uart.elf > uart.dis

clean:
	rm *.bin *.o *.elf *.dis

4.上传到Linux系统进行编译:

看一下bin文件的大小,发现既然差不多有40K?

如果直接烧录到ARM的nand flash 中肯定是不能正常运行的。

回忆一RAM芯片的启动过程:启动过程(大多数ARM芯片从0地址启动)

1.NoR启动,NoR Flash 基地址为0(片内RAM地址为:0x40000000),CPU读出NOR读出上的第一个指令(前四个字节)执行,CPU继续读出其他指令执行。

2.Nand启动,片内4K SRAM 基地之为0(Nor Falsh 不可访问),硬件2240把NAND前4K内容复制到片内内存SRAM中,然后CPU从0地址取出第一条指令(前四个字节)执行。
 

前4K复制到片内内存SRAM中,那么其他三十几k的程序就不能运行了。

但是理论上来说,我们就写这个几个文件,编译起来的bin文件为何如此之大

这是因为代码段没有衔接的原因。我们只需手动指定代码的位置,就可以减少产生bin文件的大小。

1).把uart.dis 传回window中,打开分析一下:

代码段的地址,从0开始

直到下一个Disassembly of section .rodata:(只读段),之前都是属于代码段

那么代码段的地址就是从: 0 到 0x9f0  . 他们是连续的。


从只读段继续分析,可以看到代码段和只读段是连续的,只读段从地址0x9f4开始

同理到下一个section之前的都是只读段的反汇编代码:

因此只读段的代码,是: 0x9f4 到 0x adc  ,也是连续的


继续查看下一个section,然后就发现了,原来只读段和数据段的地址是不连续的而且相差了很多,这应该就是bin文件那么大的原因了。

计算一下他们之间相差了几个字节: 0x8ae0  - 0xadc = 32772 字节。其实这段空间是不放任何有效数据的,只是空着。所以我们可以在编译的时候指定数据段的起始位置。

指定原则,肯定需要比只读段的结束地址要大,那么就制定为:0xae0

修改Makefile,如下:

all:
	arm-linux-gcc -c -o uart.o uart.c
	arm-linux-gcc -c -o myprintf.o myprintf.c  
	arm-linux-gcc -c -o lib1funcs.o lib1funcs.S
	arm-linux-gcc -c -o main.o main.c
	arm-linux-gcc -c -o start.o start.S
	arm-linux-ld -Ttext 0 -Tdata 0xae0 start.o  uart.o lib1funcs.o myprintf.o  main.o -o uart.elf
	arm-linux-objcopy -O binary -S uart.elf uart.bin	
	arm-linux-objdump -D uart.elf > uart.dis

clean:
	rm *.bin *.o *.elf *.dis

2).使用修改后的Makefile,上传到Linux系统编译一下:

可以看到 uart.bin文件减小为2800字节了(原来的是35568 ,减少了 35568 -2800 =32768),这个数值不就差不多等于我们修改的那个数据段和只读段的空缺部分嘛。

3).把bin文件传回window系统,使用oflash进行烧录(烧录到Nand Flash):

烧录完成后,打开串口调试助手:

开发版连接串口,配置好串口数据格式:

接着复位开发版,从主函数开始运行,打印数据,可以看到,这个和我们在x86平台上实现的功能是一模一样的。


附录:源代码

① x86平台实现的printf函数:https://download.csdn.net/download/qq_36243942/10887825

② arm平台实现的printf函数:https://download.csdn.net/download/qq_36243942/10887837

发布了91 篇原创文章 · 获赞 247 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/qq_36243942/article/details/85342331