天天看點

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編譯器進行編譯,運作,根據我們給的格式列印出資訊:

8.從零開始實作printf函數

2.分析printf函數的來源

2.1.printf函數的聲明

int printf(const char *format, ...);

format   :固定參數

...           :可變參數(變參)

注意:其中可變參數是重點!

2.2.printf中的格式控制

8.從零開始實作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

編譯運作:

8.從零開始實作printf函數

解釋 :當調用了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 

如果遇到錯誤:

8.從零開始實作printf函數

可以通過安裝一些配置來解決:

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平台,函數調用時的參數傳遞使用的是堆棧的形式,如下簡圖所示:

8.從零開始實作printf函數

如果調用函數myprintf("aaaa",123);  是這樣存放的

8.從零開始實作printf函數

如果知道format的位址,那麼根據format所占的位元組數,就可以知道存放123這塊記憶體的首位址了。

定義一個指針變量  p ,其存放的是format的位址,即為: p = &format;

那麼如何知道123存放的位址,隻需知道format所占的位元組數即可。

由函數原型:int myprintf(const char *format, ...) 可知,format為 (char *)類型,那麼占用四個位元組的位址。

8.從零開始實作printf函數

例子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;
	
 
}
           

編譯,運作:

8.從零開始實作printf函數

例子2:

繼續增加可變參數的數量,增加一個結構體,然後把結構體變量作為可變參數進行傳遞

定義一個結構體,然後進行初始指派,然後把定義的結構體變量傳遞給myprintf

8.從零開始實作printf函數
8.從零開始實作printf函數

這時如何去修改 myprintf函數,将結構體變量的值列印出來呢?

堆棧是一塊連續的控件,是以結構體在記憶體中這樣表示:

8.從零開始實作printf函數

修改myprintf代碼如下:

8.從零開始實作printf函數

整體代碼如下:同時列印結構體所占的位元組數

#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;
	
 
}
           

在程式還運作之前,計算一下結構體所占的記憶體大小:

8.從零開始實作printf函數

編譯運作程式:

8.從零開始實作printf函數

解釋:在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;
	
 
}
           
編譯運作:
8.從零開始實作printf函數
其在記憶體中的位置簡圖如下: 
8.從零開始實作printf函數

由于在x86(32位機器)平台下,GCC編譯器預設按4位元組對齊,

如:結構體4位元組對齊,即結構體成員變量所在的記憶體位址是4的整數倍。

可以通過使用gcc中的__attribute__選項來設定指定的對齊大小。

1):

__attribute__ ((packed)),讓所作用的結構體取消在編譯過程中的優化對齊,按照實際占用位元組數進行對齊。

2):

__attribute((aligned (n))),讓所作用的結構體成員對齊在n位元組邊界上。如果結構體中有成員變量的位元組長度大于n,則按照最大成員變量的位元組長度來對齊。

 :例子2:

設定結構體的對齊方式,讓所作用的結構體取消在編譯過程中的優化對齊,按照實際占用位元組數進行對齊,和四位元組對齊

8.從零開始實作printf函數
列印出來:
8.從零開始實作printf函數
編譯運作:
8.從零開始實作printf函數

例子3:

調用函數:myprintf("aaaa",123,st,'A');

那麼此時在記憶體中的表現就是這樣子:

8.從零開始實作printf函數

隻要修改的是myprintf函數:

8.從零開始實作printf函數

整體代碼如下:

#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;
	
 
}
           

編譯運作:

8.從零開始實作printf函數

例子4:

在上面的基礎上再增加一個參數:

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

然後直接修改myprintf函數,如下:

8.從零開始實作printf函數

這樣子修改可以嗎?試着運作一下:

8.從零開始實作printf函數

這也驗證了四位元組對齊,因為第四個參數是A,是char類型,隻占一個位元組,而GCC編譯器預設按4位元組對齊,是以存放 A字元,隻需要一個位元組的位址,其他三個應該都不存放有效值,如果需要尋找 B  的值,需要偏移3個位址,如下:

8.從零開始實作printf函數

此時簡圖如下:

8.從零開始實作printf函數

整體代碼如下:

#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;
	
 
}

           

編譯運作:

8.從零開始實作printf函數

檢視規律:

我們可以把上面的例子簡化成下面簡圖的方式,即為:兩個步驟,取值和移動指針。

8.從零開始實作printf函數

現如今程式修改如下,改變的隻有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;
}
           

運作程式,還是和上面的程式,正常列印出可變參數的資訊

8.從零開始實作printf函數

雖然我們簡化了這個程式的思路,但是我們還是需要手動靜态的去修改需要移動的指針,而且移動的距離也不是固定的,那麼有沒有一個函數能夠幫我們解決這個問題呢?

答案是可以的。

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中的定義(函數原型):

8.從零開始實作printf函數

②宏定義

8.從零開始實作printf函數

宏定義解析:

8.從零開始實作printf函數
8.從零開始實作printf函數
8.從零開始實作printf函數
8.從零開始實作printf函數

簡化上面的代碼如下:

#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;
	
 
}
           
此處注意:
8.從零開始實作printf函數

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的内容:

①首先把第四節說明的幾條宏定義複制過來,也就是下面這幾個宏定義

8.從零開始實作printf函數

②定義字元輸出和字元串輸出函數,如下,調用<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平台實作了列印的函數,可以進行調試:

8.從零開始實作printf函數

6.在ARM平台上實作自己的列印函數

現在需要在arm平台實作這個列印函數,肯定是要考慮到硬體,和上面在x86實作唯一不同的就是,putchar函數。

1).在x86平台上,我們調用的是系統 <stdio.h> 庫中的 putchar函數,輸出一個字元,也是通過putchar函數來輸出字元串的

2).是以在arm平台上,我們同樣去實作這樣一個底層函數,這個在上一節已經實作了,具體檢視上一節 點我檢視 。

解決了硬體問題了之後,剩下的就是軟體的問題了。

把上一節實作能在arm平台實作列印的所有檔案複制下來,再和上邊實作的兩個myprintf.c和myprintf.h檔案放在同一個檔案夾裡面。如下:

8.從零開始實作printf函數

 1.現在需要修改myprintf函數,把myprintf中的putchar函數,改成在ARM平台實作的putchar(也就是在uart.c中實作的putchar)

8.從零開始實作printf函數

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檔案,因為現在添加了幾個檔案同樣需要編譯,修改如下:

8.從零開始實作printf函數
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系統進行編譯:

8.從零開始實作printf函數

看一下bin檔案的大小,發現既然差不多有40K?

8.從零開始實作printf函數

如果直接燒錄到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開始

8.從零開始實作printf函數

直到下一個Disassembly of section .rodata:(隻讀段),之前都是屬于代碼段

8.從零開始實作printf函數

那麼代碼段的位址就是從: 0 到 0x9f0  . 他們是連續的。

從隻讀段繼續分析,可以看到代碼段和隻讀段是連續的,隻讀段從位址0x9f4開始

8.從零開始實作printf函數

同理到下一個section之前的都是隻讀段的反彙編代碼:

8.從零開始實作printf函數

是以隻讀段的代碼,是: 0x9f4 到 0x adc  ,也是連續的。

繼續檢視下一個section,然後就發現了,原來隻讀段和資料段的位址是不連續的而且相差了很多,這應該就是bin檔案那麼大的原因了。

8.從零開始實作printf函數

計算一下他們之間相差了幾個位元組: 0x8ae0  - 0xadc = 32772 位元組。其實這段空間是不放任何有效資料的,隻是空着。是以我們可以在編譯的時候指定資料段的起始位置。

指定原則,肯定需要比隻讀段的結束位址要大,那麼就制定為:0xae0

修改Makefile,如下:

8.從零開始實作printf函數
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系統編譯一下:

8.從零開始實作printf函數

可以看到 uart.bin檔案減小為2800位元組了(原來的是35568 ,減少了 35568 -2800 =32768),這個數值不就差不多等于我們修改的那個資料段和隻讀段的空缺部分嘛。

3).把bin檔案傳回window系統,使用oflash進行燒錄(燒錄到Nand Flash):

燒錄完成後,打開序列槽調試助手:

開發版連接配接序列槽,配置好序列槽資料格式:

8.從零開始實作printf函數

接着複位開發版,從主函數開始運作,列印資料,可以看到,這個和我們在x86平台上實作的功能是一模一樣的。

8.從零開始實作printf函數

附錄:源代碼

① x86平台實作的printf函數:https://download.csdn.net/download/qq_36243942/10887825

② arm平台實作的printf函數:https://download.csdn.net/download/qq_36243942/10887837